Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,35 @@ Useful commands are:

The API listens on port 3000 by default. Swagger UI is available at `/v6/resources/api-docs`, and the source definition is `docs/swagger.yaml`.

## Resource list filters and visibility

`GET /v6/resources` first establishes the caller's authorized resource set and
then intersects any supplied `memberId`, `memberHandle`, and exact `roleId`
filters with that set. These filters are applied before `X-Total`, ordering, and
pagination are calculated.

- Anonymous challenge reads expose only assignments with the configured
Submitter role.
- Ordinary authenticated members can see challenge Submitters plus their own
assignments for other roles. They may restrict by member only when the
requested ID or handle resolves to their own account; cross-member requests
return `403`.
- Administrators, machine callers, resource managers, members assigned the
challenge's Copilot resource role, and members with another challenge-wide
full-access resource retain their existing visibility, with the same exact
filters applied to their result candidates.

For a paginated registrant list, send the challenge UUID and canonical
Submitter role UUID together, for example:

```text
GET /v6/resources?challengeId=<challenge-uuid>&roleId=<submitter-role-uuid>&page=1&perPage=20
```

For a signed-in member's registration check, also provide that caller's own
member ID. The response pagination headers then describe only that exact
challenge/member/role combination.

## Configuration compatibility

The TypeScript conversion retains the existing environment-variable names and defaults. No deployment parameter rename is required.
Expand Down
20 changes: 16 additions & 4 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ paths:
description: |
Retrieve resources assigned to a challenge with optional filtering and sorting.

Results are filtered in two stages: the API first determines the
caller's visible resource assignments, then intersects the optional
`memberId`, `memberHandle`, and exact `roleId` filters with that set.
Filtering happens before the total, sort order, and page are computed.

Anonymous callers can list only Submitter assignments for a challenge.
Ordinary authenticated members can list challenge Submitters and their
own other assignments. An ordinary member may use `memberId` or
`memberHandle` only for their own account; requesting another member is
rejected with `403`. Administrators, M2M callers, and users with
challenge-wide resource access retain their broader visibility.

### Authentication
- JWT roles: `administrator`, `copilot`, `Connect Manager`, `Topcoder User`
- M2M scopes: `read:resources`, `all:resources`
Expand All @@ -79,15 +91,15 @@ paths:
required: true
- name: memberId
type: integer
description: The member id
description: Exact member id. Ordinary authenticated members may request only their own id.
in: query
- name: memberHandle
type: string
description: The member handle
description: Member handle resolved to an exact member filter. Ordinary authenticated members may request only their own handle.
in: query
- name: roleId
type: string
description: role id to filter on
description: Exact resource-role UUID intersected with the caller's visible assignments before pagination.
format: UUID
in: query
required: false
Expand All @@ -103,7 +115,7 @@ paths:
required: false
responses:
'200':
description: OK - the request was successful
description: OK - the request was successful. Pagination metadata is returned in X-Page, X-Per-Page, X-Total, and X-Total-Pages headers.
schema:
type: array
items:
Expand Down
55 changes: 35 additions & 20 deletions src/services/ResourceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,29 @@ async function checkAccess (currentUser, currentUserResources) {
}

/**
* Get resources with given challenge id.
* Get resources that match the requested filters within the caller's visible set.
*
* Anonymous callers can see only challenge submitters. Ordinary authenticated
* callers can see challenge submitters plus their own non-submitter roles;
* requesting another member remains forbidden. Administrators, machine users,
* and callers with challenge-wide access retain their existing visibility.
* Every supplied role or member filter is intersected with that visible set
* before count, ordering, and pagination are calculated.
*
* @param {Object} currentUser the current user
* @param {String} challengeId the challenge id
* @param {String} roleId the role id to filter on
* @param {String} memberId the member id
* @param {String} memberHandle the member handle
* @param {String} roleId the exact resource role id to filter on
* @param {String} memberId the exact member id to filter on
* @param {String} memberHandle the member handle to resolve and filter on
* @param {Number} page The page number
* @param {Number} perPage The number of items to list per page
* @param {Number} sortBy The field that becomes the sorting criteria
* @param {Number} sortOrder The sort order
* @returns {Object} the search result
* @param {String} sortBy The field that becomes the sorting criteria
* @param {String} sortOrder The sort order
* @returns {Promise<Object>} the filtered page and its pagination metadata
* @throws {BadRequestError} when no supported lookup key is supplied
* @throws {ForbiddenError} when an ordinary caller requests another member or
* the caller cannot access the requested challenge
* @throws {NotFoundError} when the requested challenge does not exist
*/
async function getResources (currentUser, challengeId, roleId, memberId, memberHandle, page, perPage, sortBy, sortOrder) {
page = page || 1
Expand Down Expand Up @@ -229,15 +241,18 @@ async function getResources (currentUser, challengeId, roleId, memberId, memberH
] }
]
})
} else {
if (roleId) {
prismaFilter.where.AND.push({ roleId })
}
if (resolvedMemberId) {
prismaFilter.where.AND.push({ memberId: resolvedMemberId })
} else if (memberHandle) {
prismaFilter.where.AND.push({ memberId: '__no_match__' })
}
}

// Query filters always narrow the caller's authorized candidate set. Keeping
// these predicates outside the access branches ensures count and pagination
// describe the exact role/member result for every caller type.
if (roleId) {
prismaFilter.where.AND.push({ roleId })
}
if (resolvedMemberId) {
prismaFilter.where.AND.push({ memberId: resolvedMemberId })
} else if (memberHandle) {
prismaFilter.where.AND.push({ memberId: '__no_match__' })
}

const orderBy = [{ [sortBy]: sortOrder }]
Expand Down Expand Up @@ -312,10 +327,10 @@ async function getResources (currentUser, challengeId, roleId, memberId, memberH

getResources.schema = {
currentUser: Joi.any(),
challengeId: Joi.optionalId(),
roleId: Joi.optionalId(),
memberId: Joi.string(),
memberHandle: Joi.string(),
challengeId: Joi.optionalId().description('Challenge UUID used to scope visible resources'),
roleId: Joi.optionalId().description('Exact resource-role UUID used to narrow visible resources'),
memberId: Joi.string().description('Exact member ID used to narrow visible resources'),
memberHandle: Joi.string().description('Member handle resolved to an exact member filter'),
page: Joi.page().default(1),
perPage: Joi.perPage().default(config.DEFAULT_PAGE_SIZE),
sortBy: Joi.string().valid('memberHandle', 'created').default('created'),
Expand Down
Loading
Loading