From b9046ff8ccaba62b131a9ce4d13526b960c86014 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Fri, 3 Oct 2025 15:59:49 -0600 Subject: [PATCH] feat: add getAdminContext --- build.gradle.kts | 1 + src/main/resources/schema.yml | 103 +++++++++++++++++- .../groovy/org/justserve/JustServeSpec.groovy | 5 + .../justserve/client/UserClientSpec.groovy | 34 +++++- 4 files changed, 139 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index de3176c..b3796eb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation("io.micronaut:micronaut-http-client") implementation("io.micronaut.picocli:micronaut-picocli") implementation("io.micronaut.serde:micronaut-serde-jackson") + testImplementation("net.datafaker:datafaker:2.5.1") compileOnly("org.projectlombok:lombok") runtimeOnly("ch.qos.logback:logback-classic") runtimeOnly("org.yaml:snakeyaml") diff --git a/src/main/resources/schema.yml b/src/main/resources/schema.yml index c9ec7aa..6beb725 100644 --- a/src/main/resources/schema.yml +++ b/src/main/resources/schema.yml @@ -21,6 +21,26 @@ paths: description: Bad Request '500': description: Internal Server Error + /api/v1/users/securitycontext/bearer: + get: + description: "Retrieves the admin context for a given user (ie org ID's, church unit id's, role levels, etc). if no user is provided, it will return the context for the currently authenticated user" + operationId: getAdminContext + tags: + - User + parameters: + - name: userId + in: query + schema: + type: string + format: uuid + responses: + '200': + description: OK + content: { application/json: { schema: { $ref: '#/components/schemas/SecurityContext' } } } + '401': # No failures for unfound endpoints. querying ID "this is a bad ID" returns the current user's bearer context. + description: Unauthorized + content: { application/json: { schema: { $ref: '#/components/schemas/ProblemDetails' } } } + /api/v1/users/slimSearch: post: operationId: userSearchSlim @@ -57,6 +77,33 @@ paths: components: schemas: + ChurchCivicGeographyUserRole: + type: object + properties: + churchGeographyId: + type: string + format: uuid + unitId: + type: string + nullable: true + civicGeographyId: + type: string + format: uuid + role: + $ref: '#/components/schemas/Role' + additionalProperties: false + ChurchGeographyUserRole: + type: object + properties: + churchGeographyId: + type: string + format: uuid + unitId: + type: string + nullable: true + role: + $ref: '#/components/schemas/Role' + additionalProperties: false DynamicRoutingDataResponse: type: object properties: @@ -74,6 +121,15 @@ components: - Organization - DisasterRelief additionalProperties: false + OrganizationUserRole: + type: object + properties: + organizationId: + type: string + format: uuid + role: + $ref: '#/components/schemas/Role' + additionalProperties: false UserHashRequest: description: | A request containing either the email or the userid for a user. @@ -358,6 +414,18 @@ components: deletedOn: { type: string, format: date-time, nullable: true } deletedBy: { type: string, format: uuid, nullable: true } additionalProperties: false + OrganizationCivicGeographyUserRole: + type: object + properties: + organizationId: + type: string + format: uuid + role: + $ref: '#/components/schemas/Role' + civicGeographyId: + type: string + format: uuid + additionalProperties: false Location: type: object properties: { latitude: { type: number, format: double }, @@ -411,4 +479,37 @@ components: type: integer format: int32 nullable: true - traceId: { type: string, nullable: true } \ No newline at end of file + traceId: { type: string, nullable: true } + SecurityContext: + type: object + properties: + userId: + type: string + format: uuid + userRepresentativeForOrganizations: + type: array + items: + type: string + format: uuid + nullable: true + churchGeographies: + type: object + additionalProperties: + $ref: '#/components/schemas/ChurchGeographyUserRole' + nullable: true + civicGeographies: + type: object + additionalProperties: + $ref: '#/components/schemas/ChurchCivicGeographyUserRole' + nullable: true + organizationRoles: + type: object + additionalProperties: + $ref: '#/components/schemas/OrganizationUserRole' + nullable: true + organizationCivicGeographyUserRoles: + type: array + items: + $ref: '#/components/schemas/OrganizationCivicGeographyUserRole' + nullable: true + additionalProperties: false \ No newline at end of file diff --git a/src/test/groovy/org/justserve/JustServeSpec.groovy b/src/test/groovy/org/justserve/JustServeSpec.groovy index 75fbe0d..78073b1 100644 --- a/src/test/groovy/org/justserve/JustServeSpec.groovy +++ b/src/test/groovy/org/justserve/JustServeSpec.groovy @@ -3,12 +3,16 @@ package org.justserve import io.micronaut.context.ApplicationContext import io.micronaut.context.env.Environment import io.micronaut.test.extensions.spock.annotation.MicronautTest +import net.datafaker.Faker import spock.lang.Shared import spock.lang.Specification @MicronautTest() class JustServeSpec extends Specification { + @Shared + Faker faker + @Shared ApplicationContext ctx, noAuthCtx @@ -16,6 +20,7 @@ class JustServeSpec extends Specification { String userEmail def setupSpec() { + faker = new Faker() userEmail = "jimmy@justserve.org" if (null != System.getenv("JUSTSERVE_TOKEN")) { diff --git a/src/test/groovy/org/justserve/client/UserClientSpec.groovy b/src/test/groovy/org/justserve/client/UserClientSpec.groovy index 5f0ffbb..166205f 100644 --- a/src/test/groovy/org/justserve/client/UserClientSpec.groovy +++ b/src/test/groovy/org/justserve/client/UserClientSpec.groovy @@ -1,13 +1,13 @@ package org.justserve.client - import io.micronaut.http.HttpResponse -import io.micronaut.http.HttpStatus import io.micronaut.http.client.exceptions.HttpClientResponseException import org.justserve.JustServeSpec import org.justserve.model.UserHashRequestByEmail import spock.lang.Shared +import static io.micronaut.http.HttpStatus.* + class UserClientSpec extends JustServeSpec { @Shared @@ -19,6 +19,34 @@ class UserClientSpec extends JustServeSpec { userClient = ctx.getBean(UserClient) } + def "get admin context for #description"() { + when: + def response = null + def thrownException = null + try { + response = client.getAdminContext(UUID.fromString(uid)) + } catch (HttpClientResponseException e) { + thrownException = e + } + + then: + if (thrownException) { + thrownException.status == expectedStatus + } else { + response.status == expectedStatus + response.body() != null + if (expectedStatus == OK) { + response.body().userId == UUID.fromString(uid) + } + } + + where: + description | uid | client | expectedStatus + "a valid user" | "e23d029c-25f6-4c93-aada-dcbdc6d50c2c" | userClient | OK + "an invalid user" | faker.internet().uuid() | userClient | NOT_FOUND + "a valid user no-auth" | "e23d029c-25f6-4c93-aada-dcbdc6d50c2c" | noAuthUserClient | UNAUTHORIZED + } + def "get tempPassword for #email"() { when: HttpResponse response = null @@ -46,7 +74,7 @@ class UserClientSpec extends JustServeSpec { where: expectedResponse | email | expectedException | expectedMessage | client | _ - HttpStatus.OK | userEmail | null | null | userClient | _ + OK | userEmail | null | null | userClient | _ null | "notanemail@mail.moc" | HttpClientResponseException | "\"status\":500" | userClient | _ null | userEmail | HttpClientResponseException | "\"status\":401" | noAuthUserClient | _ null | "notanemail@mail.moc" | HttpClientResponseException | "\"status\":401" | noAuthUserClient | _