Skip to content
Open
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 backend/data_tools/data/user_data.json5
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,26 @@
"POST_UPLOAD_DOCUMENT"
]
},
{ // 8 Read-Only
name: "READ_ONLY",
permissions: [
"GET_AGREEMENT",
"GET_BUDGET_LINE_ITEM",
"GET_SERVICES_COMPONENT",
"GET_BLI_PACKAGE",
"GET_CAN",
"GET_DIVISION",
"GET_NOTIFICATION",
"GET_PORTFOLIO",
"GET_RESEARCH_PROJECT",
"GET_USER",
"GET_HISTORY",
"GET_WORKFLOW",
"GET_CHANGE_REQUEST",
"GET_CHANGE_REQUEST_REVIEW",
"GET_UPLOAD_DOCUMENT"
]
},
],
ops_user: [
{ // 500
Expand Down Expand Up @@ -665,6 +685,15 @@
oidc_id: "00000000-0000-1111-a111-000000000028",
roles: [{"tablename": "role", "id": 7}],
status: "ACTIVE"
},
{ // 529
first_name: "Randy",
last_name: "Read-Only",
division: 3,
email: "randy.readonly@email.com",
oidc_id: "00000000-0000-1111-a111-000000000029",
roles: [{"tablename": "role", "id": 8}],
status: "ACTIVE"
}
],
notification: [
Expand Down
239 changes: 234 additions & 5 deletions backend/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2866,14 +2866,66 @@ paths:
tags:
- Users
operationId: getAllUsers
description: Get all Users
description: Get all Users. Returns full user details for admins, or SafeUser (id and full_name only) for non-admins viewing other users.
parameters:
- $ref: "#/components/parameters/simulatedError"
- name: id
in: query
description: Filter by user ID
schema:
type: integer
- name: oidc_id
in: query
description: Filter by OIDC ID
schema:
type: string
example: e5711101-bc3e-41e5-a6a2-051874b307ca
- name: hhs_id
in: query
description: Filter by HHS ID
schema:
type: string
- name: email
in: query
description: Filter by email
schema:
type: string
- name: status
in: query
description: Filter by user status
schema:
type: string
enum: [active, inactive, locked]
- name: division
in: query
description: Filter by division ID
schema:
type: integer
- name: first_name
in: query
description: Filter by first name
schema:
type: string
- name: last_name
in: query
description: Filter by last name
schema:
type: string
- name: roles
in: query
description: Filter by role names (can specify multiple)
schema:
type: array
items:
type: string
style: form
explode: true
- name: exclude_read_only
in: query
description: Exclude users with READ_ONLY role from results
schema:
type: boolean
default: false
responses:
"200":
description: OK
Expand Down Expand Up @@ -2950,23 +3002,77 @@ paths:
updated_by: 101
created_on: "2023-04-06T20:33:38.292475"
updated_on: null
/users/{user_id}:
post:
tags:
- Users
operationId: createUser
description: Create a new user. Only USER_ADMIN role can create users.
parameters:
- $ref: "#/components/parameters/simulatedError"
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- email
properties:
email:
type: string
format: email
first_name:
type: string
nullable: true
last_name:
type: string
nullable: true
division:
type: integer
nullable: true
status:
type: string
enum: [active, inactive, locked]
default: inactive
roles:
type: array
items:
type: string
default: []
example:
email: "new.user@example.com"
first_name: "John"
last_name: "Doe"
division: 1
status: "active"
roles: ["COR"]
responses:
"202":
description: User created successfully
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"400":
description: Bad Request - Invalid data or insufficient permissions
"403":
description: Forbidden - User does not have USER_ADMIN role
/users/{id}:
get:
tags:
- Users
operationId: getUserById
description: Get User by Id
description: Get User by Id. Returns full user details for admins or when viewing own profile, SafeUser (id and full_name only) otherwise.
parameters:
- $ref: "#/components/parameters/simulatedError"
- in: path
name: user_id
name: id
description: User Id
required: true
schema:
type: integer
format: int32
minimum: 0
default: 0
responses:
"200":
description: OK
Expand All @@ -2977,6 +3083,129 @@ paths:
examples:
"0":
$ref: "#/components/examples/User"
"404":
description: User not found
put:
tags:
- Users
operationId: updateUser
description: Update a user by ID. Only USER_ADMIN role can update users.
parameters:
- $ref: "#/components/parameters/simulatedError"
- in: path
name: id
description: User Id
required: true
schema:
type: integer
format: int32
minimum: 0
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
type: integer
email:
type: string
format: email
first_name:
type: string
nullable: true
last_name:
type: string
nullable: true
division:
type: integer
nullable: true
status:
type: string
enum: [active, inactive, locked]
roles:
type: array
items:
type: string
example:
email: "updated.user@example.com"
first_name: "Jane"
last_name: "Smith"
division: 2
status: "active"
roles: ["Division Director"]
responses:
"200":
description: User updated successfully
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"400":
description: Bad Request - Invalid data, user cannot deactivate themselves, or insufficient permissions
"403":
description: Forbidden - User does not have USER_ADMIN role
"404":
description: User not found
patch:
tags:
- Users
operationId: partialUpdateUser
description: Partially update a user by ID. Only USER_ADMIN role can update users.
parameters:
- $ref: "#/components/parameters/simulatedError"
- in: path
name: id
description: User Id
required: true
schema:
type: integer
format: int32
minimum: 0
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
type: integer
email:
type: string
format: email
first_name:
type: string
nullable: true
last_name:
type: string
nullable: true
division:
type: integer
nullable: true
status:
type: string
enum: [active, inactive, locked]
roles:
type: array
items:
type: string
example:
status: "inactive"
responses:
"200":
description: User updated successfully
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"400":
description: Bad Request - Invalid data, user cannot deactivate themselves, or insufficient permissions
"403":
description: Forbidden - User does not have USER_ADMIN role
"404":
description: User not found
/login/:
servers:
- url: https://localhost:8080/auth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ def __init__(
"email": "power.user@email.com",
"sub": "00000000-0000-1111-a111-000000000028",
},
"read_only_user": {
"given_name": "Randy",
"family_name": "Read-Only",
"email": "randy.readonly@email.com",
"sub": "00000000-0000-1111-a111-000000000029",
},
}

