Skip to content

Commit 707de08

Browse files
Merge branch 'master' into fix-userrole-jsonpatch-lazy-members
2 parents 4211e04 + 894a102 commit 707de08

97 files changed

Lines changed: 4690 additions & 334 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
name: Run auth IdP tests
2+
3+
# External-IdP authentication e2e suite (Keycloak OIDC, LDAP): runs the dhis-test-e2e
4+
# "auth-idp" tagged tests against the docker-compose.e2e-auth.yml stack.
5+
# Runs on every PR (reusing the dhis2/core-pr image published by "Run api tests"
6+
# when possible), nightly on master, and manually.
7+
8+
env:
9+
MAVEN_OPTS: -Xmx1024m -Xms1024m -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.http.retryHandler.class=standard -Dmaven.wagon.http.retryHandler.count=3 -Dmaven.wagon.httpconnectionManager.ttlSeconds=125
10+
11+
on:
12+
pull_request:
13+
schedule:
14+
# Nightly on the default branch
15+
- cron: "30 3 * * *"
16+
workflow_dispatch:
17+
18+
concurrency:
19+
group: ${{ github.workflow }}-${{ github.ref }}
20+
cancel-in-progress: true
21+
22+
jobs:
23+
auth-idp-test:
24+
runs-on: ubuntu-latest
25+
env:
26+
# Mirrors DOCKERHUB_PUSH in run-api-tests.yml: for same-repo PRs the api tests
27+
# workflow pushes dhis2/core-pr:<number> to Docker Hub, so we pull it instead of
28+
# rebuilding. Fork/dependabot PRs, nightly and dispatch runs build the image here.
29+
REUSE_PR_IMAGE: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
30+
CORE_IMAGE_NAME: ${{ (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]') && format('dhis2/core-pr:{0}', github.event.number) || 'dhis2/core-dev:local' }}
31+
steps:
32+
- uses: actions/checkout@v6
33+
- name: Set up JDK 17
34+
if: env.REUSE_PR_IMAGE != 'true'
35+
uses: actions/setup-java@v5
36+
with:
37+
java-version: 17
38+
distribution: temurin
39+
cache: maven
40+
41+
- name: Wait for PR image from api tests build
42+
if: env.REUSE_PR_IMAGE == 'true'
43+
run: |
44+
expected="${{ github.event.pull_request.head.sha }}"
45+
echo "Waiting for $CORE_IMAGE_NAME with revision $expected"
46+
for i in $(seq 1 60); do
47+
if docker pull --quiet "$CORE_IMAGE_NAME" > /dev/null 2>&1; then
48+
revision=$(docker inspect --format '{{ index .Config.Labels "DHIS2_BUILD_REVISION" }}' "$CORE_IMAGE_NAME")
49+
if [ "$revision" = "$expected" ]; then
50+
echo "Found $CORE_IMAGE_NAME for revision $revision"
51+
exit 0
52+
fi
53+
echo "Image revision '$revision' is stale, retrying..."
54+
fi
55+
sleep 20
56+
done
57+
echo "Timed out waiting for $CORE_IMAGE_NAME with revision $expected (did the api tests build fail?)" >&2
58+
exit 1
59+
60+
- name: Build container image
61+
if: env.REUSE_PR_IMAGE != 'true'
62+
run: |
63+
mvn clean verify --threads 2C --batch-mode --no-transfer-progress \
64+
-DskipTests -Dpackaging.type=jar -Dmdep.analyze.skip --update-snapshots --file dhis-2/pom.xml \
65+
--projects dhis-web-server --also-make --activate-profiles embedded,jibDockerBuild \
66+
-Djib.to.image=$CORE_IMAGE_NAME
67+
68+
- name: Run auth IdP tests
69+
run: |
70+
cd dhis-2/dhis-test-e2e
71+
DHIS2_E2E_TEST_ARGS="-Pauth-idp" SELENIUM_IMAGE=selenium/standalone-chrome:latest DHIS2_IMAGE=$CORE_IMAGE_NAME \
72+
docker compose -f docker-compose.yml -f docker-compose.e2e.yml -f docker-compose.e2e-auth.yml \
73+
up --remove-orphans --exit-code-from test
74+
75+
- name: Upload logs
76+
if: failure()
77+
run: |
78+
cd dhis-2/dhis-test-e2e
79+
docker compose -f docker-compose.yml -f docker-compose.e2e.yml -f docker-compose.e2e-auth.yml logs web > ~/web-logs.txt
80+
docker compose -f docker-compose.yml -f docker-compose.e2e.yml -f docker-compose.e2e-auth.yml logs keycloak > ~/keycloak-logs.txt
81+
82+
- uses: actions/upload-artifact@v7
83+
if: failure()
84+
with:
85+
name: "auth_idp_logs"
86+
path: |
87+
~/web-logs.txt
88+
~/keycloak-logs.txt
89+
90+
send-slack-message:
91+
runs-on: ubuntu-latest
92+
if: |
93+
always() &&
94+
contains(needs.*.result, 'failure') &&
95+
github.event_name == 'schedule'
96+
needs: [auth-idp-test]
97+
steps:
98+
- uses: rtCamp/action-slack-notify@v2
99+
env:
100+
SLACK_WEBHOOK: ${{ secrets.SLACK_BACKEND_WEBHOOK }}
101+
SLACK_CHANNEL: "team-backend"
102+
SLACK_MESSAGE: "Nightly auth IdP (Keycloak/LDAP) e2e run failed and needs investigation :detective-duck:."
103+
SLACK_COLOR: "#ff0000"

dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionComboStore.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@ public interface CategoryOptionComboStore extends IdentifiableObjectStore<Catego
4444
CategoryOptionCombo getCategoryOptionCombo(
4545
CategoryCombo categoryCombo, Set<CategoryOption> categoryOptions);
4646

47+
/**
48+
* Returns the {@link CategoryOptionCombo}s of the given {@link CategoryCombo}s with their {@link
49+
* CategoryOptionCombo#getCategoryOptions()} eagerly fetched in a single query. Used to prime the
50+
* session before serialising category option combos, avoiding the per-combo N+1 select on the
51+
* {@code categoryoptioncombos_categoryoptions} join table.
52+
*
53+
* @param categoryCombos the category combos whose option combos to load.
54+
* @return the option combos with their category options initialised.
55+
*/
56+
List<CategoryOptionCombo> getCategoryOptionCombosWithCategoryOptions(
57+
Collection<CategoryCombo> categoryCombos);
58+
4759
void updateNames();
4860

4961
void deleteNoRollBack(CategoryOptionCombo categoryOptionCombo);

dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryService.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,18 @@ CategoryOptionCombo getCategoryOptionCombo(
412412
*/
413413
List<CategoryOptionCombo> getAllCategoryOptionCombos();
414414

415+
/**
416+
* Returns the {@link CategoryOptionCombo}s of the given {@link CategoryCombo}s with their
417+
* category options eagerly fetched in a single query. Used to prime the session before
418+
* serialising category option combos, avoiding the per-combo N+1 select on the {@code
419+
* categoryoptioncombos_categoryoptions} join table.
420+
*
421+
* @param categoryCombos the category combos whose option combos to load.
422+
* @return the option combos with their category options initialised.
423+
*/
424+
List<CategoryOptionCombo> getCategoryOptionCombosWithCategoryOptions(
425+
Collection<CategoryCombo> categoryCombos);
426+
415427
/**
416428
* Generates and persists a default Category, CategoryOption, CategoryCombo and
417429
* CategoryOptionCombo.

dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/OAuth2ClientCredentialsAuthScheme.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
package org.hisp.dhis.common.auth;
3131

3232
import com.fasterxml.jackson.annotation.JsonProperty;
33+
import java.util.Collection;
3334
import java.util.List;
3435
import java.util.Map;
3536
import java.util.function.UnaryOperator;
@@ -44,6 +45,7 @@
4445
import org.springframework.context.ApplicationContext;
4546
import org.springframework.security.authentication.AbstractAuthenticationToken;
4647
import org.springframework.security.core.Authentication;
48+
import org.springframework.security.core.GrantedAuthority;
4749
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
4850
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
4951
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
@@ -64,7 +66,7 @@ public class OAuth2ClientCredentialsAuthScheme implements AuthScheme {
6466
public static final String OAUTH2_CLIENT_CREDENTIALS_TYPE = "oauth2-client-credentials";
6567

6668
public static final Authentication ANONYMOUS_AUTHENTICATION =
67-
new AbstractAuthenticationToken(null) {
69+
new AbstractAuthenticationToken((Collection<? extends GrantedAuthority>) null) {
6870
@Override
6971
public Object getCredentials() {
7072
return "";

dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/DataSetService.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
*/
3030
package org.hisp.dhis.dataset;
3131

32+
import java.util.Collection;
3233
import java.util.Date;
3334
import java.util.List;
3435
import org.apache.commons.collections4.SetValuedMap;
36+
import org.hisp.dhis.dataelement.DataElement;
3537
import org.hisp.dhis.dataentryform.DataEntryForm;
3638
import org.hisp.dhis.organisationunit.OrganisationUnit;
3739
import org.hisp.dhis.period.Period;
@@ -83,6 +85,19 @@ public interface DataSetService extends DataSetDataIntegrityProvider {
8385
*/
8486
DataSet getDataSet(String uid);
8587

88+
/**
89+
* Returns the distinct {@link DataElement}s that are members of the given data sets, loaded in a
90+
* single query (with each data element's {@code dataSetElements} and {@code categoryCombo}
91+
* eagerly fetched) to avoid N+1 selects when iterating {@link DataSet#getDataElements()} and
92+
* {@link DataElement#getCategoryCombos()} across many data sets. Does not apply sharing/ACL
93+
* predicates on the data elements; callers are responsible for authorizing access to the data
94+
* sets.
95+
*
96+
* @param dataSets the data sets.
97+
* @return the distinct data elements of the given data sets.
98+
*/
99+
List<DataElement> getDataElementsByDataSet(Collection<DataSet> dataSets);
100+
86101
/**
87102
* Returns the DataSet with the given UID. Bypasses the ACL system.
88103
*

dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/DataSetStore.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,22 @@ public interface DataSetStore
5353

5454
List<DataSetElement> getDataSetElementsByDataElement(Collection<DataElement> dataElements);
5555

56+
/**
57+
* Returns the distinct {@link DataElement}s that are members of the given data sets (via {@link
58+
* DataSetElement}), loaded in a single query. Each data element's {@link
59+
* DataElement#getDataSetElements()} collection and its {@link DataElement#getCategoryCombo()} are
60+
* eagerly fetched as well, so that {@link DataSet#getDataElements()} and {@link
61+
* DataElement#getCategoryCombos()} can be evaluated without triggering per-element (or
62+
* per-category-combo) N+1 selects.
63+
*
64+
* <p>This method does <b>not</b> apply sharing/ACL predicates on the data elements. Callers are
65+
* responsible for authorizing access to the data sets.
66+
*
67+
* @param dataSets the data sets.
68+
* @return the distinct data elements of the given data sets.
69+
*/
70+
List<DataElement> getDataElementsByDataSet(Collection<DataSet> dataSets);
71+
5672
/** Get all DatSets that have any of the CategoryCombos. */
5773
List<DataSet> getByCategoryCombo(List<CategoryCombo> sources);
5874

dhis-2/dhis-api/src/main/java/org/hisp/dhis/program/Program.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,21 @@ public class Program extends BaseMetadataObject
185185
@Column(name = "relationshiplabel", columnDefinition = "text")
186186
private String relationshipLabel;
187187

188+
@Column(name = "relationshipslabel", columnDefinition = "text")
189+
private String relationshipsLabel;
190+
188191
@Column(name = "notelabel", columnDefinition = "text")
189192
private String noteLabel;
190193

194+
@Column(name = "noteslabel", columnDefinition = "text")
195+
private String notesLabel;
196+
191197
@Column(name = "trackedentityattributelabel", columnDefinition = "text")
192198
private String trackedEntityAttributeLabel;
193199

200+
@Column(name = "trackedentityattributeslabel", columnDefinition = "text")
201+
private String trackedEntityAttributesLabel;
202+
194203
@Column(name = "programstagelabel", columnDefinition = "text")
195204
private String programStageLabel;
196205

@@ -969,6 +978,24 @@ public void setRelationshipLabel(String relationshipLabel) {
969978
this.relationshipLabel = relationshipLabel;
970979
}
971980

981+
@JsonProperty
982+
@JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
983+
@PropertyRange(min = 2)
984+
public String getRelationshipsLabel() {
985+
return relationshipsLabel;
986+
}
987+
988+
@JsonProperty
989+
@JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
990+
@Translatable(propertyName = "relationshipsLabel", key = "RELATIONSHIPS_LABEL")
991+
public String getDisplayRelationshipsLabel() {
992+
return getTranslation("RELATIONSHIPS_LABEL", getRelationshipsLabel());
993+
}
994+
995+
public void setRelationshipsLabel(String relationshipsLabel) {
996+
this.relationshipsLabel = relationshipsLabel;
997+
}
998+
972999
@JsonProperty
9731000
@JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
9741001
@PropertyRange(min = 2)
@@ -987,6 +1014,24 @@ public void setNoteLabel(String noteLabel) {
9871014
this.noteLabel = noteLabel;
9881015
}
9891016

1017+
@JsonProperty
1018+
@JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
1019+
@PropertyRange(min = 2)
1020+
public String getNotesLabel() {
1021+
return notesLabel;
1022+
}
1023+
1024+
@JsonProperty
1025+
@JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
1026+
@Translatable(propertyName = "notesLabel", key = "NOTES_LABEL")
1027+
public String getDisplayNotesLabel() {
1028+
return getTranslation("NOTES_LABEL", getNotesLabel());
1029+
}
1030+
1031+
public void setNotesLabel(String notesLabel) {
1032+
this.notesLabel = notesLabel;
1033+
}
1034+
9901035
@JsonProperty
9911036
@JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
9921037
@PropertyRange(min = 2)
@@ -1007,6 +1052,26 @@ public void setTrackedEntityAttributeLabel(String trackedEntityAttributeLabel) {
10071052
this.trackedEntityAttributeLabel = trackedEntityAttributeLabel;
10081053
}
10091054

1055+
@JsonProperty
1056+
@JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
1057+
@PropertyRange(min = 2)
1058+
public String getTrackedEntityAttributesLabel() {
1059+
return trackedEntityAttributesLabel;
1060+
}
1061+
1062+
@JsonProperty
1063+
@JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
1064+
@Translatable(
1065+
propertyName = "trackedEntityAttributesLabel",
1066+
key = "TRACKED_ENTITY_ATTRIBUTES_LABEL")
1067+
public String getDisplayTrackedEntityAttributesLabel() {
1068+
return getTranslation("TRACKED_ENTITY_ATTRIBUTES_LABEL", getTrackedEntityAttributesLabel());
1069+
}
1070+
1071+
public void setTrackedEntityAttributesLabel(String trackedEntityAttributesLabel) {
1072+
this.trackedEntityAttributesLabel = trackedEntityAttributesLabel;
1073+
}
1074+
10101075
@JsonProperty
10111076
@JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
10121077
@PropertyRange(min = 2)
@@ -1513,14 +1578,17 @@ private static void setShallowCopyValues(
15131578
copy.setEnrollmentLabel(original.getEnrollmentLabel());
15141579
copy.setEnrollmentsLabel(original.getEnrollmentsLabel());
15151580
copy.setNoteLabel(original.getNoteLabel());
1581+
copy.setNotesLabel(original.getNotesLabel());
15161582
copy.setFollowUpLabel(original.getFollowUpLabel());
15171583
copy.setOrgUnitLabel(original.getOrgUnitLabel());
15181584
copy.setTrackedEntityAttributeLabel(original.getTrackedEntityAttributeLabel());
1585+
copy.setTrackedEntityAttributesLabel(original.getTrackedEntityAttributesLabel());
15191586
copy.setProgramStageLabel(original.getProgramStageLabel());
15201587
copy.setProgramStagesLabel(original.getProgramStagesLabel());
15211588
copy.setEventLabel(original.getEventLabel());
15221589
copy.setEventsLabel(original.getEventsLabel());
15231590
copy.setRelationshipLabel(original.getRelationshipLabel());
1591+
copy.setRelationshipsLabel(original.getRelationshipsLabel());
15241592
copy.setEnableChangeLog(original.isEnableChangeLog());
15251593
}
15261594

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) 2004-2022, University of Oslo
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are met:
7+
*
8+
* 1. Redistributions of source code must retain the above copyright notice, this
9+
* list of conditions and the following disclaimer.
10+
*
11+
* 2. Redistributions in binary form must reproduce the above copyright notice,
12+
* this list of conditions and the following disclaimer in the documentation
13+
* and/or other materials provided with the distribution.
14+
*
15+
* 3. Neither the name of the copyright holder nor the names of its contributors
16+
* may be used to endorse or promote products derived from this software without
17+
* specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
23+
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24+
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25+
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26+
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28+
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package org.hisp.dhis.user;
31+
32+
import org.hisp.dhis.common.UID;
33+
34+
/**
35+
* Event published after the authorities of a {@link UserRole} have changed, so that active sessions
36+
* of users with that role can be invalidated asynchronously, outside the updating transaction and
37+
* off the request thread.
38+
*
39+
* @author Morten Svanæs <msvanaes@dhis2.org>
40+
*/
41+
public record UserRoleAuthoritiesChangedEvent(UID userRoleUid) {}

dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserService.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,14 @@ boolean canCurrentUserCanModify(
584584
*/
585585
void invalidateUserSessions(String username);
586586

587+
/**
588+
* Returns the usernames of all users that are members of the user role with the given UID.
589+
*
590+
* @param roleUid the UID of the user role.
591+
* @return a list of usernames.
592+
*/
593+
List<String> getUsernamesByUserRole(@Nonnull UID roleUid);
594+
587595
/**
588596
* Register a account recovery attempt for the given user account.
589597
*

0 commit comments

Comments
 (0)