Skip to content

Commit fc0a2da

Browse files
Merge branch 'master' into feat-json-column-type-sql-view
2 parents 733a1b3 + aafa052 commit fc0a2da

95 files changed

Lines changed: 2458 additions & 643 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.

.github/workflows/performance-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ jobs:
332332
333333
- name: Send Slack notification on failure
334334
if: failure() && steps.check_slack.outputs.webhook_configured == 'true'
335-
uses: slackapi/slack-github-action@v3.0.2
335+
uses: slackapi/slack-github-action@v3.0.3
336336
with:
337337
webhook: ${{ secrets.slack_webhook }}
338338
webhook-type: incoming-webhook

dhis-2/dhis-api/src/main/java/org/hisp/dhis/configuration/ConfigurationService.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
*/
3030
package org.hisp.dhis.configuration;
3131

32+
import java.util.Set;
3233
import org.hisp.dhis.user.UserDetails;
3334

3435
/**
@@ -51,6 +52,23 @@ public interface ConfigurationService {
5152
*/
5253
Configuration getConfiguration();
5354

55+
/**
56+
* Returns the configured CORS whitelist. The result is cached for fast read access on hot paths
57+
* (CORS preflight checks, CSP {@code frame-ancestors} composition); the cache is invalidated by
58+
* {@link #setCorsWhitelist(Set)}.
59+
*
60+
* @return the whitelist (never {@code null}).
61+
*/
62+
Set<String> getCorsWhitelist();
63+
64+
/**
65+
* Replaces the CORS whitelist and invalidates the read cache populated by {@link
66+
* #getCorsWhitelist()}.
67+
*
68+
* @param corsWhitelist the new whitelist (may be empty, must not be {@code null}).
69+
*/
70+
void setCorsWhitelist(Set<String> corsWhitelist);
71+
5472
/**
5573
* Indicates whether the given origin is CORS white listed.
5674
*

dhis-2/dhis-api/src/main/java/org/hisp/dhis/metadata/version/MetadataVersionService.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
*/
3030
package org.hisp.dhis.metadata.version;
3131

32+
import java.io.IOException;
33+
import java.io.OutputStream;
3234
import java.util.Date;
3335
import java.util.List;
3436

