From d5116ebd9e9bc9983931bd13e12239e8001dc25d Mon Sep 17 00:00:00 2001 From: brianbrix Date: Sun, 9 Aug 2026 19:41:49 +0300 Subject: [PATCH 01/16] AMP-31197 : AMP-SEC-001 / 002 / 031: /rest/** bypasses Spring Security, and @ApiMethod.authTypes() defaults to no auth, --- .../components/fields/AmpComponentField.java | 2 +- .../endpoints/calendar/CalendarEndpoint.java | 3 +- .../endpoints/common/AmpConfiguration.java | 8 +- .../endpoints/common/CommonEndpoint.java | 6 +- .../endpoints/common/FiltersEndpoint.java | 84 ++++++++++--------- .../common/TranslationsEndPoints.java | 9 +- .../endpoints/dashboards/EndPoints.java | 10 +-- .../publicportal/PublicEndpoint.java | 11 +-- .../endpoints/security/ActionAuthorizer.java | 34 ++++++-- .../ampapi/endpoints/security/AuthRule.java | 8 +- .../ampapi/endpoints/util/ApiMethod.java | 6 +- .../ampapi/swagger/SwaggerAuthorization.java | 3 +- .../webapp/WEB-INF/applicationContext.xml | 10 ++- 13 files changed, 121 insertions(+), 73 deletions(-) diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/fields/AmpComponentField.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/fields/AmpComponentField.java index b05606f2df7..fd7d677bc63 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/fields/AmpComponentField.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/fields/AmpComponentField.java @@ -77,7 +77,7 @@ public AmpComponentField(String id, IModel activityModel, expeditures.setOutputMarkupPlaceholderTag(true); add(expeditures); } catch (Exception e) { - e.printStackTrace(); + e.printStackTrace(); } } diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/calendar/CalendarEndpoint.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/calendar/CalendarEndpoint.java index 27fd9da9518..a57efacc6fc 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/calendar/CalendarEndpoint.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/calendar/CalendarEndpoint.java @@ -2,6 +2,7 @@ import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; +import org.digijava.kernel.ampapi.endpoints.security.AuthRule; import org.digijava.kernel.ampapi.endpoints.util.ApiMethod; import org.digijava.kernel.ampapi.endpoints.util.CalendarUtil; import org.digijava.module.aim.dbentity.AmpFiscalCalendar; @@ -22,7 +23,7 @@ public class CalendarEndpoint { @GET @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = "getCalendar", ui = false) + @ApiMethod(id = "getCalendar", ui = false, authTypes = AuthRule.PUBLIC) @ApiOperation("Retrieve calendars.") public List getCalendars(@QueryParam("id") List id) { return CalendarUtil.getCalendars(id); diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/AmpConfiguration.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/AmpConfiguration.java index 8e44c035f6f..ae87a41f4fd 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/AmpConfiguration.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/AmpConfiguration.java @@ -68,7 +68,7 @@ public class AmpConfiguration { @GET @Path("/settings") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(ui = false, id = "Settings") + @ApiMethod(ui = false, id = "Settings", authTypes = AuthRule.PUBLIC) @ApiOperation( value = "Retrieve general AMP settings", notes = "This endpoint provides access to general AMP configuration settings including " + @@ -83,7 +83,7 @@ public Response getSettings() { @GET @Path("/settings/gis") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = "GisSettings") + @ApiMethod(id = "GisSettings", authTypes = AuthRule.PUBLIC) @ApiOperation("GIS settings") @ApiResponses(@ApiResponse(code = HttpServletResponse.SC_OK, message = "GIS settings", response = AmpGeneralSettings.class)) @@ -105,7 +105,7 @@ public Response describeTopsDashboard() { @GET @Path("/amp-offline-version-check") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(ui = false, id = "version-check") + @ApiMethod(ui = false, id = "version-check", authTypes = AuthRule.PUBLIC) @ApiOperation( value = "Check if AMP Offline App is compatible with current AMP version", notes = "This endpoint verifies compatibility between the AMP Offline application and the current AMP server version.\n\n" + @@ -212,7 +212,7 @@ public Map getGlobalSettings() { @GET @Path("global-settings/public") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(ui = false, id = "public-global-settings") + @ApiMethod(ui = false, id = "public-global-settings", authTypes = AuthRule.PUBLIC) @ApiOperation( value = "Retrieve all public AMP Global Settings", notes = "This endpoint provides access to public global configuration settings in the AMP system.\n\n" + diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/CommonEndpoint.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/CommonEndpoint.java index 559ad68c50c..e615cf43a4d 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/CommonEndpoint.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/CommonEndpoint.java @@ -40,7 +40,7 @@ public class CommonEndpoint implements AmpEndpoint { @POST @Path("/fm") @Produces(MediaType.APPLICATION_JSON) - @ApiMethod(ui = false, name = "fm", id = "") + @ApiMethod(ui = false, name = "fm", id = "", authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Provides FM (Feature Manager) settings for the requested options as a tree.") public FMSettingsResult getFMSettings( @ApiParam("FM Settings with requested options") FMSettingsConfig config) { @@ -50,7 +50,7 @@ public FMSettingsResult getFMSettings( @POST @Path("/fm/flat") @Produces(MediaType.APPLICATION_JSON) - @ApiMethod(ui = false, name = "fm", id = "") + @ApiMethod(ui = false, name = "fm", id = "", authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Provides FM (Feature Manager) settings for the requested options in flat mode.") public FMSettingsResult getFMSettingsFlat( @ApiParam("FM Settings with requested options") FMSettingsConfig config) { @@ -70,7 +70,7 @@ public Response describeGetFMSettingsFlat() { @POST @Path("/fm/flatAsResponse") @Produces(MediaType.APPLICATION_JSON) - @ApiMethod(ui = false, name = "fm", id = "") + @ApiMethod(ui = false, name = "fm", id = "", authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Provides FM (Feature Manager) settings for the requested options in flat mode.") public Response getFMSettingsFlatAsResponse( @ApiParam("FM Settings with requested options") FMSettingsConfig config) { diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/FiltersEndpoint.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/FiltersEndpoint.java index 15c5d623c94..15b50570551 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/FiltersEndpoint.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/FiltersEndpoint.java @@ -8,6 +8,7 @@ import org.dgfoundation.amp.ar.ColumnConstants; import org.digijava.kernel.ampapi.endpoints.dashboards.services.PublicServices; import org.digijava.kernel.ampapi.endpoints.filters.*; +import org.digijava.kernel.ampapi.endpoints.security.AuthRule; import org.digijava.kernel.ampapi.endpoints.util.*; import org.digijava.kernel.ampapi.postgis.util.QueryUtil; import org.digijava.kernel.request.TLSUtils; @@ -136,7 +137,7 @@ public List getAvailableFilters( @GET @Path("/organizations") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = "organizations", name = "Organizations") + @ApiMethod(id = "organizations", name = "Organizations", authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Organizations' filters.", notes = "The response contains 2 objects - the list definitions and the values. \n" + "The filter widget should create a tree for each organization type.") @@ -167,7 +168,7 @@ public Response describeOrganizations() { @GET @Path("/programs") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = "programs", name = "Programs") + @ApiMethod(id = "programs", name = "Programs", authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Programs' filters.", notes = "The response contains 2 objects - the list definitions and the values. \n" + "The filter widget should create a tree for each program settings.") @@ -197,7 +198,7 @@ public Response describePrograms() { @GET @Path("/sectors") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = "sectors", name = "Sectors") + @ApiMethod(id = "sectors", name = "Sectors", authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_SECTORS) @ApiOperation(value = "Retrieve the data needed for building the 'Sectors' filters.", notes = "The response contains 2 objects - the list definitions and the values. \n" @@ -227,7 +228,7 @@ public Response describeSectors() { @GET @Path("/locations") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = "locations", name = "Locations") + @ApiMethod(id = "locations", name = "Locations", authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_LOCATIONS) @ApiOperation(value = "Retrieve the data needed for building the 'Locations' filters.", notes = "The response contains 2 objects - the list definitions and the values. \n" @@ -259,7 +260,7 @@ public Response describeLocations() { @GET @Path("/activityApprovalStatus") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.APPROVAL_STATUS, name = ColumnConstants.APPROVAL_STATUS) + @ApiMethod(id = FiltersConstants.APPROVAL_STATUS, name = ColumnConstants.APPROVAL_STATUS, authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_ACTIVITY, columns = ColumnConstants.APPROVAL_STATUS, visibilityCheck = "hasToShowActivityApprovalStatusFilter") @ApiOperation(value = "Retrieve the data needed for building the 'Approval Status' filter.", @@ -277,7 +278,7 @@ public Response getActivityApprovalStatus() { @GET @Path("/typeOfAssistance/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.TYPE_OF_ASSISTANCE, name = ColumnConstants.TYPE_OF_ASSISTANCE) + @ApiMethod(id = FiltersConstants.TYPE_OF_ASSISTANCE, name = ColumnConstants.TYPE_OF_ASSISTANCE, authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_FINANCIALS, columns = ColumnConstants.TYPE_OF_ASSISTANCE) @ApiOperation(value = "Retrieve the data needed for building the 'Type of Assistance' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" @@ -294,7 +295,7 @@ public Response getTypeOfAssistance() { @GET @Path("/modalities/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.MODALITIES, name = ColumnConstants.MODALITIES) + @ApiMethod(id = FiltersConstants.MODALITIES, name = ColumnConstants.MODALITIES, authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_FINANCIALS, columns = ColumnConstants.MODALITIES) @ApiOperation(value = "Retrieve the data needed for building the 'Type of Assistance' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" @@ -313,7 +314,7 @@ public Response getModalities(@ApiParam(value = "Force SSC workspace, default fa @GET @Path("/modeOfPayment/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.MODE_OF_PAYMENT, name = ColumnConstants.MODE_OF_PAYMENT) + @ApiMethod(id = FiltersConstants.MODE_OF_PAYMENT, name = ColumnConstants.MODE_OF_PAYMENT, authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_FINANCIALS, columns = ColumnConstants.MODE_OF_PAYMENT) @ApiOperation(value = "Retrieve the data needed for building the 'Mode of Payment' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" @@ -330,7 +331,7 @@ public Response getModeOfPayment() { @GET @Path("/activityStatus/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.STATUS, name = FiltersConstants.ACTIVITY_STATUS_NAME) + @ApiMethod(id = FiltersConstants.STATUS, name = FiltersConstants.ACTIVITY_STATUS_NAME, authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Activity Status' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for 'Activity Status' values.") @@ -358,7 +359,7 @@ public Response describeActivityStatus() { @GET @Path("/activityBudget/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.ACTIVITY_BUDGET, name = FiltersConstants.ACTIVITY_BUDGET_NAME) + @ApiMethod(id = FiltersConstants.ACTIVITY_BUDGET, name = FiltersConstants.ACTIVITY_BUDGET_NAME, authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Activity Budget' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for 'Activity Budget' values.") @@ -375,7 +376,7 @@ public Response getActivityBudget() { @GET @Path("/procurementSystem/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.PROCUREMENT_SYSTEM, name = FiltersConstants.PROCUREMENT_SYSTEM_NAME) + @ApiMethod(id = FiltersConstants.PROCUREMENT_SYSTEM, name = FiltersConstants.PROCUREMENT_SYSTEM_NAME, authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Procurement System' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for 'Procurement System' values.") @@ -400,7 +401,7 @@ public Response describeProcurementSystem() { @GET @Path("/fundingStatus/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.FUNDING_STATUS) + @ApiMethod(id = FiltersConstants.FUNDING_STATUS, authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Funding Status' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for 'Funding Status' values.") @@ -417,7 +418,7 @@ public FilterList getFundingStatus() { @GET @Path("/expenditureClass/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.EXPENDITURE_CLASS, name = ColumnConstants.EXPENDITURE_CLASS) + @ApiMethod(id = FiltersConstants.EXPENDITURE_CLASS, name = ColumnConstants.EXPENDITURE_CLASS, authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Expenditure Class' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for 'Expenditure Class' values.") @@ -434,7 +435,7 @@ public FilterList getExpenditureClass() { @GET @Path("/concessionalityLevel/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.CONCESSIONALITY_LEVEL, name = ColumnConstants.CONCESSIONALITY_LEVEL) + @ApiMethod(id = FiltersConstants.CONCESSIONALITY_LEVEL, name = ColumnConstants.CONCESSIONALITY_LEVEL, authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Concessionality Level' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for 'Concessionality Level' values.") @@ -452,7 +453,8 @@ public FilterList getConcessionalityLevel() { @Path("/performanceAlertLevel") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") @ApiMethod(id = FiltersConstants.PERFORMANCE_ALERT_LEVEL, - columns = ColumnConstants.PERFORMANCE_ALERT_LEVEL, name = ColumnConstants.PERFORMANCE_ALERT_LEVEL) + columns = ColumnConstants.PERFORMANCE_ALERT_LEVEL, name = ColumnConstants.PERFORMANCE_ALERT_LEVEL, + authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Performance Alert Level' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for 'Performance Alert Level' values.") @@ -469,7 +471,7 @@ public FilterList getPerformanceAlertLevel() { @GET @Path("/financingInstruments/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.FINANCING_INSTRUMENT, name = ColumnConstants.FINANCING_INSTRUMENT) + @ApiMethod(id = FiltersConstants.FINANCING_INSTRUMENT, name = ColumnConstants.FINANCING_INSTRUMENT, authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Financing Instrument' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for 'Financing Instrument' values.") @@ -486,7 +488,7 @@ public FilterList getFinancingInstruments() { @GET @Path("/humanitarianAid/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.HUMANITARIAN_AID, name = ColumnConstants.HUMANITARIAN_AID) + @ApiMethod(id = FiltersConstants.HUMANITARIAN_AID, name = ColumnConstants.HUMANITARIAN_AID, authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Humanitarian Aid' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for 'Humanitarian Aid' values.") @@ -504,7 +506,7 @@ public FilterList getHumanitarianAid() { @Path("/disasterResponse/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") @ApiMethod(id = FiltersConstants.DISASTER_RESPONSE_MARKER, - name = ColumnConstants.DISASTER_RESPONSE_MARKER) + name = ColumnConstants.DISASTER_RESPONSE_MARKER, authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Disaster Response Marker' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for 'Disaster Response Marker' values.") @@ -521,7 +523,7 @@ public FilterList getDisasterResponse() { @GET @Path("/workspaces") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.TEAM, name = "Workspaces") + @ApiMethod(id = FiltersConstants.TEAM, name = "Workspaces", authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Workspaces' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for 'Workspaces' values.") @@ -539,7 +541,7 @@ public Response getWorkspaces() { @GET @Path("/computedYear") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.COMPUTED_YEAR, name = ColumnConstants.COMPUTED_YEAR) + @ApiMethod(id = FiltersConstants.COMPUTED_YEAR, name = ColumnConstants.COMPUTED_YEAR, authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Computed Year' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for 'Computed Year' values.") @@ -587,7 +589,7 @@ public Response getComputedYear() { @GET @Path("/date/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = "date", name = "Date") + @ApiMethod(id = "date", name = "Date", authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Date' filter.", notes = "This endpoint is used for fetching information about 'Date' filter. " + "The items attribute contains information about the startYear and the endYear.") @@ -599,7 +601,7 @@ public Response getDates() { @GET @Path("/proposedStartDate/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.PROPOSED_START_DATE, name = ColumnConstants.PROPOSED_START_DATE) + @ApiMethod(id = FiltersConstants.PROPOSED_START_DATE, name = ColumnConstants.PROPOSED_START_DATE, authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Generic endpoint for 'Proposed Start Date' filter.", hidden = true, notes = "Since the date filters doesn't have possible values, this endpoint returns an empty list.") @FilterDefinition(tab = EPConstants.TAB_OTHER, columns = ColumnConstants.PROPOSED_START_DATE, @@ -611,7 +613,7 @@ public FilterList getProposedStartDate() { @GET @Path("/actualStartDate/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.ACTUAL_START_DATE, name = ColumnConstants.ACTUAL_START_DATE) + @ApiMethod(id = FiltersConstants.ACTUAL_START_DATE, name = ColumnConstants.ACTUAL_START_DATE, authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_OTHER, columns = ColumnConstants.ACTUAL_START_DATE, fieldType = FilterFieldType.DATE_RANGE, dataType = FilterDataType.DATE) @ApiOperation(value = "Generic endpoint for 'Actual Start Date' filter.", hidden = true, @@ -623,7 +625,7 @@ public FilterList getActualStartDate() { @GET @Path("/actualApprovalDate/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.ACTUAL_APPROVAL_DATE, name = ColumnConstants.ACTUAL_APPROVAL_DATE) + @ApiMethod(id = FiltersConstants.ACTUAL_APPROVAL_DATE, name = ColumnConstants.ACTUAL_APPROVAL_DATE, authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_OTHER, columns = ColumnConstants.ACTUAL_APPROVAL_DATE, fieldType = FilterFieldType.DATE_RANGE, dataType = FilterDataType.DATE) @ApiOperation(value = "Generic endpoint for 'Actual Approval Date' filter.", hidden = true, @@ -635,7 +637,7 @@ public FilterList getActualApprovalDate() { @GET @Path("/actualCompletionDate/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.ACTUAL_COMPLETION_DATE, name = ColumnConstants.ACTUAL_COMPLETION_DATE) + @ApiMethod(id = FiltersConstants.ACTUAL_COMPLETION_DATE, name = ColumnConstants.ACTUAL_COMPLETION_DATE, authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_OTHER, columns = ColumnConstants.ACTUAL_COMPLETION_DATE, fieldType = FilterFieldType.DATE_RANGE, dataType = FilterDataType.DATE) @ApiOperation(value = "Generic endpoint for 'Actual Completion Date' filter.", hidden = true, @@ -647,7 +649,7 @@ public FilterList getActualCompletionDate() { @GET @Path("/effectiveFundingDate/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.EFFECTIVE_FUNDING_DATE, name = ColumnConstants.EFFECTIVE_FUNDING_DATE) + @ApiMethod(id = FiltersConstants.EFFECTIVE_FUNDING_DATE, name = ColumnConstants.EFFECTIVE_FUNDING_DATE, authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_FINANCIALS, columns = ColumnConstants.EFFECTIVE_FUNDING_DATE, fieldType = FilterFieldType.DATE_RANGE, dataType = FilterDataType.DATE) @ApiOperation(value = "Generic endpoint for 'Effective Funding Date' filter.", hidden = true, @@ -659,7 +661,7 @@ public FilterList getEffectiveFundingDate() { @GET @Path("/finalDateContracting/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.FINAL_DATE_FOR_CONTRACTING, name = ColumnConstants.FINAL_DATE_FOR_CONTRACTING) + @ApiMethod(id = FiltersConstants.FINAL_DATE_FOR_CONTRACTING, name = ColumnConstants.FINAL_DATE_FOR_CONTRACTING, authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_OTHER, columns = ColumnConstants.FINAL_DATE_FOR_CONTRACTING, fieldType = FilterFieldType.DATE_RANGE, dataType = FilterDataType.DATE) @ApiOperation(value = "Generic endpoint for 'Final Date for Contracting' filter.", hidden = true, @@ -671,7 +673,7 @@ public FilterList getFinalDateForContracting() { @GET @Path("/fundingClosingDate/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.FUNDING_CLOSING_DATE, name = ColumnConstants.FUNDING_CLOSING_DATE) + @ApiMethod(id = FiltersConstants.FUNDING_CLOSING_DATE, name = ColumnConstants.FUNDING_CLOSING_DATE, authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_FINANCIALS, columns = ColumnConstants.FUNDING_CLOSING_DATE, fieldType = FilterFieldType.DATE_RANGE, dataType = FilterDataType.DATE) @ApiOperation(value = "Generic endpoint for 'Funding Closing Date' filter.", hidden = true, @@ -683,7 +685,7 @@ public FilterList getFundingClosingDate() { @GET @Path("/issueDate/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.ISSUE_DATE, name = ColumnConstants.ISSUE_DATE) + @ApiMethod(id = FiltersConstants.ISSUE_DATE, name = ColumnConstants.ISSUE_DATE, authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_OTHER, columns = ColumnConstants.ISSUE_DATE, fieldType = FilterFieldType.DATE_RANGE, dataType = FilterDataType.DATE) @ApiOperation(value = "Generic endpoint for 'Issue Date' filter.", hidden = true, @@ -695,7 +697,7 @@ public FilterList getIssueDate() { @GET @Path("/proposedApprovalDate/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.PROPOSED_APPROVAL_DATE, name = ColumnConstants.PROPOSED_APPROVAL_DATE) + @ApiMethod(id = FiltersConstants.PROPOSED_APPROVAL_DATE, name = ColumnConstants.PROPOSED_APPROVAL_DATE, authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_OTHER, columns = ColumnConstants.PROPOSED_APPROVAL_DATE, fieldType = FilterFieldType.DATE_RANGE, dataType = FilterDataType.DATE) @ApiOperation(value = "Generic endpoint for 'Proposed Approval Date' filter.", hidden = true, @@ -707,7 +709,7 @@ public FilterList getProposedApprovalDate() { @GET @Path("/proposedCompletionDate/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.PROPOSED_COMPLETION_DATE, name = ColumnConstants.PROPOSED_COMPLETION_DATE) + @ApiMethod(id = FiltersConstants.PROPOSED_COMPLETION_DATE, name = ColumnConstants.PROPOSED_COMPLETION_DATE, authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_OTHER, columns = ColumnConstants.PROPOSED_COMPLETION_DATE, fieldType = FilterFieldType.DATE_RANGE, dataType = FilterDataType.DATE) @ApiOperation(value = "Generic endpoint for 'Proposed Completion Date' filter.", hidden = true, @@ -726,7 +728,7 @@ public FilterList getProposedCompletionDate() { @GET @Path("/pledgesDonors") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = "pledges-donors", name = "Pledges Donors") + @ApiMethod(id = "pledges-donors", name = "Pledges Donors", authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Pledges Donors' filter.", notes = "The response contains 2 objects - the list definitions and the values. \n" + "The filter widget should create a tree for 'Pledges Donors' values.") @@ -745,7 +747,7 @@ public Response getPledgesDonros() { @GET @Path("/pledgesPrograms") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = "pledges-programs", name = "Pledges Programs") + @ApiMethod(id = "pledges-programs", name = "Pledges Programs", authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Pledges Programs' filter.", notes = "The response contains 2 objects - the list definitions and the values. \n" + "The filter widget should create a tree for each pledge program settings.") @@ -764,7 +766,7 @@ public Response getPledgesPrograms() { @GET @Path("/pledgesSectors") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = "pledges-sectors", name = "Pledges Sectors") + @ApiMethod(id = "pledges-sectors", name = "Pledges Sectors", authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Pledges Sectors' filter.", notes = "The response contains 2 objects - the list definitions and the values. \n" + "The filter widget should create a tree for each pledge sector scheme.") @@ -784,7 +786,7 @@ public Response getPledgesSectors() { @GET @Path("/pledgesLocations") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = "pledges-locations", name = "Pledges Locations") + @ApiMethod(id = "pledges-locations", name = "Pledges Locations", authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Pledges Locations' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for each country.") @@ -801,7 +803,7 @@ public Response getPledgesLocations() { @GET @Path("/pledgesStatus/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.STATUS, name = ColumnConstants.PLEDGE_STATUS) + @ApiMethod(id = FiltersConstants.STATUS, name = ColumnConstants.PLEDGE_STATUS, authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Pledges Status' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for 'Pledges Status' values.") @@ -819,7 +821,7 @@ public Response getPledgesStatus() { @GET @Path("/pledgesAidOfModality/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.PLEDGES_AID_MODALITY, name = ColumnConstants.PLEDGES_AID_MODALITY) + @ApiMethod(id = FiltersConstants.PLEDGES_AID_MODALITY, name = ColumnConstants.PLEDGES_AID_MODALITY, authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Pledges Aid of Modality' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for 'Pledges Aid of Modality' values.") @@ -837,7 +839,7 @@ public Response getPledgesAidOfModality() { @GET @Path("/pledgesTypeOfAssistance/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.PLEDGES_TYPE_OF_ASSISTANCE, name = ColumnConstants.PLEDGES_TYPE_OF_ASSISTANCE) + @ApiMethod(id = FiltersConstants.PLEDGES_TYPE_OF_ASSISTANCE, name = ColumnConstants.PLEDGES_TYPE_OF_ASSISTANCE, authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Pledges Type of Assistance' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for 'Pledges Type of Assistance' values.") @@ -850,7 +852,7 @@ public FilterList getPledgesTypeOfAssistance() { @GET @Path("/pledgesDetailStartDate/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.PLEDGES_DETAIL_START_DATE, name = ColumnConstants.PLEDGES_DETAIL_START_DATE) + @ApiMethod(id = FiltersConstants.PLEDGES_DETAIL_START_DATE, name = ColumnConstants.PLEDGES_DETAIL_START_DATE, authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_OTHER, columns = ColumnConstants.PLEDGES_DETAIL_START_DATE, fieldType = FilterFieldType.DATE_RANGE, dataType = FilterDataType.DATE, componentType = {FilterComponentType.REPORTS}, reportType = FilterReportType.PLEDGE) @@ -863,7 +865,7 @@ public FilterList getPledgesDetailStartDate() { @GET @Path("/pledgesDetailEndDate/") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = FiltersConstants.PLEDGES_DETAIL_END_DATE, name = ColumnConstants.PLEDGES_DETAIL_END_DATE) + @ApiMethod(id = FiltersConstants.PLEDGES_DETAIL_END_DATE, name = ColumnConstants.PLEDGES_DETAIL_END_DATE, authTypes = AuthRule.PUBLIC) @FilterDefinition(tab = EPConstants.TAB_OTHER, columns = ColumnConstants.PLEDGES_DETAIL_END_DATE, fieldType = FilterFieldType.DATE_RANGE, dataType = FilterDataType.DATE, componentType = {FilterComponentType.REPORTS}, reportType = FilterReportType.PLEDGE) @@ -881,7 +883,7 @@ public FilterList getPledgesDetailEndDate() { @GET @Path("/boundaries") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = "boundaries", name = "Boundaries") + @ApiMethod(id = "boundaries", name = "Boundaries", authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Retrieve the data needed for building the 'Boundaries' filter.", notes = "The response contains 2 objects - the filter definition and the values. \n" + "The filter widget should create a tree for 'Boundaries' values.") diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/TranslationsEndPoints.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/TranslationsEndPoints.java index bc3ed38e60b..1fb9e7491e6 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/TranslationsEndPoints.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/common/TranslationsEndPoints.java @@ -2,6 +2,7 @@ import io.swagger.annotations.*; import org.digijava.kernel.ampapi.endpoints.dto.Language; +import org.digijava.kernel.ampapi.endpoints.security.AuthRule; import org.digijava.kernel.ampapi.endpoints.util.ApiMethod; import org.digijava.kernel.ampapi.endpoints.util.AvailableMethod; import org.digijava.kernel.entity.Locale; @@ -65,7 +66,7 @@ public List getAvailableMethods() { ) }) )) - @ApiMethod(ui = false, id = "Translations") + @ApiMethod(ui = false, id = "Translations", authTypes = AuthRule.PUBLIC) public Map getLangPack( @ApiParam(name = "param", required = true, value = "Key-label pairs to translate", examples = @@ -108,7 +109,7 @@ public Map getLangPack( ) }) )) - @ApiMethod(ui = false, id = "CustomLanguageTranslations") + @ApiMethod(ui = false, id = "CustomLanguageTranslations", authTypes = AuthRule.PUBLIC) public Map getLangPack(@PathParam("langCode") @ApiParam(example = "en") String langCode, @ApiParam(name = "param", required = true, value = "Key-label pairs to translate") @@ -131,7 +132,7 @@ public Map getLangPack(@PathParam("langCode") @ApiParam(example @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") @Consumes(MediaType.APPLICATION_JSON + ";charset=utf-8") @ApiOperation("Get the list of languages used in AMP.") - @ApiMethod(ui = false, id = "languages") + @ApiMethod(ui = false, id = "languages", authTypes = AuthRule.PUBLIC) public List getLanguages() { return TranslationManager.getAmpLanguages(); } @@ -139,7 +140,7 @@ public List getLanguages() { @GET @Path("/languages/{langCode}") @ApiOperation("Change the language used in session.") - @ApiMethod(ui = false, id = "LanguageSwitch") + @ApiMethod(ui = false, id = "LanguageSwitch", authTypes = AuthRule.PUBLIC) public String switchLanguage(@PathParam("langCode") @ApiParam(name = "langCode", example = "fr") String langCode, @Context HttpServletResponse response) { Locale locale = new Locale(); diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/dashboards/EndPoints.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/dashboards/EndPoints.java index 579988c8e40..c23f58fd24e 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/dashboards/EndPoints.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/dashboards/EndPoints.java @@ -94,7 +94,7 @@ public Response describeTopsDashboard() { @Path("/tops/{type}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(ui = false, id = "tops") + @ApiMethod(ui = false, id = "tops", authTypes = AuthRule.PUBLIC) @ApiOperation("Get top funding by property") @ApiResponses(@ApiResponse(code = HttpServletResponse.SC_OK, message = "Top chart data", response = TopChartData.class)) @@ -153,7 +153,7 @@ public Response describeFundingTypeDashboard() { @POST @Path("/ftype") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(ui = false, id = "ftype") + @ApiMethod(ui = false, id = "ftype", authTypes = AuthRule.PUBLIC) @ApiOperation("Get funding type chart data") @ApiResponses(@ApiResponse(code = HttpServletResponse.SC_OK, message = "Funding type chart data", response = FundingTypeChartData.class)) @@ -173,7 +173,7 @@ public Response describeFinancingInstrument() { @POST @Path("/finstrument") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(ui = false, id = "ftype") + @ApiMethod(ui = false, id = "ftype", authTypes = AuthRule.PUBLIC) @ApiOperation("Get financing instrument chart data") @ApiResponses(@ApiResponse(code = HttpServletResponse.SC_OK, message = "Funding istrumet chart data", response = FundingTypeChartData.class)) @@ -454,7 +454,7 @@ public Response describeIndicatorReportsByProgramCountry() { @POST @Path("/me/indicatorReportsByProgramCountry") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = "getValuesForIndicatorsCountryProgram") + @ApiMethod(id = "getValuesForIndicatorsCountryProgram", authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Returns indicator report values for all indicators.") public Response getIndicatorYearValuesByIndicatorsCountryProgram(SettingsAndFiltersParameters params) { List resp = new MeService().getIndicatorYearValuesByIndicatorCountryProgramId(params); @@ -473,7 +473,7 @@ public final List getIndicatorsBySector(@PathParam("id") Long se @POST @Path("/me/dashboardCoreIndicatorData") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(id = "getCoreIndicatorData") + @ApiMethod(id = "getCoreIndicatorData", authTypes = AuthRule.PUBLIC) @ApiOperation(value = "Returns indicator report values for all indicators.") public Response getCoreIndicatorData(ReportFormParameters formParams) { ReportSpecificationImpl diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/PublicEndpoint.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/PublicEndpoint.java index 84a1250c578..e0b14a587c5 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/PublicEndpoint.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/publicportal/PublicEndpoint.java @@ -14,6 +14,7 @@ import org.digijava.kernel.ampapi.endpoints.publicportal.dto.PublicTotalsByMeasure; import org.digijava.kernel.ampapi.endpoints.reports.ReportFormParameters; import org.digijava.kernel.ampapi.endpoints.reports.ReportsUtil; +import org.digijava.kernel.ampapi.endpoints.security.AuthRule; import org.digijava.kernel.ampapi.endpoints.util.ApiMethod; import javax.servlet.http.HttpServletResponse; @@ -40,7 +41,7 @@ public class PublicEndpoint { @POST @Path("/topprojects") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(ui = false, id = "topprojects") + @ApiMethod(ui = false, id = "topprojects", authTypes = AuthRule.PUBLIC) @ApiOperation("Retrieves top 'count' projects based on fixed requirements.") @ApiResponses(@ApiResponse(code = HttpServletResponse.SC_OK, message = "Top projects", response = PublicTopData.class)) @@ -64,7 +65,7 @@ public Response describeTopProjects() { @POST @Path("/donorFunding") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(ui = false, id = "donorFunding") + @ApiMethod(ui = false, id = "donorFunding", authTypes = AuthRule.PUBLIC) @ApiOperation( value = "Retrieves Donor Disbursements/Commitments List for the last X days", notes = "Get donor funding for the specific funding type " @@ -98,7 +99,7 @@ public Response describeDonorFunding() { @POST @Path("/activitiesPledges") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(ui = false, id = "activitiesPledges") + @ApiMethod(ui = false, id = "activitiesPledges", authTypes = AuthRule.PUBLIC) @ApiOperation("Retrieves the count for activities that have been at least linked to one pledge") public int getActivitiesPledgesCount(@ApiParam(required = true) PublicReportFormParameters config) { return PublicPortalService.getActivitiesCount(config != null ? config.getFilters() : null, true); @@ -107,7 +108,7 @@ public int getActivitiesPledgesCount(@ApiParam(required = true) PublicReportForm @POST @Path("/totalByMeasure") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(ui = false, id = "getTopByMeasure") + @ApiMethod(ui = false, id = "getTopByMeasure", authTypes = AuthRule.PUBLIC) @ApiOperation("Total funding by measure") @ApiResponses(@ApiResponse(code = HttpServletResponse.SC_OK, message = "Top chart data", response = PublicTotalsByMeasure.class)) @@ -129,7 +130,7 @@ public Response describeTotalByMeasure() { @POST @Path("/projectCount") @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") - @ApiMethod(ui = false, id = "projectCount") + @ApiMethod(ui = false, id = "projectCount", authTypes = AuthRule.PUBLIC) @ApiOperation(value = "get total project count respecting filters") @ApiResponses(@ApiResponse(code = HttpServletResponse.SC_OK, message = "Top chart data", response = PublicTotalsByMeasure.class)) diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/ActionAuthorizer.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/ActionAuthorizer.java index 23624380823..2fe712e41b1 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/ActionAuthorizer.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/ActionAuthorizer.java @@ -20,6 +20,7 @@ import java.lang.reflect.Method; import java.util.Collection; +import java.util.Collections; import java.util.Map; import java.util.TreeMap; @@ -39,15 +40,38 @@ public class ActionAuthorizer { .addRuleDependency(AuthRule.VIEW_ACTIVITY, AuthRule.IN_WORKSPACE) .build(); + /** + * Returns true if the given ApiMethod is explicitly opted out of authorization via {@link AuthRule#PUBLIC}. + */ + private static boolean isExplicitlyPublic(ApiMethod apiMethod) { + for (AuthRule authRule : apiMethod.authTypes()) { + if (authRule == AuthRule.PUBLIC) { + return true; + } + } + return false; + } + + /** + * Returns the effective auth rules for the given ApiMethod, defaulting to {@link AuthRule#AUTHENTICATED} + * (fail-closed) when no authTypes were declared at all. + */ + private static Collection getEffectiveRules(ApiMethod apiMethod) { + if (apiMethod.authTypes().length == 0) { + return Collections.singleton(AuthRule.AUTHENTICATED); + } + return ruleHierarchy.getEffectiveRules(apiMethod.authTypes()); + } + /** * Returns true if the given ApiMethod requires an authenticated user session. * Used by request filters to decide whether CSRF protection is warranted. */ public static boolean requiresAuthentication(ApiMethod apiMethod) { - if (apiMethod.authTypes().length == 0) { + if (isExplicitlyPublic(apiMethod)) { return false; } - Collection authRules = ruleHierarchy.getEffectiveRules(apiMethod.authTypes()); + Collection authRules = getEffectiveRules(apiMethod); return authRules.contains(AuthRule.AUTHENTICATED); } @@ -58,12 +82,12 @@ public static boolean requiresAuthentication(ApiMethod apiMethod) { * @param containerReq general container request to be used for additional information */ public static void authorize(Method method, ApiMethod apiMethod, ContainerRequest containerReq) { - if (apiMethod.authTypes().length == 0) { - // no authorization -> nothing to check, skip immediately + if (isExplicitlyPublic(apiMethod)) { + // explicitly and deliberately open to anonymous callers -> nothing to check, skip immediately return; } - Collection authRules = ruleHierarchy.getEffectiveRules(apiMethod.authTypes()); + Collection authRules = getEffectiveRules(apiMethod); logger.info("Authenticated: "+AuthRule.AUTHENTICATED +"User: "+TeamUtil.getCurrentUser()); if (authRules.contains(AuthRule.AUTHENTICATED) && TeamUtil.getCurrentUser() == null) { diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/AuthRule.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/AuthRule.java index dc2f4d2a175..84fa281d81d 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/AuthRule.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/AuthRule.java @@ -26,7 +26,13 @@ public enum AuthRule { /** if amp offline user-agent is present in headers check for AMP_OFFLINE. If not, check for other actions */ AMP_OFFLINE_OPTIONAL, /** Current rule: If activity was created in private ws, it can only be access from there **/ - PUBLIC_VIEW_ACTIVITY; + PUBLIC_VIEW_ACTIVITY, + /** + * Explicitly marks a method as intentionally open to anonymous callers. + * Must be used deliberately: a method with no authTypes at all is treated as + * {@link #AUTHENTICATED} by default (see {@link org.digijava.kernel.ampapi.endpoints.security.ActionAuthorizer}). + */ + PUBLIC; @Override public String toString() { diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/util/ApiMethod.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/util/ApiMethod.java index 386b2258c95..5c9f7e03549 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/util/ApiMethod.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/util/ApiMethod.java @@ -64,7 +64,11 @@ String visibilityCheck() default ""; /** - * Authorization rules that must be applied to this method. Default is no authorization to be done. + * Authorization rules that must be applied to this method. + * + *

Fail-closed default: when left empty, the method still requires an authenticated session + * (equivalent to {@link AuthRule#AUTHENTICATED}). Use {@link AuthRule#PUBLIC} explicitly to allow + * anonymous access.

*/ AuthRule[] authTypes() default {}; } diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/swagger/SwaggerAuthorization.java b/amp/src/main/java/org/digijava/kernel/ampapi/swagger/SwaggerAuthorization.java index 4e41bcd5a3b..a2a26e4c732 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/swagger/SwaggerAuthorization.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/swagger/SwaggerAuthorization.java @@ -20,7 +20,8 @@ public class SwaggerAuthorization extends AbstractSwaggerExtension { private static final Set IGNORE_RULES = new TreeSet<>(Arrays.asList( AuthRule.AMP_OFFLINE, AuthRule.AMP_OFFLINE_OPTIONAL, - AuthRule.PUBLIC_VIEW_ACTIVITY)); + AuthRule.PUBLIC_VIEW_ACTIVITY, + AuthRule.PUBLIC)); @Override public void decorateOperation(Operation operation, Method method, Iterator chain) { diff --git a/amp/src/main/webapp/WEB-INF/applicationContext.xml b/amp/src/main/webapp/WEB-INF/applicationContext.xml index 7af9c51952d..954c0e610a7 100644 --- a/amp/src/main/webapp/WEB-INF/applicationContext.xml +++ b/amp/src/main/webapp/WEB-INF/applicationContext.xml @@ -109,7 +109,15 @@ - + + + + + + + From 5193561020bbab9625a8c9e6de7bc266dffdbc79 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Sun, 9 Aug 2026 21:03:32 +0300 Subject: [PATCH 02/16] AMP-31197 : AMP-SEC-017 / 054: AMP passwords are stored and compared in plaintext via NoOpPasswordEncoder --- .../endpoints/security/SecurityService.java | 14 +++--- .../security/services/UserManagerService.java | 7 ++- .../security/auth/AmpPasswordEncoder.java | 46 +++++++++++++++++++ .../kernel/util/DigiSchemaPopulate.java | 3 +- .../module/aim/action/RegisterUser.java | 8 ++-- .../startup/AmpBackgroundActivitiesUtil.java | 7 +-- .../module/um/action/RegisterUser.java | 8 ++-- .../module/um/action/UserRegister.java | 8 ++-- .../module/um/action/UserRegisterBlank.java | 8 ++-- .../webapp/WEB-INF/applicationContext.xml | 5 +- 10 files changed, 88 insertions(+), 26 deletions(-) create mode 100644 amp/src/main/java/org/digijava/kernel/security/auth/AmpPasswordEncoder.java diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/SecurityService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/SecurityService.java index 73762c6b17c..5f2af74289a 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/SecurityService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/SecurityService.java @@ -14,6 +14,7 @@ import org.digijava.kernel.ampapi.endpoints.security.dto.*; import org.digijava.kernel.request.SiteDomain; import org.digijava.kernel.request.TLSUtils; +import org.digijava.kernel.security.auth.AmpPasswordEncoder; import org.digijava.kernel.services.AmpVersionInfo; import org.digijava.kernel.services.AmpVersionService; import org.digijava.kernel.translator.TranslatorWorker; @@ -38,8 +39,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -206,13 +205,16 @@ public UserSessionInformation authenticate(AuthenticationRequest authRequest) { } User user = UserUtils.getUserByEmailAddress(username); - String storedPassword = (user != null && user.getPassword() != null) ? user.getPassword() : ""; - boolean passwordMatches = MessageDigest.isEqual( - storedPassword.getBytes(StandardCharsets.UTF_8), - password.getBytes(StandardCharsets.UTF_8)); + String storedPassword = (user != null) ? user.getPassword() : null; + AmpPasswordEncoder passwordEncoder = new AmpPasswordEncoder(); + boolean passwordMatches = storedPassword != null && passwordEncoder.matches(password, storedPassword); if (user == null || !passwordMatches) { ApiErrorResponseService.reportForbiddenAccess(SecurityErrors.INVALID_USER_PASSWORD); } + if (!passwordEncoder.isHashed(storedPassword)) { + // opportunistically upgrade legacy plaintext password to a bcrypt hash on successful login + user.setPassword(passwordEncoder.encode(password)); + } ApiErrorMessage result = ApiAuthentication.login(user, TLSUtils.getRequest()); if (result != null) { diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/services/UserManagerService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/services/UserManagerService.java index f36cc0f31db..92879e9b7c0 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/services/UserManagerService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/services/UserManagerService.java @@ -32,6 +32,7 @@ import org.digijava.kernel.request.SiteDomain; import org.digijava.kernel.request.TLSUtils; import org.digijava.kernel.security.PasswordPolicyValidator; +import org.digijava.kernel.security.auth.AmpPasswordEncoder; import org.digijava.kernel.services.AmpVersionInfo; import org.digijava.kernel.services.AmpVersionService; import org.digijava.kernel.translator.TranslatorWorker; @@ -114,8 +115,10 @@ public LoggedUserInformation createUser(CreateUserRequest createUser) { user.setFirstNames(firstName); user.setLastName(lastName); user.setEmail(email); - user.setPassword(password); - user.setSalt(password); + // AMP-SEC-017/054: never store the plaintext password + String hashedPassword = new AmpPasswordEncoder().encode(password); + user.setPassword(hashedPassword); + user.setSalt(hashedPassword); user.setNotificationEmailEnabled(notificationEmailEnabled); if(notificationEmailEnabled){ user.setNotificationEmail(notificationEmail); diff --git a/amp/src/main/java/org/digijava/kernel/security/auth/AmpPasswordEncoder.java b/amp/src/main/java/org/digijava/kernel/security/auth/AmpPasswordEncoder.java new file mode 100644 index 00000000000..634e6a90bdc --- /dev/null +++ b/amp/src/main/java/org/digijava/kernel/security/auth/AmpPasswordEncoder.java @@ -0,0 +1,46 @@ +package org.digijava.kernel.security.auth; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.regex.Pattern; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * Hashes new/changed passwords with bcrypt (replaces {@code NoOpPasswordEncoder}), + * while still being able to verify accounts whose password was stored in plaintext before this + * encoder was introduced, so existing users are not locked out. + */ +public class AmpPasswordEncoder implements PasswordEncoder { + + private static final Pattern BCRYPT_PATTERN = Pattern.compile("^\\$2[aby]\\$\\d{2}\\$.{53}$"); + + private final BCryptPasswordEncoder delegate = new BCryptPasswordEncoder(); + + @Override + public String encode(CharSequence rawPassword) { + return delegate.encode(rawPassword); + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + if (rawPassword == null || encodedPassword == null) { + return false; + } + if (isHashed(encodedPassword)) { + return delegate.matches(rawPassword, encodedPassword); + } + // legacy account: password column still holds the plaintext value, compare directly + return MessageDigest.isEqual( + encodedPassword.getBytes(StandardCharsets.UTF_8), + rawPassword.toString().getBytes(StandardCharsets.UTF_8)); + } + + /** + * @return true if the stored value is already a bcrypt hash produced by this encoder + */ + public boolean isHashed(String storedPassword) { + return storedPassword != null && BCRYPT_PATTERN.matcher(storedPassword).matches(); + } +} diff --git a/amp/src/main/java/org/digijava/kernel/util/DigiSchemaPopulate.java b/amp/src/main/java/org/digijava/kernel/util/DigiSchemaPopulate.java index 0033e359ee7..69e8bbefd2b 100644 --- a/amp/src/main/java/org/digijava/kernel/util/DigiSchemaPopulate.java +++ b/amp/src/main/java/org/digijava/kernel/util/DigiSchemaPopulate.java @@ -294,7 +294,8 @@ static void createGlobalAdmin() throws Exception { user.setFirstNames("System"); user.setLastName("System"); user.setEmail("system@digijava.org"); - user.setPassword("changeme"); + // AMP-SEC-017/054: never store the plaintext password + user.setPassword(new org.digijava.kernel.security.auth.AmpPasswordEncoder().encode("changeme")); user.setRegisterLanguage(english); user.setBanned(false); user.setOrganizationTypeOther(" "); diff --git a/amp/src/main/java/org/digijava/module/aim/action/RegisterUser.java b/amp/src/main/java/org/digijava/module/aim/action/RegisterUser.java index a5d56cbc456..475a840fdca 100644 --- a/amp/src/main/java/org/digijava/module/aim/action/RegisterUser.java +++ b/amp/src/main/java/org/digijava/module/aim/action/RegisterUser.java @@ -14,6 +14,7 @@ import org.digijava.kernel.request.Site; import org.digijava.kernel.request.SiteDomain; import org.digijava.kernel.security.PasswordPolicyValidator; +import org.digijava.kernel.security.auth.AmpPasswordEncoder; import org.digijava.kernel.user.Group; import org.digijava.kernel.user.User; import org.digijava.kernel.util.DgUtil; @@ -55,9 +56,10 @@ public ActionForward execute(ActionMapping mapping, ActionForm form, request.setAttribute(PasswordPolicyValidator.SHOW_PASSWORD_POLICY_RULES, true); return (mapping.getInputForward()); } - // set password - user.setPassword(userRegisterForm.getPassword().trim()); - user.setSalt(userRegisterForm.getPassword().trim()); + // set password (AMP-SEC-017/054: never store the plaintext password) + String hashedPassword = new AmpPasswordEncoder().encode(userRegisterForm.getPassword().trim()); + user.setPassword(hashedPassword); + user.setSalt(hashedPassword); // set Website user.setUrl(userRegisterForm.getWebSite()); diff --git a/amp/src/main/java/org/digijava/module/aim/startup/AmpBackgroundActivitiesUtil.java b/amp/src/main/java/org/digijava/module/aim/startup/AmpBackgroundActivitiesUtil.java index 480f9669065..aef77b852a8 100644 --- a/amp/src/main/java/org/digijava/module/aim/startup/AmpBackgroundActivitiesUtil.java +++ b/amp/src/main/java/org/digijava/module/aim/startup/AmpBackgroundActivitiesUtil.java @@ -93,9 +93,10 @@ protected static void createAmpValidatorUser(String userEmail, String firstNames // set client IP address user.setModifyingIP("0.0.0.0"); - // set password - user.setPassword(AMP_USER_PASSWORD); - user.setSalt(AMP_USER_PASSWORD); + // set password (AMP-SEC-017/054: never store the plaintext password) + String hashedPassword = new org.digijava.kernel.security.auth.AmpPasswordEncoder().encode(AMP_USER_PASSWORD); + user.setPassword(hashedPassword); + user.setSalt(hashedPassword); // set Website user.setUrl("/"); diff --git a/amp/src/main/java/org/digijava/module/um/action/RegisterUser.java b/amp/src/main/java/org/digijava/module/um/action/RegisterUser.java index a0238dea28a..96c26f1aebb 100644 --- a/amp/src/main/java/org/digijava/module/um/action/RegisterUser.java +++ b/amp/src/main/java/org/digijava/module/um/action/RegisterUser.java @@ -17,6 +17,7 @@ import org.digijava.kernel.request.Site; import org.digijava.kernel.request.SiteDomain; import org.digijava.kernel.security.PasswordPolicyValidator; +import org.digijava.kernel.security.auth.AmpPasswordEncoder; import org.digijava.kernel.translator.TranslatorWorker; import org.digijava.kernel.user.Group; import org.digijava.kernel.user.User; @@ -71,9 +72,10 @@ public ActionForward execute(ActionMapping mapping, ActionForm form, request.setAttribute(PasswordPolicyValidator.SHOW_PASSWORD_POLICY_RULES, true); return (mapping.getInputForward()); } - // set password - user.setPassword(userRegisterForm.getPassword().trim()); - user.setSalt(userRegisterForm.getPassword().trim()); + // set password (AMP-SEC-017/054: never store the plaintext password) + String hashedPassword = new AmpPasswordEncoder().encode(userRegisterForm.getPassword().trim()); + user.setPassword(hashedPassword); + user.setSalt(hashedPassword); // set Website user.setUrl(userRegisterForm.getWebSite()); diff --git a/amp/src/main/java/org/digijava/module/um/action/UserRegister.java b/amp/src/main/java/org/digijava/module/um/action/UserRegister.java index 28c28a04ca7..8fece628ebb 100644 --- a/amp/src/main/java/org/digijava/module/um/action/UserRegister.java +++ b/amp/src/main/java/org/digijava/module/um/action/UserRegister.java @@ -34,6 +34,7 @@ import org.digijava.kernel.entity.UserLangPreferences; import org.digijava.kernel.entity.UserPreferences; import org.digijava.kernel.request.SiteDomain; +import org.digijava.kernel.security.auth.AmpPasswordEncoder; import org.digijava.kernel.user.User; import org.digijava.kernel.util.DgUtil; import org.digijava.kernel.util.I18NHelper; @@ -82,9 +83,10 @@ public ActionForward execute(ActionMapping mapping, // set client IP address user.setModifyingIP(RequestUtils.getRemoteAddress(request)); - // set password - user.setPassword(userRegisterForm.getPassword().trim()); - user.setSalt(userRegisterForm.getPassword().trim()); + // set password (AMP-SEC-017/054: never store the plaintext password) + String hashedPassword = new AmpPasswordEncoder().encode(userRegisterForm.getPassword().trim()); + user.setPassword(hashedPassword); + user.setSalt(hashedPassword); // set Website user.setUrl(userRegisterForm.getWebSite()); diff --git a/amp/src/main/java/org/digijava/module/um/action/UserRegisterBlank.java b/amp/src/main/java/org/digijava/module/um/action/UserRegisterBlank.java index e538105030e..9e13e5d3994 100644 --- a/amp/src/main/java/org/digijava/module/um/action/UserRegisterBlank.java +++ b/amp/src/main/java/org/digijava/module/um/action/UserRegisterBlank.java @@ -34,6 +34,7 @@ import org.digijava.kernel.mail.DgEmailManager; import org.digijava.kernel.request.SiteDomain; import org.digijava.kernel.security.HttpLoginManager; +import org.digijava.kernel.security.auth.AmpPasswordEncoder; import org.digijava.kernel.user.User; import org.digijava.kernel.util.DgUtil; import org.digijava.kernel.util.RequestUtils; @@ -77,9 +78,10 @@ public ActionForward execute(ActionMapping mapping, // set client IP address user.setModifyingIP(RequestUtils.getRemoteAddress(request)); - // set password - user.setPassword(userRegisterForm.getPassword().trim()); - user.setSalt(userRegisterForm.getPassword().trim()); + // set password (AMP-SEC-017/054: never store the plaintext password) + String hashedPassword = new AmpPasswordEncoder().encode(userRegisterForm.getPassword().trim()); + user.setPassword(hashedPassword); + user.setSalt(hashedPassword); // set Website user.setUrl(userRegisterForm.getWebSite()); diff --git a/amp/src/main/webapp/WEB-INF/applicationContext.xml b/amp/src/main/webapp/WEB-INF/applicationContext.xml index 954c0e610a7..9c329499352 100644 --- a/amp/src/main/webapp/WEB-INF/applicationContext.xml +++ b/amp/src/main/webapp/WEB-INF/applicationContext.xml @@ -283,8 +283,9 @@ - + + From 53983de10d733a47b4fbecf190d9440da4bd6552 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 10 Aug 2026 06:38:19 +0300 Subject: [PATCH 03/16] AMP-31197 : AMP-SEC-017 / 054: AMP passwords are stored and compared in plaintext via NoOpPasswordEncoder --- .../digijava/kernel/security/auth/AmpPasswordEncoder.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/amp/src/main/java/org/digijava/kernel/security/auth/AmpPasswordEncoder.java b/amp/src/main/java/org/digijava/kernel/security/auth/AmpPasswordEncoder.java index 634e6a90bdc..f14e0d5078f 100644 --- a/amp/src/main/java/org/digijava/kernel/security/auth/AmpPasswordEncoder.java +++ b/amp/src/main/java/org/digijava/kernel/security/auth/AmpPasswordEncoder.java @@ -16,11 +16,11 @@ public class AmpPasswordEncoder implements PasswordEncoder { private static final Pattern BCRYPT_PATTERN = Pattern.compile("^\\$2[aby]\\$\\d{2}\\$.{53}$"); - private final BCryptPasswordEncoder delegate = new BCryptPasswordEncoder(); + private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); @Override public String encode(CharSequence rawPassword) { - return delegate.encode(rawPassword); + return encoder.encode(rawPassword); } @Override @@ -29,7 +29,7 @@ public boolean matches(CharSequence rawPassword, String encodedPassword) { return false; } if (isHashed(encodedPassword)) { - return delegate.matches(rawPassword, encodedPassword); + return encoder.matches(rawPassword, encodedPassword); } // legacy account: password column still holds the plaintext value, compare directly return MessageDigest.isEqual( From ee3fff1998e528a98e9dcd64453b485b0a0a3f18 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 10 Aug 2026 07:02:16 +0300 Subject: [PATCH 04/16] AMP-31197 : AMP-SEC-006 / 077: POST /rest/indicator/indicator-layer/import is unauthenticated and accepts arbitrary Excel --- .../indicator/IndicatorEndPoints.java | 3 ++- .../endpoints/indicator/IndicatorErrors.java | 6 +++++ .../indicator/IndicatorImporter.java | 27 ++++++++++++------- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/IndicatorEndPoints.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/IndicatorEndPoints.java index f8823b16287..1f8cf57a624 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/IndicatorEndPoints.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/IndicatorEndPoints.java @@ -106,11 +106,12 @@ public Response download( @Path("/indicator-layer/import") @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8") + @ApiMethod(id = "importIndicator", ui = false) @ApiOperation( value = "Import indicator layers values in the file.", notes = "Parameters:\n" + "* admLevelId - to check if the file has same adm level than the indicator\n" - + "* file - file with values to be uploaded\n\n" + + "* file - file with values to be uploaded(MAX Size=100MB)\n\n" + "
\n" + "The file to be uploaded is generated on /indicator-layer/download\n") public IndicatorImporterResult importIndicator( diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/IndicatorErrors.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/IndicatorErrors.java index dd894730fbb..985bb323e7e 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/IndicatorErrors.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/IndicatorErrors.java @@ -41,4 +41,10 @@ public class IndicatorErrors { new ApiErrorMessage(ERROR_CLASS_INDICATOR_ID, 10, "Invalid Import: Administrative divisions in the uploaded file do not match " + "Admin level selected in wizard"); + + public static final ApiErrorMessage FILE_TOO_LARGE = new ApiErrorMessage(ERROR_CLASS_INDICATOR_ID, 11, + "Uploaded file exceeds the maximum allowed size"); + + public static final ApiErrorMessage INVALID_FILE_TYPE = new ApiErrorMessage(ERROR_CLASS_INDICATOR_ID, 12, + "Uploaded file is not a valid Excel (.xls) file"); } diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/IndicatorImporter.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/IndicatorImporter.java index f4e39497006..aabc29f9e95 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/IndicatorImporter.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/indicator/IndicatorImporter.java @@ -5,11 +5,12 @@ import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; -import org.apache.poi.hssf.usermodel.HSSFSheet; -import org.apache.poi.hssf.usermodel.HSSFWorkbook; -import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; import org.digijava.kernel.ampapi.endpoints.errors.ApiEMGroup; import org.digijava.kernel.ampapi.endpoints.errors.ApiError; import org.digijava.kernel.ampapi.endpoints.errors.GenericErrors; @@ -36,6 +37,8 @@ public class IndicatorImporter { private static final Logger LOGGER = Logger.getLogger(IndicatorImporter.class); + // AMP-SEC-006/077: cap upload size before it is fully buffered/parsed to avoid unbounded memory use + private static final long MAX_UPLOAD_SIZE_BYTES = 100L * 1024 * 1024; private ApiEMGroup errors = new ApiEMGroup(); public ApiEMGroup getApiErrors() { @@ -49,14 +52,10 @@ public ApiEMGroup getApiErrors() { * @throws org.digijava.module.aim.exception.AimException */ public List processExcelFile(InputStream inputStream, long admLevelId) { - POIFSFileSystem fsFileSystem = null; List locationIndicatorValueList = new ArrayList<>(); Set geoIdsWithProblems = new HashSet(); - try { - fsFileSystem = new POIFSFileSystem(inputStream); - HSSFWorkbook workBook = new HSSFWorkbook(fsFileSystem); - - HSSFSheet hssfSheet = workBook.getSheetAt(0); + try (Workbook workBook = WorkbookFactory.create(inputStream)) { + Sheet hssfSheet = workBook.getSheetAt(0); Row hssfRow = hssfSheet.getRow(0); Cell admLevelCell = hssfRow.getCell(0); String admLevel = ""; @@ -144,6 +143,8 @@ public List processExcelFile(InputStream inputStre } catch (NullPointerException e) { errors.addApiErrorMessage(GenericErrors.UNKNOWN_ERROR, " Cannot import indicator values "); + } catch (InvalidFormatException e) { + errors.addApiErrorMessage(IndicatorErrors.INVALID_FILE_TYPE, " File is not a valid Excel (.xls/.xlsx) file "); } catch (IllegalStateException e) { errors.addApiErrorMessage(IndicatorErrors.INCORRECT_CONTENT, " File is not ok "); } catch (IOException e) { @@ -180,11 +181,17 @@ public static IndicatorImporterResult importIndicator(InputStream uploadedInputS byte[] fileData; try { - fileData = org.apache.commons.io.IOUtils.toByteArray(uploadedInputStream); + fileData = org.apache.commons.io.IOUtils.toByteArray( + new org.apache.commons.io.input.BoundedInputStream(uploadedInputStream, MAX_UPLOAD_SIZE_BYTES + 1)); } catch (IOException e) { throw new WebApplicationException(e); } + if (fileData.length > MAX_UPLOAD_SIZE_BYTES) { + throw new AmpWebApplicationException(Response.Status.BAD_REQUEST, + ApiError.toError(IndicatorErrors.FILE_TOO_LARGE)); + } + InputStream inputStream = new ByteArrayInputStream(fileData); IndicatorImporter importer = new IndicatorImporter(); From 68f70809b63103423d21974c8e7fb765140125c3 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 10 Aug 2026 07:18:31 +0300 Subject: [PATCH 05/16] AMP-31197 : AMP-SEC-004 / 005: Resource read/upload endpoints are unauthenticated --- .../module/contentrepository/action/DownloadFile.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/amp/src/main/java/org/digijava/module/contentrepository/action/DownloadFile.java b/amp/src/main/java/org/digijava/module/contentrepository/action/DownloadFile.java index 227bcb7a4ee..87f482b5eb9 100644 --- a/amp/src/main/java/org/digijava/module/contentrepository/action/DownloadFile.java +++ b/amp/src/main/java/org/digijava/module/contentrepository/action/DownloadFile.java @@ -8,6 +8,7 @@ import org.digijava.kernel.ampapi.endpoints.resource.ResourceErrors; import org.digijava.kernel.util.ResponseUtil; import org.digijava.module.aim.helper.Constants; +import org.digijava.module.aim.util.TeamUtil; import org.digijava.module.contentrepository.helper.CrConstants; import org.digijava.module.contentrepository.helper.DocumentData; import org.digijava.module.contentrepository.helper.NodeWrapper; @@ -30,6 +31,12 @@ public ActionForward execute(ActionMapping mapping, ActionForm form, javax.servlet.http.HttpServletResponse response) throws java.lang.Exception { + // moduleConfig/contentrepository/module-spring.xml), so it must enforce its own session check here. + if (TeamUtil.getCurrentUser() == null) { + response.sendError(javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED); + return null; + } + String nodeUUID = request.getParameter("uuid"); if (nodeUUID != null) { From 26c9abec997227b6490ee9cb3ec4bde55941991c Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 10 Aug 2026 07:35:58 +0300 Subject: [PATCH 06/16] AMP-31197 : AMP-SEC-025 / 026 / 061 / 062: JAXB and Commons Digester XML parsing lack XXE protections. --- .../syndication/aggregator/Rss1Impl.java | 3 +++ .../syndication/aggregator/Rss2Impl.java | 3 +++ .../kernel/text/parser/LocaleParser.java | 3 +++ .../digijava/kernel/util/DigesterFactory.java | 2 ++ .../kernel/viewmanager/RepositoryParser.java | 3 +++ .../kernel/viewmanager/SiteConfigParser.java | 3 +++ .../module/aim/action/TranslatorManager.java | 5 +++-- .../module/aim/action/VisibilityManager.java | 3 ++- .../publicview/conf/ConfigurationUtil.java | 3 ++- .../module/autopatcher/core/PatcherUtil.java | 5 +++-- .../gateperm/action/ExchangePermission.java | 3 ++- .../module/help/action/HelpActions.java | 3 ++- .../action/ExportAndImportTemplates.java | 3 ++- .../action/ImportExportTranslations.java | 3 ++- .../translation/util/ImportExportUtil.java | 5 ++++- .../xmlpatcher/util/XmlPatcherUtil.java | 21 ++++++++++++++++++- 16 files changed, 59 insertions(+), 12 deletions(-) diff --git a/amp/src/main/java/org/digijava/kernel/syndication/aggregator/Rss1Impl.java b/amp/src/main/java/org/digijava/kernel/syndication/aggregator/Rss1Impl.java index 46deabee28c..ec68930dc28 100644 --- a/amp/src/main/java/org/digijava/kernel/syndication/aggregator/Rss1Impl.java +++ b/amp/src/main/java/org/digijava/kernel/syndication/aggregator/Rss1Impl.java @@ -27,6 +27,7 @@ import org.digijava.kernel.syndication.digester.Rss; import org.digijava.kernel.syndication.digester.RssChannel; import org.digijava.kernel.syndication.digester.RssItem; +import org.digijava.kernel.util.XmlSecurityUtils; import org.xml.sax.SAXException; import java.io.IOException; @@ -66,6 +67,8 @@ public Rss parseXml(InputStream stream) throws SAXException, IOException{ digester.clear(); digester.setValidating(false); digester.setUseContextClassLoader(true); + // AMP-SEC-025/061: remote RSS feeds are untrusted input, block XXE + XmlSecurityUtils.secureDigester(digester); digester.addObjectCreate("rdf:RDF", Rss.class); digester.addObjectCreate("rdf:RDF/channel", RssChannel.class); diff --git a/amp/src/main/java/org/digijava/kernel/syndication/aggregator/Rss2Impl.java b/amp/src/main/java/org/digijava/kernel/syndication/aggregator/Rss2Impl.java index 5175250f56c..efb253c31e8 100644 --- a/amp/src/main/java/org/digijava/kernel/syndication/aggregator/Rss2Impl.java +++ b/amp/src/main/java/org/digijava/kernel/syndication/aggregator/Rss2Impl.java @@ -27,6 +27,7 @@ import org.digijava.kernel.syndication.digester.Rss; import org.digijava.kernel.syndication.digester.RssChannel; import org.digijava.kernel.syndication.digester.RssItem; +import org.digijava.kernel.util.XmlSecurityUtils; import org.xml.sax.SAXException; import java.io.IOException; @@ -98,6 +99,8 @@ public Digester createDigester() { digester.clear(); digester.setValidating(false); digester.setUseContextClassLoader(true); + // AMP-SEC-025/061: remote RSS feeds are untrusted input, block XXE + XmlSecurityUtils.secureDigester(digester); digester.addObjectCreate("rss", Rss.class); digester.addObjectCreate("rss/channel", RssChannel.class); diff --git a/amp/src/main/java/org/digijava/kernel/text/parser/LocaleParser.java b/amp/src/main/java/org/digijava/kernel/text/parser/LocaleParser.java index 65397320e7b..d77a5a976ed 100644 --- a/amp/src/main/java/org/digijava/kernel/text/parser/LocaleParser.java +++ b/amp/src/main/java/org/digijava/kernel/text/parser/LocaleParser.java @@ -24,6 +24,7 @@ import org.apache.commons.digester.Digester; import org.apache.log4j.Logger; +import org.digijava.kernel.util.XmlSecurityUtils; import java.io.InputStream; import java.lang.ref.SoftReference; @@ -63,6 +64,8 @@ private static Digester createDigester() { digester.setValidating(true); // Workaround Tomcat's issue with ClassLoader digester.setUseContextClassLoader(true); + // AMP-SEC-025/061: block XXE; DOCTYPE stays allowed since validation needs the registered local DTD + XmlSecurityUtils.secureDigester(digester, true); // Configure digester digester.addObjectCreate("locale", LocaleData.class); diff --git a/amp/src/main/java/org/digijava/kernel/util/DigesterFactory.java b/amp/src/main/java/org/digijava/kernel/util/DigesterFactory.java index f6f523795dc..47a0c0a2dbd 100644 --- a/amp/src/main/java/org/digijava/kernel/util/DigesterFactory.java +++ b/amp/src/main/java/org/digijava/kernel/util/DigesterFactory.java @@ -127,6 +127,8 @@ public static Digester newDigester(boolean xmlValidation, if (rule != null) { digester.addRuleSet(rule); } + // AMP-SEC-025/061: block XXE (DOCTYPE/external entities are not needed for XSD-based validation) + XmlSecurityUtils.secureDigester(digester, false); return (digester); } diff --git a/amp/src/main/java/org/digijava/kernel/viewmanager/RepositoryParser.java b/amp/src/main/java/org/digijava/kernel/viewmanager/RepositoryParser.java index 5cd19e79fab..de9020e5e8e 100644 --- a/amp/src/main/java/org/digijava/kernel/viewmanager/RepositoryParser.java +++ b/amp/src/main/java/org/digijava/kernel/viewmanager/RepositoryParser.java @@ -25,6 +25,7 @@ import org.apache.commons.digester.Digester; import org.apache.log4j.Logger; import org.digijava.kernel.siteconfig.*; +import org.digijava.kernel.util.XmlSecurityUtils; import org.xml.sax.SAXException; import java.io.File; @@ -46,6 +47,8 @@ private static Digester createDigester() { digester.setValidating(false); // Workaround Tomcat's issue with ClassLoader digester.setUseContextClassLoader(true); + // AMP-SEC-025/061: block XXE + XmlSecurityUtils.secureDigester(digester); // Configure digester digester.addObjectCreate("config", RepositoryLayout.class); diff --git a/amp/src/main/java/org/digijava/kernel/viewmanager/SiteConfigParser.java b/amp/src/main/java/org/digijava/kernel/viewmanager/SiteConfigParser.java index bae53554a88..25607e8b145 100644 --- a/amp/src/main/java/org/digijava/kernel/viewmanager/SiteConfigParser.java +++ b/amp/src/main/java/org/digijava/kernel/viewmanager/SiteConfigParser.java @@ -25,6 +25,7 @@ import org.apache.commons.digester.Digester; import org.apache.log4j.Logger; import org.digijava.kernel.siteconfig.*; +import org.digijava.kernel.util.XmlSecurityUtils; import org.xml.sax.SAXException; import java.io.File; @@ -46,6 +47,8 @@ private static Digester createDigester() { digester.setValidating(false); // Workaround Tomcat's issue with ClassLoader digester.setUseContextClassLoader(true); + // AMP-SEC-025/061: block XXE + XmlSecurityUtils.secureDigester(digester); // Configure digester digester.addObjectCreate("site-config", SiteConfig.class); diff --git a/amp/src/main/java/org/digijava/module/aim/action/TranslatorManager.java b/amp/src/main/java/org/digijava/module/aim/action/TranslatorManager.java index 5e822b83687..67a86c1b20e 100644 --- a/amp/src/main/java/org/digijava/module/aim/action/TranslatorManager.java +++ b/amp/src/main/java/org/digijava/module/aim/action/TranslatorManager.java @@ -12,6 +12,7 @@ import org.digijava.kernel.translator.CachedTranslatorWorker; import org.digijava.kernel.translator.TranslatorWorker; import org.digijava.kernel.util.RequestUtils; +import org.digijava.kernel.util.XmlSecurityUtils; import org.digijava.module.aim.form.TranslatorManagerForm; import org.digijava.module.aim.helper.TrnHashMap; import org.digijava.module.translation.entity.MessageGroup; @@ -67,7 +68,7 @@ public ActionForward execute(ActionMapping mapping, ActionForm form,HttpServletR List languagesImport = new ArrayList(); try { - trns_in = (Translations) m.unmarshal(inputStream); + trns_in = (Translations) m.unmarshal(XmlSecurityUtils.secureSource(inputStream)); if (trns_in.getTrn() != null) { Iterator it = trns_in.getTrn().iterator(); while (it.hasNext()) { @@ -121,7 +122,7 @@ public ActionForward execute(ActionMapping mapping, ActionForm form,HttpServletR trnHashMaps.add(tHashMap); } try { - trns_in = (Translations) m.unmarshal(inputStream); + trns_in = (Translations) m.unmarshal(XmlSecurityUtils.secureSource(inputStream)); if (trns_in.getTrn() != null) { logger.info("Processing "+trns_in.getTrn().size()+" translation groups (trn tags)..."); // Iterator it = trns_in.getTrn().iterator(); diff --git a/amp/src/main/java/org/digijava/module/aim/action/VisibilityManager.java b/amp/src/main/java/org/digijava/module/aim/action/VisibilityManager.java index 3ca82e69c5c..b545da84d75 100644 --- a/amp/src/main/java/org/digijava/module/aim/action/VisibilityManager.java +++ b/amp/src/main/java/org/digijava/module/aim/action/VisibilityManager.java @@ -18,6 +18,7 @@ import org.digijava.module.aim.helper.VisibilityManagerExportHelper; import org.digijava.module.aim.util.DbUtil; import org.digijava.module.aim.util.FeaturesUtil; +import org.digijava.kernel.util.XmlSecurityUtils; import org.hibernate.HibernateException; import org.hibernate.Session; @@ -95,7 +96,7 @@ public ActionForward modeImportTreeVisibility(ActionMapping mapping,ActionForm f JAXBContext jc = JAXBContext.newInstance("org.dgfoundation.amp.visibility.feed.fm.schema"); Unmarshaller um = jc.createUnmarshaller(); try { - VisibilityTemplates vtemplate = (VisibilityTemplates) um.unmarshal(vForm.getUploadFile().getInputStream()); + VisibilityTemplates vtemplate = (VisibilityTemplates) um.unmarshal(XmlSecurityUtils.secureSource(vForm.getUploadFile().getInputStream())); VisibilityManagerExportHelper vhelper = new VisibilityManagerExportHelper(); vhelper.importXmlVisbilityTemplate(vtemplate); } catch (JAXBException je) { diff --git a/amp/src/main/java/org/digijava/module/aim/services/publicview/conf/ConfigurationUtil.java b/amp/src/main/java/org/digijava/module/aim/services/publicview/conf/ConfigurationUtil.java index 46c73ac25ff..8822d633e9a 100644 --- a/amp/src/main/java/org/digijava/module/aim/services/publicview/conf/ConfigurationUtil.java +++ b/amp/src/main/java/org/digijava/module/aim/services/publicview/conf/ConfigurationUtil.java @@ -1,6 +1,7 @@ package org.digijava.module.aim.services.publicview.conf; +import org.digijava.kernel.util.XmlSecurityUtils; import org.xml.sax.InputSource; import javax.servlet.ServletContext; @@ -28,7 +29,7 @@ public static Configuration getConfiguration (ServletContext ctx) throws JAXBExc public static Configuration initConfig (InputSource inputSource) throws JAXBException, FileNotFoundException { JAXBContext jc = JAXBContext.newInstance(Configuration.class); Unmarshaller um = jc.createUnmarshaller(); - Configuration retVal = (Configuration)um.unmarshal(inputSource); + Configuration retVal = (Configuration)um.unmarshal(XmlSecurityUtils.secureSource(inputSource)); return retVal; } diff --git a/amp/src/main/java/org/digijava/module/autopatcher/core/PatcherUtil.java b/amp/src/main/java/org/digijava/module/autopatcher/core/PatcherUtil.java index 04a7e0d1386..fb576f4cde3 100644 --- a/amp/src/main/java/org/digijava/module/autopatcher/core/PatcherUtil.java +++ b/amp/src/main/java/org/digijava/module/autopatcher/core/PatcherUtil.java @@ -3,6 +3,7 @@ import org.apache.log4j.Logger; import org.digijava.module.autopatcher.exceptions.InvalidPatchRepositoryException; import org.digijava.module.autopatcher.schema.Patch; +import org.digijava.kernel.util.XmlSecurityUtils; import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.query.Query; @@ -84,13 +85,13 @@ public static Collection getAllPatchesFiles(String abstractPatchesLocation } public static Patch getUnmarshalledPatch(File patchFile) - throws JAXBException { + throws JAXBException, IOException { JAXBContext jc = JAXBContext .newInstance("org.digijava.module.autopatcher.schema"); Unmarshaller m = jc.createUnmarshaller(); m.setValidating(true); - Patch p = (Patch) m.unmarshal(patchFile); + Patch p = (Patch) m.unmarshal(XmlSecurityUtils.secureSource(patchFile)); return p; } diff --git a/amp/src/main/java/org/digijava/module/gateperm/action/ExchangePermission.java b/amp/src/main/java/org/digijava/module/gateperm/action/ExchangePermission.java index 437d4141170..c05a6fe7da6 100644 --- a/amp/src/main/java/org/digijava/module/gateperm/action/ExchangePermission.java +++ b/amp/src/main/java/org/digijava/module/gateperm/action/ExchangePermission.java @@ -11,6 +11,7 @@ import org.dgfoundation.amp.utils.MultiAction; import org.digijava.kernel.exception.DgException; import org.digijava.kernel.persistence.PersistenceManager; +import org.digijava.kernel.util.XmlSecurityUtils; import org.digijava.module.aim.util.Identifiable; import org.digijava.module.gateperm.core.*; import org.digijava.module.gateperm.feed.schema.*; @@ -93,7 +94,7 @@ private ActionForward modeImportPerform(ActionMapping mapping, JAXBContext jc = JAXBContext.newInstance("org.digijava.module.gateperm.feed.schema"); Unmarshaller m = jc.createUnmarshaller(); - Permissions xmlPermissions = (Permissions) m.unmarshal(inputStream); + Permissions xmlPermissions = (Permissions) m.unmarshal(XmlSecurityUtils.secureSource(inputStream)); List gatePerm = xmlPermissions.getGatePerm(); Iterator i=gatePerm.iterator(); Session session=PersistenceManager.getRequestDBSession(); diff --git a/amp/src/main/java/org/digijava/module/help/action/HelpActions.java b/amp/src/main/java/org/digijava/module/help/action/HelpActions.java index eb05bd6d68f..0955927391f 100644 --- a/amp/src/main/java/org/digijava/module/help/action/HelpActions.java +++ b/amp/src/main/java/org/digijava/module/help/action/HelpActions.java @@ -38,6 +38,7 @@ import javax.xml.bind.JAXBContext; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; +import org.digijava.kernel.util.XmlSecurityUtils; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.*; @@ -952,7 +953,7 @@ public ActionForward importing(ActionMapping mapping,ActionForm form, HttpServle if(xmlContent == null) return mapping.findForward("admin"); JAXBContext jc = JAXBContext.newInstance("org.digijava.module.help.jaxbi"); Unmarshaller m = jc.createUnmarshaller(); - help_in = (AmpHelpRoot) m.unmarshal(new ByteArrayInputStream(xmlContent)); + help_in = (AmpHelpRoot) m.unmarshal(XmlSecurityUtils.secureSource(new ByteArrayInputStream(xmlContent))); //remove all existing help topics List firstLevelTopics=HelpUtil.getFirstLevelTopics(site); diff --git a/amp/src/main/java/org/digijava/module/message/action/ExportAndImportTemplates.java b/amp/src/main/java/org/digijava/module/message/action/ExportAndImportTemplates.java index f72a086ed80..4632c0a6bc3 100644 --- a/amp/src/main/java/org/digijava/module/message/action/ExportAndImportTemplates.java +++ b/amp/src/main/java/org/digijava/module/message/action/ExportAndImportTemplates.java @@ -16,6 +16,7 @@ import javax.servlet.http.HttpServletResponse; import javax.xml.bind.JAXBContext; import javax.xml.bind.Unmarshaller; +import org.digijava.kernel.util.XmlSecurityUtils; import java.io.*; import java.util.List; @@ -79,7 +80,7 @@ public ActionForward importTemplates (ActionMapping mapping,ActionForm form, Htt Unmarshaller m = jc.createUnmarshaller(); Messaging item; try { - item = (Messaging) m.unmarshal(inputStream); + item = (Messaging) m.unmarshal(XmlSecurityUtils.secureSource(inputStream)); TemplatesList tempList=item.getTemplatesList(); if(tempList!=null){ List templates=tempList.getTemplate(); diff --git a/amp/src/main/java/org/digijava/module/translation/action/ImportExportTranslations.java b/amp/src/main/java/org/digijava/module/translation/action/ImportExportTranslations.java index a535130d77c..936e2674441 100644 --- a/amp/src/main/java/org/digijava/module/translation/action/ImportExportTranslations.java +++ b/amp/src/main/java/org/digijava/module/translation/action/ImportExportTranslations.java @@ -26,6 +26,7 @@ import org.digijava.module.translation.jaxb.Translations; import org.digijava.module.translation.lucene.TrnLuceneModule; import org.digijava.module.translation.util.ImportExportUtil; +import org.digijava.kernel.util.XmlSecurityUtils; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; @@ -314,7 +315,7 @@ private boolean doImport(HttpServletRequest request, ImportExportForm ioForm, Ht request.getSession().setAttribute(SESSION_FILE, uploadedFile); try { Unmarshaller unmarshaller = ImportExportUtil.getUnmarshaler(); - Translations root = (Translations) unmarshaller.unmarshal(inputStream); + Translations root = (Translations) unmarshaller.unmarshal(XmlSecurityUtils.secureSource(inputStream)); request.getSession().setAttribute(SESSION_ROOT, root); Set languagesInFile = ImportExportUtil.extractUsedLangages(root); ioForm.setImportedLanguages(new ArrayList(languagesInFile)); diff --git a/amp/src/main/java/org/digijava/module/translation/util/ImportExportUtil.java b/amp/src/main/java/org/digijava/module/translation/util/ImportExportUtil.java index 25369865073..25a0c55e09d 100644 --- a/amp/src/main/java/org/digijava/module/translation/util/ImportExportUtil.java +++ b/amp/src/main/java/org/digijava/module/translation/util/ImportExportUtil.java @@ -28,6 +28,7 @@ import org.digijava.module.translation.importexport.TranslationSearcher; import org.digijava.module.translation.jaxb.Language; import org.digijava.module.translation.jaxb.Translations; +import org.digijava.kernel.util.XmlSecurityUtils; import org.digijava.module.translation.jaxb.Trn; import org.digijava.module.translation.util.importexport.ImportResult; import org.digijava.module.translation.util.importexport.ImportRowConsumerCallable; @@ -322,9 +323,11 @@ private static Translations getRootNode(File file){ Translations root = null; try { Unmarshaller unmarshaller = getUnmarshaler(); - root = (Translations) unmarshaller.unmarshal(file); + root = (Translations) unmarshaller.unmarshal(XmlSecurityUtils.secureSource(file)); } catch (JAXBException e) { logger.error(e.getMessage(), e); + } catch (java.io.FileNotFoundException e) { + logger.error(e.getMessage(), e); } return root; } diff --git a/amp/src/main/java/org/digijava/module/xmlpatcher/util/XmlPatcherUtil.java b/amp/src/main/java/org/digijava/module/xmlpatcher/util/XmlPatcherUtil.java index 80f250641a5..887d6f4d3dd 100644 --- a/amp/src/main/java/org/digijava/module/xmlpatcher/util/XmlPatcherUtil.java +++ b/amp/src/main/java/org/digijava/module/xmlpatcher/util/XmlPatcherUtil.java @@ -212,10 +212,22 @@ public static Set getAllDiscoveredPatchNames() throws DgException, return ret; } - static javax.xml.transform.TransformerFactory transFact = javax.xml.transform.TransformerFactory.newInstance( ); + static javax.xml.transform.TransformerFactory transFact = createSecureTransformerFactory(); static javax.xml.transform.Transformer cached_transformer; static String lastPathTransformerPath = null; + // AMP-SEC-026/062: block XXE via XSLT (external DTD/stylesheet access) + private static javax.xml.transform.TransformerFactory createSecureTransformerFactory() { + javax.xml.transform.TransformerFactory factory = javax.xml.transform.TransformerFactory.newInstance(); + try { + factory.setAttribute(javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(javax.xml.XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + } catch (IllegalArgumentException e) { + logger.warn("TransformerFactory implementation does not support restricting external access", e); + } + return factory; + } + static Unmarshaller cached_unmarshaller; static String lastUnmarshallerPath = null; @@ -244,6 +256,13 @@ static Unmarshaller getUnmarshaller(String schemaURI) throws JAXBException, SAXE // initialize JAXB 2.0 validation SchemaFactory sf = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema"); + // AMP-SEC-026/062: block XXE via the XSD (external DTD/schema access) + try { + sf.setProperty(javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD, ""); + sf.setProperty(javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + } catch (SAXException e) { + logger.warn("SchemaFactory implementation does not support restricting external access", e); + } Schema schema = sf.newSchema(new File(schemaURI)); cached_unmarshaller.setSchema(schema); cached_unmarshaller.setEventHandler(new DefaultValidationEventHandler()); From 2bf07780db7ef8c39297f26ed58fa17c4029f4de Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 10 Aug 2026 08:06:07 +0300 Subject: [PATCH 07/16] AMP-31197 : Add cameroon to workflow countries --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index beaec221b03..fbe8a544747 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,6 +17,7 @@ on: options: - bfaso - boad + - cameroontrubudget - chad - civ - drc From bddd173b5216b978ec0f6fff2554e8621ad09a4d Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 10 Aug 2026 08:28:35 +0300 Subject: [PATCH 08/16] AMP-31197: add missing XmlSecurityUtils.java (fixes CI compile failure from previous commit) --- .../kernel/util/XmlSecurityUtils.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 amp/src/main/java/org/digijava/kernel/util/XmlSecurityUtils.java diff --git a/amp/src/main/java/org/digijava/kernel/util/XmlSecurityUtils.java b/amp/src/main/java/org/digijava/kernel/util/XmlSecurityUtils.java new file mode 100644 index 00000000000..efa456b8c21 --- /dev/null +++ b/amp/src/main/java/org/digijava/kernel/util/XmlSecurityUtils.java @@ -0,0 +1,99 @@ +package org.digijava.kernel.util; + +import org.apache.commons.digester.Digester; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; + +import javax.xml.bind.Unmarshaller; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.Source; +import javax.xml.transform.sax.SAXSource; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.Reader; + +/** + * Helpers to lock down JAXB and Commons Digester XML parsing against XML External Entity (XXE) attacks + * (AMP-SEC-025/026/061/062): disallow DOCTYPE declarations and external entity/DTD resolution. + */ +public final class XmlSecurityUtils { + + private static final String FEATURE_DISALLOW_DOCTYPE = "http://apache.org/xml/features/disallow-doctype-decl"; + private static final String FEATURE_EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities"; + private static final String FEATURE_EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities"; + private static final String FEATURE_LOAD_EXTERNAL_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; + + private XmlSecurityUtils() { + } + + /** A {@link SAXParserFactory} hardened against XXE, with DOCTYPE declarations disallowed entirely. */ + public static SAXParserFactory secureSaxParserFactory() { + SAXParserFactory factory = SAXParserFactory.newInstance(); + try { + factory.setFeature(FEATURE_DISALLOW_DOCTYPE, true); + factory.setFeature(FEATURE_EXTERNAL_GENERAL_ENTITIES, false); + factory.setFeature(FEATURE_EXTERNAL_PARAMETER_ENTITIES, false); + factory.setXIncludeAware(false); + } catch (ParserConfigurationException | SAXException e) { + throw new IllegalStateException("Unable to configure a secure SAXParserFactory", e); + } + return factory; + } + + private static XMLReader secureXmlReader() { + try { + return secureSaxParserFactory().newSAXParser().getXMLReader(); + } catch (ParserConfigurationException | SAXException e) { + throw new IllegalStateException("Unable to create a secure XMLReader", e); + } + } + + /** Wraps a stream so it can be safely passed to {@link Unmarshaller#unmarshal(Source)}. */ + public static Source secureSource(InputStream inputStream) { + return new SAXSource(secureXmlReader(), new InputSource(inputStream)); + } + + /** Wraps a reader so it can be safely passed to {@link Unmarshaller#unmarshal(Source)}. */ + public static Source secureSource(Reader reader) { + return new SAXSource(secureXmlReader(), new InputSource(reader)); + } + + /** Wraps a file so it can be safely passed to {@link Unmarshaller#unmarshal(Source)}. */ + public static Source secureSource(File file) throws FileNotFoundException { + InputSource inputSource = new InputSource(new FileInputStream(file)); + inputSource.setSystemId(file.toURI().toString()); + return new SAXSource(secureXmlReader(), inputSource); + } + + /** Wraps an existing {@link InputSource} so it can be safely passed to {@link Unmarshaller#unmarshal(Source)}. */ + public static Source secureSource(InputSource inputSource) { + return new SAXSource(secureXmlReader(), inputSource); + } + + /** + * Hardens a Commons Digester instance against XXE. DOCTYPE declarations are disallowed entirely + * unless {@code allowDoctypeDecl} is true, which is only needed when the digester validates + * against a locally registered DTD. + */ + public static void secureDigester(Digester digester, boolean allowDoctypeDecl) { + try { + if (!allowDoctypeDecl) { + digester.setFeature(FEATURE_DISALLOW_DOCTYPE, true); + } + digester.setFeature(FEATURE_EXTERNAL_GENERAL_ENTITIES, false); + digester.setFeature(FEATURE_EXTERNAL_PARAMETER_ENTITIES, false); + digester.setFeature(FEATURE_LOAD_EXTERNAL_DTD, false); + } catch (ParserConfigurationException | SAXException e) { + throw new IllegalStateException("Unable to configure a secure Digester", e); + } + } + + /** Hardens a Commons Digester instance against XXE, disallowing DOCTYPE declarations entirely. */ + public static void secureDigester(Digester digester) { + secureDigester(digester, false); + } +} From 249beae4fad5faf1bcfed510fff99e77429f5f16 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 10 Aug 2026 09:03:47 +0300 Subject: [PATCH 09/16] AMP-31197 : Add mvn retry to try navigate 429 mvn error --- amp/Dockerfile | 9 ++++++--- amp/docker/mvn-with-retry.sh | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 amp/docker/mvn-with-retry.sh diff --git a/amp/Dockerfile b/amp/Dockerfile index 903d1a37a87..c20092e23b4 100644 --- a/amp/Dockerfile +++ b/amp/Dockerfile @@ -195,6 +195,9 @@ RUN npm run build \ && rm -rf node_modules FROM maven:3.8.4-jdk-8 as compile-mvn WORKDIR /tmp/amp +# Copy Maven retry helper +COPY docker/mvn-with-retry.sh /usr/local/bin/mvn-with-retry.sh +RUN chmod +x /usr/local/bin/mvn-with-retry.sh # Copy pom.xml first for better Maven caching COPY pom.xml . #COPY amp/pom.xml amp/ @@ -210,13 +213,13 @@ COPY --from=compile-reampv2 /tmp/amp/TEMPLATE/reampv2 TEMPLATE/reampv2 # Download Maven dependencies (this layer will be cached if pom.xml doesn't change) ARG BUILD_SOURCE RUN --mount=type=cache,target=/root/.m2 \ - mvn -B dependency:go-offline -f pom.xml || true + mvn-with-retry.sh -B -Dmaven.wagon.http.retryHandler.count=5 dependency:go-offline -f pom.xml || true # Copy source code after dependencies are downloaded COPY . . ARG SKIP_TESTS=false RUN --mount=type=cache,target=/root/.m2 \ if [ "$SKIP_TESTS" = "true" ]; then \ - mvn -B clean compile war:exploded \ + mvn-with-retry.sh -B -Dmaven.wagon.http.retryHandler.count=5 clean compile war:exploded \ -DbuildSource=$BUILD_SOURCE \ -Djdbc.user=amp -Djdbc.password=amp122006 -Djdbc.db=amp -Djdbc.host=db \ -Djdbc.port=5432 -DdbName=postgresql -Djdbc.driverClassName=org.postgresql.Driver \ @@ -224,7 +227,7 @@ RUN --mount=type=cache,target=/root/.m2 \ && mv target/amp exploded \ && rm -rf target; \ else \ - mvn -B test war:exploded \ + mvn-with-retry.sh -B -Dmaven.wagon.http.retryHandler.count=5 test war:exploded \ -DbuildSource=$BUILD_SOURCE \ -Djdbc.user=amp -Djdbc.password=amp122006 -Djdbc.db=amp -Djdbc.host=db \ -Djdbc.port=5432 -DdbName=postgresql -Djdbc.driverClassName=org.postgresql.Driver \ diff --git a/amp/docker/mvn-with-retry.sh b/amp/docker/mvn-with-retry.sh new file mode 100644 index 00000000000..b5a84e1018c --- /dev/null +++ b/amp/docker/mvn-with-retry.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# Run Maven with retries to handle transient repository/network failures. + +set -eu + +if [ "$#" -eq 0 ]; then + echo "Usage: mvn-with-retry.sh " >&2 + exit 1 +fi + +attempt=1 +max_attempts="${MAVEN_MAX_ATTEMPTS:-4}" +base_delay_seconds="${MAVEN_RETRY_DELAY_SECONDS:-15}" + +while [ "$attempt" -le "$max_attempts" ]; do + echo "Running Maven attempt ${attempt}/${max_attempts}: mvn $*" + if mvn "$@"; then + exit 0 + fi + + if [ "$attempt" -eq "$max_attempts" ]; then + break + fi + + delay_seconds=$((base_delay_seconds * attempt)) + echo "Maven command failed, retrying in ${delay_seconds}s..." + sleep "$delay_seconds" + attempt=$((attempt + 1)) +done + +echo "ERROR: Maven command failed after ${max_attempts} attempts" >&2 +exit 1 \ No newline at end of file From e0e0ab0f4741397f14f1e75fb5a9588cdeb5b346 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 10 Aug 2026 15:57:16 +0300 Subject: [PATCH 10/16] AMP-31197 : Remove startup failing session policy --- amp/src/main/webapp/WEB-INF/applicationContext.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/amp/src/main/webapp/WEB-INF/applicationContext.xml b/amp/src/main/webapp/WEB-INF/applicationContext.xml index 9c329499352..2a75cb73538 100644 --- a/amp/src/main/webapp/WEB-INF/applicationContext.xml +++ b/amp/src/main/webapp/WEB-INF/applicationContext.xml @@ -110,13 +110,12 @@ --> - + - From 67c881fbaaa0871dfaa93fc67942e502401cab79 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 10 Aug 2026 18:55:20 +0300 Subject: [PATCH 11/16] AMP-31197: fix startup crash - allow DOCTYPE/general entities for trusted digi.xml config digester digi.xml intentionally uses a DOCTYPE-declared SYSTEM general entity (&commonEntities;) to include digi-common.xml. Disallowing DOCTYPE declarations in DigesterFactory.newDigester broke Tomcat startup (DigiConfigManager/DigiXmlECSLoaderListener). This factory only ever parses trusted, locally-deployed config files, so DOCTYPE and general entities are now allowed there; external parameter entities and external DTD loading stay disabled. --- .../java/org/digijava/kernel/util/DigesterFactory.java | 5 +++-- .../java/org/digijava/kernel/util/XmlSecurityUtils.java | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/amp/src/main/java/org/digijava/kernel/util/DigesterFactory.java b/amp/src/main/java/org/digijava/kernel/util/DigesterFactory.java index 47a0c0a2dbd..bcf606e2b26 100644 --- a/amp/src/main/java/org/digijava/kernel/util/DigesterFactory.java +++ b/amp/src/main/java/org/digijava/kernel/util/DigesterFactory.java @@ -127,8 +127,9 @@ public static Digester newDigester(boolean xmlValidation, if (rule != null) { digester.addRuleSet(rule); } - // AMP-SEC-025/061: block XXE (DOCTYPE/external entities are not needed for XSD-based validation) - XmlSecurityUtils.secureDigester(digester, false); + // AMP-SEC-025/061: block XXE, but this factory is only used for trusted, locally-deployed + // config files (digi.xml relies on a DOCTYPE-declared general entity to include digi-common.xml) + XmlSecurityUtils.secureDigester(digester, true); return (digester); } diff --git a/amp/src/main/java/org/digijava/kernel/util/XmlSecurityUtils.java b/amp/src/main/java/org/digijava/kernel/util/XmlSecurityUtils.java index efa456b8c21..eaaaf9e1a1d 100644 --- a/amp/src/main/java/org/digijava/kernel/util/XmlSecurityUtils.java +++ b/amp/src/main/java/org/digijava/kernel/util/XmlSecurityUtils.java @@ -75,16 +75,17 @@ public static Source secureSource(InputSource inputSource) { } /** - * Hardens a Commons Digester instance against XXE. DOCTYPE declarations are disallowed entirely - * unless {@code allowDoctypeDecl} is true, which is only needed when the digester validates - * against a locally registered DTD. + * Hardens a Commons Digester instance against XXE. DOCTYPE declarations (and the general + * entities they may declare, e.g. this codebase's own digi.xml file-inclusion trick) are + * disallowed entirely unless {@code allowDoctypeDecl} is true, which is only needed for + * digesters that parse trusted, locally-controlled config files relying on DOCTYPE features. */ public static void secureDigester(Digester digester, boolean allowDoctypeDecl) { try { if (!allowDoctypeDecl) { digester.setFeature(FEATURE_DISALLOW_DOCTYPE, true); + digester.setFeature(FEATURE_EXTERNAL_GENERAL_ENTITIES, false); } - digester.setFeature(FEATURE_EXTERNAL_GENERAL_ENTITIES, false); digester.setFeature(FEATURE_EXTERNAL_PARAMETER_ENTITIES, false); digester.setFeature(FEATURE_LOAD_EXTERNAL_DTD, false); } catch (ParserConfigurationException | SAXException e) { From cb698af48f5ebd5affcda19313c541c7ce46e117 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Mon, 10 Aug 2026 20:21:12 +0300 Subject: [PATCH 12/16] AMP-31197: fix XXE hardening regression in SiteConfigParser ampTemplate/site-config.xml declares internal-only general entities (&Version;, &releaseDate;, etc.) via a DOCTYPE internal subset. Disallowing DOCTYPE broke ShowLayout for any page rendered through this template. Allow DOCTYPE here (still fully local/internal, no SYSTEM/external entity involved). --- .../org/digijava/kernel/viewmanager/SiteConfigParser.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/amp/src/main/java/org/digijava/kernel/viewmanager/SiteConfigParser.java b/amp/src/main/java/org/digijava/kernel/viewmanager/SiteConfigParser.java index 25607e8b145..591f01f9324 100644 --- a/amp/src/main/java/org/digijava/kernel/viewmanager/SiteConfigParser.java +++ b/amp/src/main/java/org/digijava/kernel/viewmanager/SiteConfigParser.java @@ -47,8 +47,9 @@ private static Digester createDigester() { digester.setValidating(false); // Workaround Tomcat's issue with ClassLoader digester.setUseContextClassLoader(true); - // AMP-SEC-025/061: block XXE - XmlSecurityUtils.secureDigester(digester); + // AMP-SEC-025/061: block XXE, but allow DOCTYPE - ampTemplate/site-config.xml declares + // internal-only general entities (&Version; etc.) via a DOCTYPE internal subset + XmlSecurityUtils.secureDigester(digester, true); // Configure digester digester.addObjectCreate("site-config", SiteConfig.class); From 5a5fdbb9da457a6b331b24029c18dff03922ec1b Mon Sep 17 00:00:00 2001 From: brianbrix Date: Wed, 12 Aug 2026 21:58:16 +0300 Subject: [PATCH 13/16] AMP-31197 : Change Password Encoder for API created users --- amp/src/main/java/org/digijava/module/um/util/DbUtil.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/amp/src/main/java/org/digijava/module/um/util/DbUtil.java b/amp/src/main/java/org/digijava/module/um/util/DbUtil.java index c0b355e72b6..0fdabf8463b 100644 --- a/amp/src/main/java/org/digijava/module/um/util/DbUtil.java +++ b/amp/src/main/java/org/digijava/module/um/util/DbUtil.java @@ -411,12 +411,8 @@ public static void registerUser(User user) throws UMException { session = PersistenceManager.getSession(); //beginTransaction(); - // set encrypted password - user.setPassword(ShaCrypt.crypt(user.getPassword().trim()).trim()); - - // set hashed password - user.setSalt(new Long(user.getPassword().trim().hashCode()). - toString()); + // AMP-SEC-017/054: caller already stores the password hashed via AmpPasswordEncoder; + // do not re-hash it here with the legacy (unsalted SHA1) ShaCrypt scheme // update user session.save(user); From 8a3390bfb8bcbe0df8c0be83e8a974725a95edb1 Mon Sep 17 00:00:00 2001 From: brianbrix Date: Wed, 12 Aug 2026 23:21:42 +0300 Subject: [PATCH 14/16] AMP-31197 : Unify password encoder for all endpoints requiring auth --- .../js_2/jdigestauth/digest-auth.js | 153 +++++++++--------- .../endpoints/security/SecurityService.java | 51 ++++-- .../org/digijava/kernel/util/UserUtils.java | 5 +- .../module/aim/auth/AmpPostLoginAction.java | 61 +++---- .../org/digijava/module/um/util/DbUtil.java | 24 ++- 5 files changed, 161 insertions(+), 133 deletions(-) diff --git a/amp/TEMPLATE/ampTemplate/js_2/jdigestauth/digest-auth.js b/amp/TEMPLATE/ampTemplate/js_2/jdigestauth/digest-auth.js index c571e76ef27..c243e67989f 100644 --- a/amp/TEMPLATE/ampTemplate/js_2/jdigestauth/digest-auth.js +++ b/amp/TEMPLATE/ampTemplate/js_2/jdigestauth/digest-auth.js @@ -1,90 +1,89 @@ /* - * A JavaScript implementation of the Digest Authentication - * Digest Authentication, as defined in RFC 2617. - * Version 1.0 Copyright (C) Maricn Michalski (http://marcin-michalski.pl) - * Distributed under the BSD License - * - * site: http://arrowgroup.eu + * Login widget submission handler. + * Sends the SHA-1 hash of the password (never the plaintext) to /aim/postLogin.do, + * which verifies it the same way as the /rest/security/user API login. */ function ajaxLogin() { $('#loader').show(); - var digestAuth = new pl.arrowgroup.DigestAuthentication( - { - onSuccess : function(data) { - var serverResponse=JSON.parse(data); - var error = jQuery.trim(serverResponse.original_result); + var reportError = function(id) { + $('#loader').hide(); + $('#result').show(); + $(".error_text_login > span").hide(); + $("#" + id).show(); + }; - $('#result').hide(); - - //Suspended login - var suspendedLoginText = null; - var suspendReasons = []; - if (error != null && error.length > 13) { - if (error.substring (0, 13) == "userSuspended") { - //split reasons - var startIndex = 0; - var endIndex = 0; - - while (endIndex != error.length - 1) { - startIndex = error.indexOf("{", endIndex); - endIndex = error.indexOf("}", startIndex); - var reasonTxt = error.substring (startIndex + 1, endIndex); - suspendReasons.push(reasonTxt); - } - } - error = "userSuspended"; - } - - //endOf Suspended login - - switch (error) { - case 'noTeamMember': - reportError("unassigned_user"); - break; - case 'userBanned': - reportError("banned_user"); - break; - case 'invalidUser': - // isn't the generic onFailure actually called for this use case? - reportError("invalid_user"); - break; - case 'userSuspended': - var suspUserErrTxt = ""; - var reasonIdx = 0; - for (reasonIdx = 0; reasonIdx < suspendReasons.length; reasonIdx ++){ - suspUserErrTxt += suspendReasons[reasonIdx]; - if (reasonIdx < suspendReasons.length) { - suspUserErrTxt += "
" - } - } - $('#suspend').html(suspUserErrTxt); - reportError("suspend"); - break; - case 'noError': - location.href = '/index.do'; - break; - } - }, - onFailure : function(response){ - reportError("invalid_user_pwd"); - }, - cnonce : 'testCnonce' - } - ); - - var reportError = function(id) { - $('#loader').hide(); - $('#result').show(); - $(".error_text_login > span").hide(); - $("#" + id).show(); - }; + var handleResponse = function(data) { + var serverResponse = JSON.parse(data); + var error = jQuery.trim(serverResponse.original_result); - digestAuth.setCredentials($('#j_username').val().trim(),$('#j_password').val()); - digestAuth.call('/aim/postLogin.do'); + $('#result').hide(); + + //Suspended login + var suspendReasons = []; + if (error != null && error.length > 13) { + if (error.substring (0, 13) == "userSuspended") { + //split reasons + var startIndex = 0; + var endIndex = 0; + + while (endIndex != error.length - 1) { + startIndex = error.indexOf("{", endIndex); + endIndex = error.indexOf("}", startIndex); + var reasonTxt = error.substring (startIndex + 1, endIndex); + suspendReasons.push(reasonTxt); + } + } + error = "userSuspended"; + } + + //endOf Suspended login + + switch (error) { + case 'noTeamMember': + reportError("unassigned_user"); + break; + case 'userBanned': + reportError("banned_user"); + break; + case 'invalidUser': + reportError("invalid_user_pwd"); + break; + case 'userSuspended': + var suspUserErrTxt = ""; + var reasonIdx = 0; + for (reasonIdx = 0; reasonIdx < suspendReasons.length; reasonIdx ++){ + suspUserErrTxt += suspendReasons[reasonIdx]; + if (reasonIdx < suspendReasons.length) { + suspUserErrTxt += "
" + } + } + $('#suspend').html(suspUserErrTxt); + reportError("suspend"); + break; + case 'noError': + location.href = '/index.do'; + break; + } + }; + + $.ajax({ + url: '/aim/postLogin.do', + type: 'POST', + cache: false, + data: { + j_username: $('#j_username').val().trim(), + j_password: CryptoJS.SHA1($('#j_password').val()).toString() + }, + success: handleResponse, + error: function(response) { + reportError("invalid_user_pwd"); + } + }); } + $.Class("pl.arrowgroup.DigestAuthentication", { MAX_ATTEMPTS : 1, AUTHORIZATION_HEADER : "Authorization", diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/SecurityService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/SecurityService.java index 5f2af74289a..fbd3d6fcf3c 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/SecurityService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/SecurityService.java @@ -15,6 +15,7 @@ import org.digijava.kernel.request.SiteDomain; import org.digijava.kernel.request.TLSUtils; import org.digijava.kernel.security.auth.AmpPasswordEncoder; +import org.digijava.kernel.security.auth.DigiUserDetailsService; import org.digijava.kernel.services.AmpVersionInfo; import org.digijava.kernel.services.AmpVersionService; import org.digijava.kernel.translator.TranslatorWorker; @@ -35,7 +36,9 @@ import org.digijava.module.gateperm.util.PermissionUtil; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; @@ -204,17 +207,10 @@ public UserSessionInformation authenticate(AuthenticationRequest authRequest) { ApiErrorResponseService.reportError(BAD_REQUEST, SecurityErrors.INVALID_USER_PASSWORD); } - User user = UserUtils.getUserByEmailAddress(username); - String storedPassword = (user != null) ? user.getPassword() : null; - AmpPasswordEncoder passwordEncoder = new AmpPasswordEncoder(); - boolean passwordMatches = storedPassword != null && passwordEncoder.matches(password, storedPassword); - if (user == null || !passwordMatches) { + User user = verifyCredentials(username, password); + if (user == null) { ApiErrorResponseService.reportForbiddenAccess(SecurityErrors.INVALID_USER_PASSWORD); } - if (!passwordEncoder.isHashed(storedPassword)) { - // opportunistically upgrade legacy plaintext password to a bcrypt hash on successful login - user.setPassword(passwordEncoder.encode(password)); - } ApiErrorMessage result = ApiAuthentication.login(user, TLSUtils.getRequest()); if (result != null) { @@ -234,6 +230,26 @@ public UserSessionInformation authenticate(AuthenticationRequest authRequest) { return SecurityService.getInstance().createUserSessionInformation(isAdmin, user, ampTeamName, true); } + /** + * Verifies the given credentials, opportunistically upgrading a legacy (non-bcrypt) stored + * password on success, so both the REST login and the login widget share one verification path. + * + * @return the matching user, or null if the credentials are invalid + */ + public User verifyCredentials(String username, String password) { + User user = UserUtils.getUserByEmailAddress(username); + String storedPassword = (user != null) ? user.getPassword() : null; + AmpPasswordEncoder passwordEncoder = new AmpPasswordEncoder(); + if (storedPassword == null || !passwordEncoder.matches(password, storedPassword)) { + return null; + } + if (!passwordEncoder.isHashed(storedPassword)) { + // opportunistically upgrade legacy plaintext password to a bcrypt hash on successful login + user.setPassword(passwordEncoder.encode(password)); + } + return user; + } + public void invalidateExistingSession() { HttpSession session = TLSUtils.getRequest().getSession(false); if (session != null) { @@ -249,14 +265,27 @@ private AmpTeamMember getAmpTeamMember(String username, Long workspaceId) { return teamMember; } - private void storeInSession(String username, AmpTeamMember teamMember, User user) { + /** + * Establishes the AMP session (legacy attributes + Spring Security context) for a user that has + * already passed {@link #verifyCredentials(String, String)} and {@link ApiAuthentication#login}. + * Used by both the REST login and the login widget (see AmpPostLoginAction), so both share the + * same authenticated state, including on pages/apps (e.g. the activity form) gated by Spring's + * own ROLE_AUTHENTICATED access control rather than only the legacy session attributes. + */ + public void storeInSession(String username, AmpTeamMember teamMember, User user) { // Do not pass credentials to the token — the submitted hash must not be // stored in the Spring Security context or serialised into the session. + UserDetails userDetails = SpringUtil.getBean(DigiUserDetailsService.class).loadUserByUsername(username); final UsernamePasswordAuthenticationToken authToken = - new UsernamePasswordAuthenticationToken(username, null); + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetails(TLSUtils.getRequest())); SecurityContextHolder.getContext().setAuthentication(authToken); final HttpSession session = TLSUtils.getRequest().getSession(); + // /rest/** is create-session="stateless" so SecurityContextPersistenceFilter never persists this; + // save it explicitly under the same key so non-REST pages (Struts .do actions, the Wicket activity + // form) restore it via the normal HttpSessionSecurityContextRepository on their own request chain. + session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, + SecurityContextHolder.getContext()); PermissionUtil.putInScope(session, GatePermConst.ScopeKeys.CURRENT_MEMBER, teamMember); if (teamMember != null) { session.setAttribute(Constants.CURRENT_MEMBER, teamMember.toTeamMember()); diff --git a/amp/src/main/java/org/digijava/kernel/util/UserUtils.java b/amp/src/main/java/org/digijava/kernel/util/UserUtils.java index 088b6c0581a..5d09d48a010 100644 --- a/amp/src/main/java/org/digijava/kernel/util/UserUtils.java +++ b/amp/src/main/java/org/digijava/kernel/util/UserUtils.java @@ -33,6 +33,7 @@ import org.digijava.kernel.request.SiteDomain; import org.digijava.kernel.security.DgSecurityManager; import org.digijava.kernel.security.ResourcePermission; +import org.digijava.kernel.security.auth.AmpPasswordEncoder; import org.digijava.kernel.security.principal.GroupPrincipal; import org.digijava.kernel.security.principal.UserPrincipal; import org.digijava.kernel.user.Group; @@ -512,7 +513,9 @@ public static User getUserByEmailAddress(String email) { * @param password String new password */ public static void setPassword(User user, String password) { - user.setPassword(ShaCrypt.crypt(password.trim()).trim()); + // bcrypt-wrap the SHA1(password) value, matching the domain the login widget submits + // (see AmpPasswordEncoder / SecurityService.verifyCredentials) + user.setPassword(new AmpPasswordEncoder().encode(ShaCrypt.crypt(password.trim()).trim())); user.setSalt(new Long(password.trim().hashCode()).toString()); user.setPasswordChangedAt(new Date()); } diff --git a/amp/src/main/java/org/digijava/module/aim/auth/AmpPostLoginAction.java b/amp/src/main/java/org/digijava/module/aim/auth/AmpPostLoginAction.java index 9ea5aa6b27b..5412126ad95 100644 --- a/amp/src/main/java/org/digijava/module/aim/auth/AmpPostLoginAction.java +++ b/amp/src/main/java/org/digijava/module/aim/auth/AmpPostLoginAction.java @@ -10,12 +10,10 @@ import org.digijava.kernel.ampapi.endpoints.errors.ApiErrorMessage; import org.digijava.kernel.ampapi.endpoints.security.ApiAuthentication; import org.digijava.kernel.ampapi.endpoints.security.SecurityErrors; -import org.digijava.kernel.exception.DgException; +import org.digijava.kernel.ampapi.endpoints.security.SecurityService; import org.digijava.kernel.user.User; -import org.digijava.kernel.util.UserUtils; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; +import org.digijava.module.aim.helper.Constants; +import org.digijava.module.aim.util.AuditLoggerUtil; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -38,19 +36,26 @@ public ActionForward execute(ActionMapping mapping, ActionForm form, String id = request.getParameter("j_autoWorkspaceId"); request.getSession().setAttribute("j_autoWorkspaceId", id); - - Authentication authResult = SecurityContextHolder.getContext().getAuthentication(); - User currentUser = null; - try { - currentUser = getUser(authResult); - } catch(DgException ex) { - throw new RuntimeException(ex); + + String username = request.getParameter("j_username"); + String password = request.getParameter("j_password"); + + SecurityService securityService = SecurityService.getInstance(); + User currentUser = securityService.verifyCredentials(username, password); + if (currentUser == null) { + out.println(getJsonResponse(toLoginWidgetErrorCode(SecurityErrors.INVALID_USER_PASSWORD))); + return null; } - ApiErrorMessage res = ApiAuthentication.login(currentUser, request); - if(res != null) { + ApiErrorMessage res = ApiAuthentication.performSecurityChecks(currentUser, request); + if (res != null) { out.println(getJsonResponse(toLoginWidgetErrorCode(res))); } else { + securityService.invalidateExistingSession(); + securityService.storeInSession(username, null, currentUser); + // re-apply: invalidateExistingSession() discarded the session it was written to above + request.getSession().setAttribute("j_autoWorkspaceId", id); + AuditLoggerUtil.logUserLogin(request, currentUser, Constants.LOGIN_ACTION); out.println(getJsonResponse("noError", null)); } @@ -99,32 +104,4 @@ private String getJsonResponse(String originalMessage, String newMessage) { json+="}"; return json; } - - protected User getUser(Authentication currentAuth) throws DgException { - if(currentAuth == null) { - return null; - } - - if(currentAuth.getPrincipal() == null) { - return null; - } - - User user; - Object principal = currentAuth.getPrincipal(); - if(principal instanceof Long) { - Long userId = (Long) principal; - user = UserUtils.getUser(userId); - } else { - String userName; - if(principal instanceof UserDetails) { - UserDetails userDetails = (UserDetails) principal; - userName = userDetails.getUsername(); - } else { - userName = principal.toString(); - } - user = UserUtils.getUserByEmailAddress(userName); - } - - return user; - } } diff --git a/amp/src/main/java/org/digijava/module/um/util/DbUtil.java b/amp/src/main/java/org/digijava/module/um/util/DbUtil.java index 0fdabf8463b..628edd8c0c3 100644 --- a/amp/src/main/java/org/digijava/module/um/util/DbUtil.java +++ b/amp/src/main/java/org/digijava/module/um/util/DbUtil.java @@ -24,6 +24,7 @@ import org.digijava.kernel.exception.DgException; import org.digijava.kernel.persistence.PersistenceManager; import org.digijava.kernel.request.Site; +import org.digijava.kernel.security.auth.AmpPasswordEncoder; import org.digijava.kernel.user.Group; import org.digijava.kernel.user.User; import org.digijava.kernel.util.*; @@ -102,6 +103,17 @@ public static boolean isCorrectPassword(String user, String pass) throws ////////////////////// while(iter.hasNext()) { User iterUser = (User) iter.next(); + AmpPasswordEncoder passwordEncoder = new AmpPasswordEncoder(); + + if (passwordEncoder.isHashed(iterUser.getPassword())) { + // password was already upgraded (e.g. on a successful login); the login widget + // hashes with SHA1 before this encoder bcrypt-wraps it, so mirror that here + String sha1OfPass = ShaCrypt.crypt(pass.trim()).trim(); + if (passwordEncoder.matches(sha1OfPass, iterUser.getPassword())) { + iscorrect = true; + } + continue; + } for(int i = 0; i < 3; i++) { @@ -198,7 +210,7 @@ public static boolean ResetPassword(String email, String code, String newPasswor return false; } - iterUser.setPassword(ShaCrypt.crypt(newPassword.trim()).trim()); + iterUser.setPassword(hashNewPassword(newPassword)); iterUser.setSalt(new Long(newPassword.trim().hashCode()).toString()); session.update(iterUser); session.delete(resetPassword); @@ -223,6 +235,14 @@ public static boolean ResetPassword(String email, String code, String newPasswor public static void updatePassword(String user, String newPassword) throws UMException{ updatePassword(user, null, newPassword); } + + /** + * Bcrypt-wraps the SHA1(password) value, matching the domain the login widget submits + * (see AmpPasswordEncoder / SecurityService.verifyCredentials). + */ + private static String hashNewPassword(String newPassword) { + return new AmpPasswordEncoder().encode(ShaCrypt.crypt(newPassword.trim()).trim()); + } /** * Update password in database see table * @@ -238,7 +258,7 @@ public static void updatePassword(String user, String oldPassword, session = PersistenceManager.getSession(); User userToUpdate = UserUtils.getUserByEmailAddress(user); - userToUpdate.setPassword(ShaCrypt.crypt(newPassword.trim()).trim()); + userToUpdate.setPassword(hashNewPassword(newPassword)); userToUpdate.setSalt(new Long(newPassword.trim().hashCode()).toString()); userToUpdate.updateLastModified(); session.saveOrUpdate(userToUpdate); From db070972bb9007a04a4fc897c64b159497c6adaa Mon Sep 17 00:00:00 2001 From: brianbrix Date: Thu, 13 Aug 2026 00:07:26 +0300 Subject: [PATCH 15/16] AMP-31197 : Login fails --- .../ampTemplate/js_2/jdigestauth/digest-auth.js | 13 +++++++++++++ amp/src/main/webapp/WEB-INF/applicationContext.xml | 8 ++++++++ 2 files changed, 21 insertions(+) diff --git a/amp/TEMPLATE/ampTemplate/js_2/jdigestauth/digest-auth.js b/amp/TEMPLATE/ampTemplate/js_2/jdigestauth/digest-auth.js index c243e67989f..54d822767a0 100644 --- a/amp/TEMPLATE/ampTemplate/js_2/jdigestauth/digest-auth.js +++ b/amp/TEMPLATE/ampTemplate/js_2/jdigestauth/digest-auth.js @@ -4,6 +4,11 @@ * which verifies it the same way as the /rest/security/user API login. */ +function getCsrfTokenFromCookie() { + var match = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]*)/); + return match ? decodeURIComponent(match[1]) : null; +} + function ajaxLogin() { $('#loader').show(); @@ -72,6 +77,14 @@ function ajaxLogin() { url: '/aim/postLogin.do', type: 'POST', cache: false, + // this page may not load common.js (e.g. the standalone publicPortalLogin.jsp), so + // don't rely on its global CSRF header injection — attach it here directly + beforeSend: function(xhr) { + var token = getCsrfTokenFromCookie(); + if (token) { + xhr.setRequestHeader('X-XSRF-TOKEN', token); + } + }, data: { j_username: $('#j_username').val().trim(), j_password: CryptoJS.SHA1($('#j_password').val()).toString() diff --git a/amp/src/main/webapp/WEB-INF/applicationContext.xml b/amp/src/main/webapp/WEB-INF/applicationContext.xml index 2a75cb73538..fd445c968bc 100644 --- a/amp/src/main/webapp/WEB-INF/applicationContext.xml +++ b/amp/src/main/webapp/WEB-INF/applicationContext.xml @@ -231,6 +231,14 @@ + + + + + + + From ee03261f11a9f6efdf3f6aa9fd5fe1ac8589906c Mon Sep 17 00:00:00 2001 From: brianbrix Date: Thu, 13 Aug 2026 00:42:04 +0300 Subject: [PATCH 16/16] AMP-31197 : Firsttime login fails --- .../ampapi/endpoints/security/services/UserManagerService.java | 2 +- .../main/java/org/digijava/kernel/util/DigiSchemaPopulate.java | 2 +- .../main/java/org/digijava/module/aim/action/RegisterUser.java | 2 +- .../module/aim/startup/AmpBackgroundActivitiesUtil.java | 3 ++- .../main/java/org/digijava/module/um/action/RegisterUser.java | 3 ++- .../main/java/org/digijava/module/um/action/UserRegister.java | 3 ++- .../java/org/digijava/module/um/action/UserRegisterBlank.java | 3 ++- 7 files changed, 11 insertions(+), 7 deletions(-) diff --git a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/services/UserManagerService.java b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/services/UserManagerService.java index 92879e9b7c0..57cec192bee 100644 --- a/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/services/UserManagerService.java +++ b/amp/src/main/java/org/digijava/kernel/ampapi/endpoints/security/services/UserManagerService.java @@ -116,7 +116,7 @@ public LoggedUserInformation createUser(CreateUserRequest createUser) { user.setLastName(lastName); user.setEmail(email); // AMP-SEC-017/054: never store the plaintext password - String hashedPassword = new AmpPasswordEncoder().encode(password); + String hashedPassword = new AmpPasswordEncoder().encode(ShaCrypt.crypt(password.trim()).trim()); user.setPassword(hashedPassword); user.setSalt(hashedPassword); user.setNotificationEmailEnabled(notificationEmailEnabled); diff --git a/amp/src/main/java/org/digijava/kernel/util/DigiSchemaPopulate.java b/amp/src/main/java/org/digijava/kernel/util/DigiSchemaPopulate.java index 69e8bbefd2b..ddcc4c3bee0 100644 --- a/amp/src/main/java/org/digijava/kernel/util/DigiSchemaPopulate.java +++ b/amp/src/main/java/org/digijava/kernel/util/DigiSchemaPopulate.java @@ -295,7 +295,7 @@ static void createGlobalAdmin() throws Exception { user.setLastName("System"); user.setEmail("system@digijava.org"); // AMP-SEC-017/054: never store the plaintext password - user.setPassword(new org.digijava.kernel.security.auth.AmpPasswordEncoder().encode("changeme")); + user.setPassword(new org.digijava.kernel.security.auth.AmpPasswordEncoder().encode(ShaCrypt.crypt("changeme").trim())); user.setRegisterLanguage(english); user.setBanned(false); user.setOrganizationTypeOther(" "); diff --git a/amp/src/main/java/org/digijava/module/aim/action/RegisterUser.java b/amp/src/main/java/org/digijava/module/aim/action/RegisterUser.java index 475a840fdca..f519a210445 100644 --- a/amp/src/main/java/org/digijava/module/aim/action/RegisterUser.java +++ b/amp/src/main/java/org/digijava/module/aim/action/RegisterUser.java @@ -57,7 +57,7 @@ public ActionForward execute(ActionMapping mapping, ActionForm form, return (mapping.getInputForward()); } // set password (AMP-SEC-017/054: never store the plaintext password) - String hashedPassword = new AmpPasswordEncoder().encode(userRegisterForm.getPassword().trim()); + String hashedPassword = new AmpPasswordEncoder().encode(ShaCrypt.crypt(userRegisterForm.getPassword().trim()).trim()); user.setPassword(hashedPassword); user.setSalt(hashedPassword); diff --git a/amp/src/main/java/org/digijava/module/aim/startup/AmpBackgroundActivitiesUtil.java b/amp/src/main/java/org/digijava/module/aim/startup/AmpBackgroundActivitiesUtil.java index aef77b852a8..3a3e71eaf5a 100644 --- a/amp/src/main/java/org/digijava/module/aim/startup/AmpBackgroundActivitiesUtil.java +++ b/amp/src/main/java/org/digijava/module/aim/startup/AmpBackgroundActivitiesUtil.java @@ -7,6 +7,7 @@ import org.digijava.kernel.request.Site; import org.digijava.kernel.user.User; import org.digijava.kernel.util.DgUtil; +import org.digijava.kernel.util.ShaCrypt; import org.digijava.kernel.util.SiteUtils; import org.digijava.kernel.util.UserUtils; import org.digijava.module.aim.dbentity.*; @@ -94,7 +95,7 @@ protected static void createAmpValidatorUser(String userEmail, String firstNames user.setModifyingIP("0.0.0.0"); // set password (AMP-SEC-017/054: never store the plaintext password) - String hashedPassword = new org.digijava.kernel.security.auth.AmpPasswordEncoder().encode(AMP_USER_PASSWORD); + String hashedPassword = new org.digijava.kernel.security.auth.AmpPasswordEncoder().encode(ShaCrypt.crypt(AMP_USER_PASSWORD).trim()); user.setPassword(hashedPassword); user.setSalt(hashedPassword); diff --git a/amp/src/main/java/org/digijava/module/um/action/RegisterUser.java b/amp/src/main/java/org/digijava/module/um/action/RegisterUser.java index 96c26f1aebb..ddc90ff4a3d 100644 --- a/amp/src/main/java/org/digijava/module/um/action/RegisterUser.java +++ b/amp/src/main/java/org/digijava/module/um/action/RegisterUser.java @@ -23,6 +23,7 @@ import org.digijava.kernel.user.User; import org.digijava.kernel.util.DgUtil; import org.digijava.kernel.util.RequestUtils; +import org.digijava.kernel.util.ShaCrypt; import org.digijava.module.aim.dbentity.*; import org.digijava.module.aim.helper.GlobalSettingsConstants; import org.digijava.module.aim.util.FeaturesUtil; @@ -73,7 +74,7 @@ public ActionForward execute(ActionMapping mapping, ActionForm form, return (mapping.getInputForward()); } // set password (AMP-SEC-017/054: never store the plaintext password) - String hashedPassword = new AmpPasswordEncoder().encode(userRegisterForm.getPassword().trim()); + String hashedPassword = new AmpPasswordEncoder().encode(ShaCrypt.crypt(userRegisterForm.getPassword().trim()).trim()); user.setPassword(hashedPassword); user.setSalt(hashedPassword); diff --git a/amp/src/main/java/org/digijava/module/um/action/UserRegister.java b/amp/src/main/java/org/digijava/module/um/action/UserRegister.java index 8fece628ebb..26b487b5f04 100644 --- a/amp/src/main/java/org/digijava/module/um/action/UserRegister.java +++ b/amp/src/main/java/org/digijava/module/um/action/UserRegister.java @@ -39,6 +39,7 @@ import org.digijava.kernel.util.DgUtil; import org.digijava.kernel.util.I18NHelper; import org.digijava.kernel.util.RequestUtils; +import org.digijava.kernel.util.ShaCrypt; import org.digijava.module.um.form.UserRegisterForm; import org.digijava.module.um.util.DbUtil; @@ -84,7 +85,7 @@ public ActionForward execute(ActionMapping mapping, user.setModifyingIP(RequestUtils.getRemoteAddress(request)); // set password (AMP-SEC-017/054: never store the plaintext password) - String hashedPassword = new AmpPasswordEncoder().encode(userRegisterForm.getPassword().trim()); + String hashedPassword = new AmpPasswordEncoder().encode(ShaCrypt.crypt(userRegisterForm.getPassword().trim()).trim()); user.setPassword(hashedPassword); user.setSalt(hashedPassword); diff --git a/amp/src/main/java/org/digijava/module/um/action/UserRegisterBlank.java b/amp/src/main/java/org/digijava/module/um/action/UserRegisterBlank.java index 9e13e5d3994..23b247bebbe 100644 --- a/amp/src/main/java/org/digijava/module/um/action/UserRegisterBlank.java +++ b/amp/src/main/java/org/digijava/module/um/action/UserRegisterBlank.java @@ -38,6 +38,7 @@ import org.digijava.kernel.user.User; import org.digijava.kernel.util.DgUtil; import org.digijava.kernel.util.RequestUtils; +import org.digijava.kernel.util.ShaCrypt; import org.digijava.kernel.util.SiteUtils; import org.digijava.module.um.form.UserRegisterForm; import org.digijava.module.um.util.DbUtil; @@ -79,7 +80,7 @@ public ActionForward execute(ActionMapping mapping, user.setModifyingIP(RequestUtils.getRemoteAddress(request)); // set password (AMP-SEC-017/054: never store the plaintext password) - String hashedPassword = new AmpPasswordEncoder().encode(userRegisterForm.getPassword().trim()); + String hashedPassword = new AmpPasswordEncoder().encode(ShaCrypt.crypt(userRegisterForm.getPassword().trim()).trim()); user.setPassword(hashedPassword); user.setSalt(hashedPassword);