Skip to content

Commit 225ab93

Browse files
committed
change oauth mode check method. add external user check for update password/email in put/patch
Signed-off-by: wenyang-cao <wenyang.cao@ibm.com>
1 parent d4a8301 commit 225ab93

10 files changed

Lines changed: 332 additions & 182 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

7+
## [2.133.0](https://github.com/open-horizon/exchange-api/pull/784) - 2025-07-04
8+
- Issue 782: Disable password update for external users in OAuth mode. Affects Put and Patch User routes, and the change-password route.
9+
- Issue 783: Disable user creation via PUT endpoint in OAuth mode
10+
711
## [2.132.0](https://github.com/open-horizon/exchange-api/pull/792) - 2025-07-03
812
- Enables OAuth authentication without creating an existing or new identity for the route .../v1/myorgs.
913

src/main/resources/exchange.conf

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@ api {
4848

4949
root.enabled = ${?EXCHANGE_ROOT_ENABLED}
5050
root.password = ${?EXCHANGE_ROOT_PW}
51-
52-
oauth.enabled = ${?EXCHANGE_OAUTH_ENABLED}
5351

5452
tls.password = ${?api.tls.password}
5553
tls.password = ${?EXCHANGE_TLS_PASSWORD}

src/main/resources/reference.conf

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,6 @@ api {
153153
root.enabled = true # If set to false it will not honor the root credentials
154154
root.password = null
155155

156-
oauth.enabled = false
157-
158156
service.host = "0.0.0.0"
159157
service.shutdownWaitForRequestsToComplete = 5
160158

src/main/scala/org/openhorizon/exchangeapi/route/user/ChangePassword.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ trait ChangePassword extends JacksonSupport with AuthenticationSupport {
9999

100100
validateWithMsg(reqBody.getAnyProblem) {
101101
val timestamp: java.sql.Timestamp = ApiTime.nowUTCTimestamp
102-
val isOAuthEnabled = Configuration.getConfig.getBoolean("api.oauth.enabled")
102+
val isOAuthEnabled = Configuration.getConfig.hasPath("api.authentication.oauth.provider.user_info.url")
103103

104104
val action =
105105
Compiled(UsersTQ.filter(user => (user.organization === organization &&

src/main/scala/org/openhorizon/exchangeapi/route/user/User.scala

Lines changed: 161 additions & 129 deletions
Large diffs are not rendered by default.

src/main/scala/org/openhorizon/exchangeapi/utility/ApiRespType.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,4 @@ object ApiRespType {
1616
val INFO: String = ExchMsg.translate("info")
1717
val OK: String = ExchMsg.translate("ok")
1818
val TOO_BUSY: String = ExchMsg.translate("too.busy")
19-
val METHOD_NOT_ALLOWED: String = ExchMsg.translate("api.method.not.allowed")
2019
}

src/test/scala/org/openhorizon/exchangeapi/route/user/TestPatchUserRoute.scala

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,15 @@ class TestPatchUserRoute extends AnyFunSuite with BeforeAndAfterAll with BeforeA
109109
modifiedAt = TIMESTAMP,
110110
organization = TESTORGS(0).orgId,
111111
password = Option(Password.hash(ORG1USERPASSWORD)),
112-
username = "orgUser2"))
112+
username = "orgUser2"),
113+
UserRow(createdAt = TIMESTAMP,
114+
isHubAdmin = false,
115+
isOrgAdmin = false,
116+
modifiedAt = TIMESTAMP,
117+
organization = TESTORGS(0).orgId,
118+
password = None,
119+
identityProvider ="External OAuth",
120+
username = "externalUser"))
113121
}
114122

115123
private val TESTAGBOTS: Seq[AgbotRow] =
@@ -205,33 +213,33 @@ class TestPatchUserRoute extends AnyFunSuite with BeforeAndAfterAll with BeforeA
205213
}
206214

207215
def withOauthDisabled(testCode: => Unit): Unit = {
208-
val oauthEnabled = Configuration.getConfig.getBoolean("api.oauth.enabled")
216+
val oauthEnabled = Configuration.getConfig.hasPath("api.authentication.oauth.provider.user_info.url")
209217
assume(!oauthEnabled || runningLocally, "Skipping: OAuth mode enabled and not running locally")
210218

211219
if (oauthEnabled && runningLocally) {
212-
updateConfig("api.oauth.enabled", "false")
220+
updateConfig("api.authentication.oauth.provider.user_info.url", "")
213221
}
214222
try {
215223
testCode
216224
} finally {
217225
if (oauthEnabled && runningLocally) {
218-
updateConfig("api.oauth.enabled", "true")
226+
updateConfig("api.authentication.oauth.provider.user_info.url", "http://localhost:8080/mock-oauth")
219227
}
220228
}
221229
}
222230

223231
def withOauthEnabled(testCode: => Unit): Unit = {
224-
val oauthEnabled = Configuration.getConfig.getBoolean("api.oauth.enabled")
232+
val oauthEnabled = Configuration.getConfig.hasPath("api.authentication.oauth.provider.user_info.url")
225233
assume(oauthEnabled || runningLocally, "Skipping: OAuth mode disabled and not running locally")
226234

227235
if (!oauthEnabled && runningLocally) {
228-
updateConfig("api.oauth.enabled", "true")
229-
}
236+
updateConfig("api.authentication.oauth.provider.user_info.url", "http://localhost:8080/mock-oauth")
237+
}
230238
try {
231239
testCode
232240
} finally {
233241
if (!oauthEnabled && runningLocally) {
234-
updateConfig("api.oauth.enabled", "false")
242+
updateConfig("api.authentication.oauth.provider.user_info.url", "")
235243
}
236244
}
237245
}
@@ -578,8 +586,9 @@ class TestPatchUserRoute extends AnyFunSuite with BeforeAndAfterAll with BeforeA
578586
assert(newUser.modifiedAt.after(TESTUSERS(2).modifiedAt))
579587
}
580588
}
581-
// OAuth mode tests
582-
test("PATCH /orgs/" + TESTORGS(0).orgId + ROUTE + normalUsernameToUpdate + " -- OAuth mode blocks password modification -- 400 bad input") {
589+
590+
// OAuth mode tests
591+
test("PATCH /orgs/" + TESTORGS(0).orgId + ROUTE + normalUsernameToUpdate + " -- OAuth mode allows password modification for local user -- 201 OK") {
583592
withOauthEnabled {
584593
val requestBody: PatchUsersRequest = PatchUsersRequest(
585594
password = Some("newPassword"),
@@ -590,14 +599,32 @@ class TestPatchUserRoute extends AnyFunSuite with BeforeAndAfterAll with BeforeA
590599
val response: HttpResponse[String] = Http(URL + TESTORGS(0).orgId + ROUTE + normalUsernameToUpdate).postData(Serialization.write(requestBody)).method("PATCH").headers(ACCEPT).headers(CONTENT).headers(ORG1ADMINAUTH).asString
591600
info("Code: " + response.code)
592601
info("Body: " + response.body)
602+
assert(response.code === HttpCode.POST_OK.intValue)
603+
604+
val updatedUser: UserRow = Await.result(DBCONNECTION.run(UsersTQ.filter(_.user === TESTUSERS(2).user).result), AWAITDURATION).head
605+
assert(updatedUser.password !== TESTUSERS(2).password) // Password should be changed
606+
}
607+
}
608+
609+
test("PATCH /orgs/" + TESTORGS(0).orgId + ROUTE + TESTUSERS(5).username + " -- OAuth mode blocks password modification for external user -- 400 bad input") {
610+
withOauthEnabled {
611+
val requestBody: PatchUsersRequest = PatchUsersRequest(
612+
password = Some("newPassword"),
613+
admin = None,
614+
hubAdmin = None,
615+
email = None
616+
)
617+
val response: HttpResponse[String] = Http(URL + TESTORGS(0).orgId + ROUTE + TESTUSERS(5).username).postData(Serialization.write(requestBody)).method("PATCH").headers(ACCEPT).headers(CONTENT).headers(ORG1ADMINAUTH).asString
618+
info("Code: " + response.code)
619+
info("Body: " + response.body)
593620
assert(response.code === HttpCode.BAD_INPUT.intValue)
594621
val responseBody: ApiResponse = JsonMethods.parse(response.body).extract[ApiResponse]
595622
assert(responseBody.msg === ExchMsg.translate("user.attr.not.allowed.oauth", "password"))
596-
assertNoChanges(TESTUSERS(2))
623+
assertNoChanges(TESTUSERS(5))
597624
}
598625
}
599626

600-
test("PATCH /orgs/" + TESTORGS(0).orgId + ROUTE + normalUsernameToUpdate + " -- OAuth mode blocks email modification -- 400 bad input") {
627+
test("PATCH /orgs/" + TESTORGS(0).orgId + ROUTE + normalUsernameToUpdate + " -- OAuth mode allows email modification for local user -- 201 OK") {
601628
withOauthEnabled {
602629
val requestBody: PatchUsersRequest = PatchUsersRequest(
603630
password = None,
@@ -608,14 +635,32 @@ class TestPatchUserRoute extends AnyFunSuite with BeforeAndAfterAll with BeforeA
608635
val response: HttpResponse[String] = Http(URL + TESTORGS(0).orgId + ROUTE + normalUsernameToUpdate).postData(Serialization.write(requestBody)).method("PATCH").headers(ACCEPT).headers(CONTENT).headers(ORG1ADMINAUTH).asString
609636
info("Code: " + response.code)
610637
info("Body: " + response.body)
638+
assert(response.code === HttpCode.POST_OK.intValue)
639+
640+
val updatedUser: UserRow = Await.result(DBCONNECTION.run(UsersTQ.filter(_.user === TESTUSERS(2).user).result), AWAITDURATION).head
641+
assert(updatedUser.email === Some("newEmail@example.com"))
642+
}
643+
}
644+
645+
test("PATCH /orgs/" + TESTORGS(0).orgId + ROUTE + TESTUSERS(5).username + " -- OAuth mode blocks email modification for external user -- 400 bad input") {
646+
withOauthEnabled {
647+
val requestBody: PatchUsersRequest = PatchUsersRequest(
648+
password = None,
649+
admin = None,
650+
hubAdmin = None,
651+
email = Some("newEmail@example.com")
652+
)
653+
val response: HttpResponse[String] = Http(URL + TESTORGS(0).orgId + ROUTE + TESTUSERS(5).username).postData(Serialization.write(requestBody)).method("PATCH").headers(ACCEPT).headers(CONTENT).headers(ORG1ADMINAUTH).asString
654+
info("Code: " + response.code)
655+
info("Body: " + response.body)
611656
assert(response.code === HttpCode.BAD_INPUT.intValue)
612657
val responseBody: ApiResponse = JsonMethods.parse(response.body).extract[ApiResponse]
613658
assert(responseBody.msg === ExchMsg.translate("user.attr.not.allowed.oauth", "email"))
614-
assertNoChanges(TESTUSERS(2))
659+
assertNoChanges(TESTUSERS(5))
615660
}
616661
}
617662

618-
test("PATCH /orgs/" + TESTORGS(0).orgId + ROUTE + normalUsernameToUpdate + " -- OAuth mode allows admin modification -- 201 OK") {
663+
test("PATCH /orgs/" + TESTORGS(0).orgId + ROUTE + normalUsernameToUpdate + " -- OAuth mode allows admin modification for local user -- 201 OK") {
619664
withOauthEnabled {
620665
val requestBody: PatchUsersRequest = PatchUsersRequest(
621666
password = None,
@@ -635,6 +680,26 @@ class TestPatchUserRoute extends AnyFunSuite with BeforeAndAfterAll with BeforeA
635680
}
636681
}
637682

683+
test("PATCH /orgs/" + TESTORGS(0).orgId + ROUTE + TESTUSERS(5).username + " -- OAuth mode allows admin modification for external user -- 201 OK") {
684+
withOauthEnabled {
685+
val requestBody: PatchUsersRequest = PatchUsersRequest(
686+
password = None,
687+
admin = Some(true),
688+
hubAdmin = None,
689+
email = None
690+
)
691+
val response: HttpResponse[String] = Http(URL + TESTORGS(0).orgId + ROUTE + TESTUSERS(5).username).postData(Serialization.write(requestBody)).method("PATCH").headers(ACCEPT).headers(CONTENT).headers(ORG1ADMINAUTH).asString
692+
info("Code: " + response.code)
693+
info("Body: " + response.body)
694+
assert(response.code === HttpCode.POST_OK.intValue)
695+
696+
val updatedUser: UserRow = Await.result(DBCONNECTION.run(UsersTQ.filter(_.user === TESTUSERS(5).user).result), AWAITDURATION).head
697+
assert(updatedUser.isOrgAdmin === true)
698+
assert(updatedUser.email === TESTUSERS(5).email) // Should remain unchanged
699+
assert(updatedUser.password === TESTUSERS(5).password) // Should remain unchanged
700+
}
701+
}
702+
638703
test("PATCH /orgs/root" + ROUTE + "TestPatchUserRouteHubAdmin -- OAuth mode allows hubAdmin modification -- 201 OK") {
639704
withOauthEnabled {
640705
val requestBody: PatchUsersRequest = PatchUsersRequest(
@@ -672,5 +737,5 @@ class TestPatchUserRoute extends AnyFunSuite with BeforeAndAfterAll with BeforeA
672737
assertNoChanges(TESTUSERS(2))
673738
}
674739
}
675-
740+
676741
}

src/test/scala/org/openhorizon/exchangeapi/route/user/TestPostChangeUserPasswordRoute.scala

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,33 +197,34 @@ class TestPostChangeUserPasswordRoute extends AnyFunSuite with BeforeAndAfterAll
197197
}
198198

199199
def withOauthDisabled(testCode: => Unit): Unit = {
200-
val oauthEnabled = Configuration.getConfig.getBoolean("api.oauth.enabled")
200+
val oauthEnabled = Configuration.getConfig.hasPath("api.authentication.oauth.provider.user_info.url")
201201
assume(!oauthEnabled || runningLocally, "Skipping: OAuth mode enabled and not running locally")
202202

203203
if (oauthEnabled && runningLocally) {
204-
updateConfig("api.oauth.enabled", "false")
204+
updateConfig("api.authentication.oauth.provider.user_info.url", "")
205205
}
206206
try {
207207
testCode
208208
} finally {
209209
if (oauthEnabled && runningLocally) {
210-
updateConfig("api.oauth.enabled", "true")
210+
updateConfig("api.authentication.oauth.provider.user_info.url", "http://localhost:8080/mock-oauth")
211211
}
212212
}
213213
}
214214

215215
def withOauthEnabled(testCode: => Unit): Unit = {
216-
val oauthEnabled = Configuration.getConfig.getBoolean("api.oauth.enabled")
216+
val oauthEnabled = Configuration.getConfig.hasPath("api.authentication.oauth.provider.user_info.url")
217217
assume(oauthEnabled || runningLocally, "Skipping: OAuth mode disabled and not running locally")
218218

219219
if (!oauthEnabled && runningLocally) {
220-
updateConfig("api.oauth.enabled", "true")
221-
}
220+
updateConfig("api.authentication.oauth.provider.user_info.url", "http://localhost:8080/mock-oauth")
221+
}
222+
222223
try {
223224
testCode
224225
} finally {
225226
if (!oauthEnabled && runningLocally) {
226-
updateConfig("api.oauth.enabled", "false")
227+
updateConfig("api.authentication.oauth.provider.user_info.url", "")
227228
}
228229
}
229230
}

src/test/scala/org/openhorizon/exchangeapi/route/user/TestPostUserRoute.scala

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ class TestPostUserRoute extends AnyFunSuite with BeforeAndAfterAll with BeforeAn
160160
(UsersTQ.filter(users => users.organization === TESTORGS(0).orgId && users.username === "newUser")).delete andThen
161161
(UsersTQ.filter(users => users.organization === "root" && users.username === "TestPostUserRouteNewUser").delete).transactionally
162162
), AWAITDURATION)
163+
164+
val response: HttpResponse[String] = Http(BASEURL + "/admin/clearauthcaches").method("POST").headers(ACCEPT).headers(CONTENT).headers(ROOTAUTH).asString
165+
info("Code: " + response.code)
166+
info("Body: " + response.body)
163167
}
164168

165169
def updateConfig(key: String, value: String): Unit = {
@@ -169,33 +173,33 @@ class TestPostUserRoute extends AnyFunSuite with BeforeAndAfterAll with BeforeAn
169173
}
170174

171175
def withOauthDisabled(testCode: => Unit): Unit = {
172-
val oauthEnabled = Configuration.getConfig.getBoolean("api.oauth.enabled")
176+
val oauthEnabled = Configuration.getConfig.hasPath("api.authentication.oauth.provider.user_info.url")
173177
assume(!oauthEnabled || runningLocally, "Skipping: OAuth mode enabled and not running locally")
174178

175179
if (oauthEnabled && runningLocally) {
176-
updateConfig("api.oauth.enabled", "false")
180+
updateConfig("api.authentication.oauth.provider.user_info.url", "")
177181
}
178182
try {
179183
testCode
180184
} finally {
181185
if (oauthEnabled && runningLocally) {
182-
updateConfig("api.oauth.enabled", "true")
186+
updateConfig("api.authentication.oauth.provider.user_info.url", "http://localhost:8080/mock-oauth")
183187
}
184188
}
185189
}
186190

187191
def withOauthEnabled(testCode: => Unit): Unit = {
188-
val oauthEnabled = Configuration.getConfig.getBoolean("api.oauth.enabled")
192+
val oauthEnabled = Configuration.getConfig.hasPath("api.authentication.oauth.provider.user_info.url")
189193
assume(oauthEnabled || runningLocally, "Skipping: OAuth mode disabled and not running locally")
190194

191195
if (!oauthEnabled && runningLocally) {
192-
updateConfig("api.oauth.enabled", "true")
193-
}
196+
updateConfig("api.authentication.oauth.provider.user_info.url", "http://localhost:8080/mock-oauth")
197+
}
194198
try {
195199
testCode
196200
} finally {
197201
if (!oauthEnabled && runningLocally) {
198-
updateConfig("api.oauth.enabled", "false")
202+
updateConfig("api.authentication.oauth.provider.user_info.url", "")
199203
}
200204
}
201205
}

0 commit comments

Comments
 (0)