def authenticate(self, auth_code):
Expand Down
1 change: 1 addition & 0 deletions backend/ops_api/ops/schemas/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class QueryParameters(Schema):
first_name: Optional[str] = fields.String()
last_name: Optional[str] = fields.String()
roles: Optional[list[str]] = custom_types.List(fields.String())
exclude_read_only: Optional[bool] = fields.Boolean(load_default=False)


class RoleSchema(Schema):
Expand Down
14 changes: 13 additions & 1 deletion backend/ops_api/ops/services/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,35 @@ def get_users(session: Session, **kwargs) -> list[User]:
Get all users that match the given criteria.

:param session: The database session.
:param exclude_read_only: Whether to exclude read-only users.
:param **kwargs: The criteria to filter the users by.
:return: The users that match the criteria.

Business Rules:
- Users with READ_ONLY role are excluded from the response

"""
stmt = select(User)

for key, value in kwargs.items():
if key == "roles":
stmt = stmt.where(User.roles.any(Role.name.in_(value)))
elif key == "exclude_read_only":
exclude_read_only = value
else:
stmt = stmt.where(cast(ColumnElement[bool], getattr(User, key)) == value)

stmt = stmt.order_by(User.id)

users = session.execute(stmt).scalars().all()

return list(users)
if exclude_read_only:
# Filter out users with READ_ONLY role
filtered_users = [user for user in users if not any(role.name == "READ_ONLY" for role in user.roles)]

return filtered_users
else:
return users


def create_user(session: Session, **kwargs) -> User:
Expand Down
7 changes: 7 additions & 0 deletions backend/ops_api/tests/ops/users/test_users_get.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ def test_get_all_users(auth_client, loaded_db, app_ctx):
assert response.json[0]["roles"] == get_expected_roles(expected_user)
assert response.json[0]["is_superuser"] is False

response_filtered = auth_client.get(url_for("api.users-group", exclude_read_only=True))
# Verify READ_ONLY users are filtered out
user_ids = [user["id"] for user in response_filtered.json]
read_only_user = loaded_db.get(User, 529) # Read-Only Randall
assert read_only_user is not None, "READ_ONLY user should exist in database"
assert read_only_user.id not in user_ids, "READ_ONLY user should be filtered from response"


def test_get_all_users_by_id(auth_client, loaded_db, app_ctx):
response = auth_client.get(url_for("api.users-group", id=500))
Expand Down
Loading