@@ -128,12 +130,27 @@ public interface MetadataVersionService {
128130
boolean saveVersion(VersionType versionType);
129131

130132
/**
131-
* Gets the Version data - the actual JSON snapshot given the version name.
133+
* Streams the metadata snapshot for the given version name directly to the output stream.
132134
*
133-
* @param versionName
134-
* @return JSON data for the version snapshot
135+
* <p>Scans the stored JSONB wrapper directly without materialising a full Java String copy of the
136+
* metadata field, which is a more memory-efficient path for large snapshots.
137+
*
138+
* @param versionName the version name
139+
* @param out the output stream to write the snapshot to
140+
* @return true if the snapshot was found and written, false if no snapshot exists
141+
* @throws IOException if writing to the output stream fails
142+
*/
143+
boolean streamVersionData(String versionName, OutputStream out) throws IOException;
144+
145+
/**
146+
* Returns whether a metadata snapshot exists for the given version name. Cheap pre-flight check
147+
* intended to be called before opening any response output stream — see {@link
148+
* MetadataVersionStore#metadataVersionSnapshotExists} for the rationale.
149+
*
150+
* @param versionName the version name
151+
* @return true if a snapshot is stored for that version
135152
*/
136-
String getVersionData(String versionName);
153+
boolean snapshotExists(String versionName);
137154

138155
/**
139156
* Creates an entry in the DataStore given the MetadataVersion details.

dhis-2/dhis-api/src/main/java/org/hisp/dhis/metadata/version/MetadataVersionStore.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
*/
3030
package org.hisp.dhis.metadata.version;
3131

32+
import java.io.IOException;
33+
import java.io.OutputStream;
3234
import java.util.Date;
3335
import java.util.List;
3436
import org.hisp.dhis.common.GenericStore;
@@ -75,4 +77,29 @@ public interface MetadataVersionStore extends GenericStore<MetadataVersion> {
7577
* @return Initial/First MetadataVersion of the system
7678
*/
7779
MetadataVersion getInitialVersion();
80+
81+
/**
82+
* Streams the metadata snapshot for the given version name directly from the underlying datastore
83+
* row to the output stream. Avoids materialising the snapshot as a Java String, which is
84+
* memory-prohibitive for large snapshots and would also trip Jackson's default 20MB string-token
85+
* limit during deserialisation of the wrapper object.
86+
*
87+
* @param versionName the version name
88+
* @param out the output stream to write the snapshot to
89+
* @return true if the snapshot was found and written, false if no snapshot exists
90+
* @throws IOException if writing to the output stream fails
91+
*/
92+
boolean streamMetadataVersionData(String versionName, OutputStream out) throws IOException;
93+
94+
/**
95+
* Returns whether a metadata snapshot exists for the given version name. Intended as a cheap
96+
* pre-flight check before opening a response output stream — once {@link
97+
* #streamMetadataVersionData} starts writing, response headers are committed and an error status
98+
* can no longer be returned to the client (especially relevant for the gzipped variant where
99+
* {@code GZIPOutputStream} writes its magic header on construction).
100+
*
101+
* @param versionName the version name
102+
* @return true if a snapshot row exists in the metadata datastore namespace
103+
*/
104+
boolean metadataVersionSnapshotExists(String versionName);
78105
}

dhis-2/dhis-api/src/main/java/org/hisp/dhis/security/oauth2/OAuth2GrantTypes.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
/**
3636
* Shared helpers for Spring Authorization Server {@link AuthorizationGrantType} values.
3737
*
38+
* <p>Used by {@code Dhis2OAuth2ClientServiceImpl} when building a Spring AS {@link
39+
* org.springframework.security.oauth2.server.authorization.client.RegisteredClient} from a
40+
* persisted {@link org.hisp.dhis.security.oauth2.client.Dhis2OAuth2Client}, where the stored
41+
* grant-type strings need to be resolved back to Spring's typed constants.
42+
*
3843
* @author Morten Svanæs <msvanaes@dhis2.org>
3944
*/
4045
public final class OAuth2GrantTypes {
@@ -44,7 +49,7 @@ private OAuth2GrantTypes() {}
4449
/**
4550
* Map a grant-type string back to Spring's canonical {@link AuthorizationGrantType} singleton
4651
* (authorization_code, client_credentials, refresh_token, device_code). Falls back to a new
47-
* instance for any custom value — the equality contract on {@code AuthorizationGrantType} is
52+
* instance for any custom value. The equality contract on {@code AuthorizationGrantType} is
4853
* value-based, but returning the singleton where possible keeps identity comparisons working.
4954
*
5055
* <p>Case labels are the RFC-defined grant-type strings (RFC 6749 + RFC 8628); they match

dhis-2/dhis-api/src/main/java/org/hisp/dhis/security/oauth2/authorization/Dhis2OAuth2Authorization.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
* /api/metadata} export; token-bearing fields are additionally {@link JsonIgnore}'d so no REST
6363
* surface can leak them even on explicit requests. Persistence uses Hibernate field access ({@code
6464
* Dhis2OAuth2Authorization.hbm.xml}) and is independent of the JSON annotations.
65+
*
66+
* @author Morten Svanæs <msvanaes@dhis2.org>
6567
*/
6668
@Getter
6769
@Setter
@@ -73,11 +75,32 @@ public class Dhis2OAuth2Authorization extends BaseIdentifiableObject
7375
/** Required by Hibernate + Jackson for reflective instantiation. */
7476
public Dhis2OAuth2Authorization() {}
7577

78+
/**
79+
* Reference to the {@link org.hisp.dhis.security.oauth2.client.Dhis2OAuth2Client} this grant was
80+
* issued to. Holds the internal id of the registered client, not its public {@code clientId}.
81+
*/
7682
@JsonProperty private String registeredClientId;
83+
84+
/**
85+
* Name of the resource owner the grant is tied to. For user-delegated flows this is the DHIS2
86+
* username; for {@code client_credentials} it is the client itself.
87+
*/
7788
@JsonProperty private String principalName;
89+
90+
/**
91+
* The grant type that produced this authorization (e.g. {@code authorization_code}, {@code
92+
* client_credentials}, {@code refresh_token}, {@code
93+
* urn:ietf:params:oauth:grant-type:device_code}).
94+
*/
7895
@JsonProperty private String authorizationGrantType;
96+
97+
/** Comma-separated list of scopes that were actually granted for this authorization. */
7998
@JsonProperty private String authorizedScopes;
99+
100+
/** JSON-encoded Spring AS attributes map (authenticated principal, request metadata, etc.). */
80101
@JsonIgnore private String attributes;
102+
103+
/** Opaque {@code state} value used by Spring AS for OAuth2 CSRF protection during the flow. */
81104
@JsonIgnore private String state;
82105

83106
@JsonIgnore private String authorizationCodeValue;

dhis-2/dhis-api/src/main/java/org/hisp/dhis/security/oauth2/authorization/Dhis2OAuth2AuthorizationStore.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,19 @@
3333
import javax.annotation.Nonnull;
3434
import org.hisp.dhis.common.IdentifiableObjectStore;
3535

36-
/** Store for OAuth2Authorization entities. */
36+
/**
37+
* Persistence store for {@link Dhis2OAuth2Authorization}. These lookup methods back Spring
38+
* Authorization Server's {@link
39+
* org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService}
40+
* implementation, in particular its {@code findByToken(String, OAuth2TokenType)} contract.
41+
*
42+
* <p>{@link #getByToken(String)} has union semantics: it searches across every token column on
43+
* {@link Dhis2OAuth2Authorization} (authorization code, access token, refresh token, OIDC ID token,
44+
* user code, device code) and returns the first matching row, which is what Spring AS needs when
45+
* asked to resolve a token without knowing its type up-front.
46+
*
47+
* @author Morten Svanæs <msvanaes@dhis2.org>
48+
*/
3749
public interface Dhis2OAuth2AuthorizationStore
3850
extends IdentifiableObjectStore<Dhis2OAuth2Authorization> {
3951

dhis-2/dhis-api/src/main/java/org/hisp/dhis/security/oauth2/client/Dhis2OAuth2Client.java

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,26 @@
3838
import org.hisp.dhis.common.DxfNamespaces;
3939
import org.hisp.dhis.common.MetadataObject;
4040

41+
/**
42+
* Persisted OAuth2 registered-client entity. Mirrors Spring Authorization Server's {@link
43+
* org.springframework.security.oauth2.server.authorization.client.RegisteredClient}, mapped to the
44+
* {@code oauth2_client} table, and is the DB row that authorizes an app to request tokens from
45+
* DHIS2 acting as an Authorization Server.
46+
*
47+
* <p>Exposed via the admin CRUD endpoints under {@code /api/oAuth2Clients}, gated by the {@code
48+
* F_OAUTH2_CLIENT_MANAGE} authority. Clients can also be created dynamically via the Dynamic Client
49+
* Registration (RFC 7591) endpoint {@code /connect/register}.
50+
*
51+
* <p>Several fields are stored as comma-separated strings (for example {@link
52+
* #authorizationGrantTypes}, {@link #clientAuthenticationMethods}, {@link #redirectUris}, {@link
53+
* #postLogoutRedirectUris}, {@link #scopes}) and are parsed into Spring AS's typed values (e.g.
54+
* {@link org.springframework.security.oauth2.core.AuthorizationGrantType}, {@link
55+
* org.springframework.security.oauth2.core.ClientAuthenticationMethod}) by {@code
56+
* Dhis2OAuth2ClientServiceImpl.toObject} when building a {@link
57+
* org.springframework.security.oauth2.server.authorization.client.RegisteredClient}.
58+
*
59+
* @author Morten Svanæs <msvanaes@dhis2.org>
60+
*/
4161
@Getter
4262
@Setter
4363
@JacksonXmlRootElement(localName = "oauth2Client", namespace = DxfNamespaces.DXF_2_0)
@@ -49,7 +69,7 @@ public Dhis2OAuth2Client() {}
4969
* Override so that the persisted {@code name} column is always populated even if the caller (the
5070
* settings UI, which has no name field) doesn't supply one. Hibernate uses property access for
5171
* this entity, so the value returned here is what gets written to the DB and what the schema
52-
* validator at {@code POST /api/schemas/oAuth2Client} reads via reflection letting us keep
72+
* validator at {@code POST /api/schemas/oAuth2Client} reads via reflection, letting us keep
5373
* {@code not-null="true"} on the column without breaking UI pre-validation. Truncated to the
5474
* column length (230) so the schema-validator's {@code @PropertyRange} check on a long {@code
5575
* clientId} (max 255) doesn't reject the request.
@@ -75,15 +95,65 @@ public String getRawName() {
7595
return super.getName();
7696
}
7797

98+
/**
99+
* Public OAuth2 {@code client_id} presented by the client at the token and authorize endpoints.
100+
*/
78101
@JsonProperty private String clientId;
102+
103+
/**
104+
* Client secret used for the {@code client_secret_basic} / {@code client_secret_post}
105+
* authentication methods. Stored hashed; null for public clients and for clients that
106+
* authenticate via {@code private_key_jwt}.
107+
*/
79108
@JsonProperty private String clientSecret;
109+
110+
/** Timestamp at which {@link #clientId} was issued. */
80111
@JsonProperty private Date clientIdIssuedAt;
112+
113+
/** Optional expiry for {@link #clientSecret}; null means the secret does not expire. */
81114
@JsonProperty private Date clientSecretExpiresAt;
115+
116+
/**
117+
* Comma-separated list of OAuth2 client authentication methods the client may use at the token
118+
* endpoint (e.g. {@code client_secret_basic}, {@code client_secret_post}, {@code
119+
* private_key_jwt}, {@code none}). Parsed into Spring AS {@link
120+
* org.springframework.security.oauth2.core.ClientAuthenticationMethod} values at load time.
121+
*/
82122
@JsonProperty private String clientAuthenticationMethods;
123+
124+
/**
125+
* Comma-separated list of OAuth2 authorization grant types the client is permitted to use (e.g.
126+
* {@code authorization_code}, {@code client_credentials}, {@code refresh_token}, {@code
127+
* urn:ietf:params:oauth:grant-type:device_code}). Parsed into Spring AS {@link
128+
* org.springframework.security.oauth2.core.AuthorizationGrantType} values via {@link
129+
* org.hisp.dhis.security.oauth2.OAuth2GrantTypes#resolve(String)}.
130+
*/
83131
@JsonProperty private String authorizationGrantTypes;
132+
133+
/**
134+
* Comma-separated list of registered redirect URIs used by the authorization-code and device-code
135+
* flows; an incoming {@code redirect_uri} must match one of these exactly.
136+
*/
84137
@JsonProperty private String redirectUris;
138+
139+
/** Comma-separated list of post-logout redirect URIs allowed after OIDC RP-initiated logout. */
85140
@JsonProperty private String postLogoutRedirectUris;
141+
142+
/**
143+
* Comma-separated list of OAuth2 / OpenID Connect scopes (e.g. {@code openid}, {@code profile},
144+
* {@code email}) the client is permitted to request.
145+
*/
86146
@JsonProperty private String scopes;
147+
148+
/**
149+
* JSON-encoded Spring AS {@code ClientSettings}; controls client-level options such as whether
150+
* user consent is required and PKCE requirements.
151+
*/
87152
@JsonProperty private String clientSettings;
153+
154+
/**
155+
* JSON-encoded Spring AS {@code TokenSettings}; controls token lifetimes, access-token format
156+
* (opaque vs JWT), refresh-token behavior and ID-token signature algorithm.
157+
*/
88158
@JsonProperty private String tokenSettings;
89159
}

dhis-2/dhis-api/src/main/java/org/hisp/dhis/security/oauth2/client/Dhis2OAuth2ClientStore.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@
3333
import javax.annotation.Nonnull;
3434
import org.hisp.dhis.common.IdentifiableObjectStore;
3535

36-
/** Store for OAuth2Client entities. */
36+
/**
37+
* Persistence store for {@link Dhis2OAuth2Client}. Used by the authorization server's {@link
38+
* org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository}
39+
* implementation to load clients at token-endpoint time.
40+
*
41+
* @author Morten Svanæs <msvanaes@dhis2.org>
42+
*/
3743
public interface Dhis2OAuth2ClientStore extends IdentifiableObjectStore<Dhis2OAuth2Client> {
3844

3945
/**

dhis-2/dhis-api/src/main/java/org/hisp/dhis/security/oauth2/consent/Dhis2OAuth2AuthorizationConsent.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
* <p>Marked {@link SecondaryMetadataObject} so the type is excluded from the default {@code
6161
* /api/metadata} export. Persistence uses Hibernate field access ({@code
6262
* Dhis2OAuth2AuthorizationConsent.hbm.xml}) and is independent of the JSON annotations.
63+
*
64+
* @author Morten Svanæs <msvanaes@dhis2.org>
6365
*/
6466
@Getter
6567
@Setter

0 commit comments

Comments
 (0)