WPB-21294: Add fields to apps: category, description, creator; WPB-21295: Add "get app" endpoint#4879
WPB-21294: Add fields to apps: category, description, creator; WPB-21295: Add "get app" endpoint#4879
Conversation
25f1188 to
b3b42dc
Compare
50be8c0 to
572b93f
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds new fields (category, description, author) to the apps feature and implements a GET endpoint to retrieve app details. The changes span from API definitions through database schema to integration tests, introducing password verification for app creation and restructuring the NewApp/GetApp types.
Key Changes:
- Restructured
NewAppto wrapGetAppwith password authentication, addingcategory,description, andauthorfields - Added
GET /teams/:tid/apps/:idendpoint for retrieving app details - Implemented password verification in the app creation flow using
verifyUserPasswordError
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| libs/wire-api/src/Wire/API/App.hs | Defines new Category enum and restructures types: NewApp now wraps GetApp with password; adds category, description, author fields |
| libs/wire-api/src/Wire/API/Routes/Public/Brig.hs | Adds GET endpoint definition for retrieving apps |
| services/brig/src/Brig/API/Public.hs | Wires up the new getApp handler to the API routes |
| libs/wire-subsystems/src/Wire/AppSubsystem.hs | Updates subsystem interface to include GetApp operation and adds AppSubsystemErrorNoAppUser error type |
| libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs | Implements getAppImpl to retrieve apps, adds ensureTeamMember helper, and integrates password verification into app creation |
| libs/wire-subsystems/src/Wire/AppStore.hs | Extends StoredApp with new fields and updates marshalling/unmarshalling instances |
| libs/wire-subsystems/src/Wire/AppStore/Postgres.hs | Updates SQL statements to include new columns in INSERT and SELECT operations |
| postgres-schema.sql | Adds category, description, author columns to apps table |
| libs/wire-subsystems/postgres-migrations/20251127115917-app-fields-category-description-author.sql | Migration to add new columns to existing apps table |
| integration/test/Test/Apps.hs | Adds tests for app retrieval and category validation |
| integration/test/API/Brig.hs | Updates test helper to include new fields and password in createApp requests, adds getApp helper |
| changelog.d/1-api-changes/add-get-app-endpoint | Documents new GET endpoint |
| changelog.d/1-api-changes/add-app-fields-category-description-author | Documents new app fields |
Comments suppressed due to low confidence (1)
libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs:152
- Security issue:
getAppImpldoes not verify that the requested app belongs to the team (tid) before returning its data. This means a team member can retrieve apps from other teams by providing anyuid, as long as they are a member of the team specified intid.
Compare with refreshAppCookieImpl (lines 176-180) which correctly validates:
app <- Store.getApp appId >>= note AppSubsystemErrorNoApp
note AppSubsystemErrorNoApp $ guard (app.teamId == tid)Add a similar check after line 151:
note AppSubsystemErrorNoApp $ guard (storedApp.teamId == tid) storedApp <- Store.getApp uid >>= note AppSubsystemErrorNoApp
u <- Store.getUser uid >>= note AppSubsystemErrorNoAppUser
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
integration/test/Test/Apps.hs
Outdated
| -- get app for the app created above succeeds | ||
| void $ getApp bob tid appId `bindResponse` \resp -> do | ||
| resp.status `shouldMatchInt` 200 | ||
| (resp.json %. "name") `shouldMatch` "chappie" | ||
| (resp.json %. "description") `shouldMatch` "some description of this app" | ||
| (resp.json %. "category") `shouldMatch` "ai" | ||
| (resp.json %. "author") `shouldMatch` "an author" |
There was a problem hiding this comment.
The integration test only verifies that bob (a team member) can retrieve the app. There's no test coverage for:
- Whether users outside the team are properly blocked from accessing the app
- Whether retrieving an app from a different team is properly blocked (related to the security issue in getAppImpl)
Consider adding test cases:
-- user outside team should not be able to get app
charlie <- randomUser OwnDomain def
bindResponse (getApp charlie tid appId) $ \resp -> do
resp.status `shouldMatchInt` 403
-- team member should not be able to get app from different team
(alice2, tid2, _) <- createTeam domain 1
bindResponse (getApp alice2 tid appId) $ \resp -> do
resp.status `shouldMatchInt` 404 -- or 403
libs/wire-api/src/Wire/API/App.hs
Outdated
| description :: Text, -- max 300 | ||
| author :: Text -- max 255, same as handle |
There was a problem hiding this comment.
The description and author fields lack proper length validation. The comments indicate max lengths (300 and 255 respectively), but no validation is enforced at the type level. Consider using Range types similar to other fields in the codebase:
description :: Range 1 300 Text,
author :: Range 1 255 TextThis pattern is used throughout the codebase (e.g., Name uses Range 1 128 Text, TextStatus uses Range 1 256 Text) to ensure validation happens at deserialization time rather than allowing invalid data to propagate through the system.
| description :: Text, -- max 300 | |
| author :: Text -- max 255, same as handle | |
| description :: Range 1 300 Text, -- max 300 | |
| author :: Range 1 255 Text -- max 255, same as handle |
libs/wire-api/src/Wire/API/App.hs
Outdated
| instance A.ToJSON Category where toJSON = schemaToJSON | ||
|
|
||
| categoryFromText :: Text -> Either Text Category | ||
| categoryFromText = either (Left . TS.pack) Right . A.eitherDecode . A.encode . A.String |
There was a problem hiding this comment.
The categoryFromText function has an inefficient and potentially fragile implementation. It converts Text → JSON → Either, which is unnecessarily complex.
Consider implementing a direct pattern match:
categoryFromText :: Text -> Either Text Category
categoryFromText = \case
"security" -> Right Security
"collaboration" -> Right Collaboration
"productivity" -> Right Productivity
"automation" -> Right Automation
"files" -> Right Files
"ai" -> Right AI
"developer" -> Right Developer
"support" -> Right Support
"finance" -> Right Finance
"hr" -> Right HR
"integration" -> Right Integration
"compliance" -> Right Compliance
"other" -> Right Other
t -> Left $ "Invalid category: " <> tThis is more efficient and clearer in intent.
| categoryFromText = either (Left . TS.pack) Right . A.eitherDecode . A.encode . A.String | |
| categoryFromText = \case | |
| "security" -> Right Security | |
| "collaboration" -> Right Collaboration | |
| "productivity" -> Right Productivity | |
| "automation" -> Right Automation | |
| "files" -> Right Files | |
| "ai" -> Right AI | |
| "developer" -> Right Developer | |
| "support" -> Right Support | |
| "finance" -> Right Finance | |
| "hr" -> Right HR | |
| "integration" -> Right Integration | |
| "compliance" -> Right Compliance | |
| "other" -> Right Other | |
| t -> Left $ "Invalid category: " <> t |
libs/wire-api/src/Wire/API/App.hs
Outdated
| categoryToText a = case A.toJSON a of | ||
| A.String t -> t | ||
| _ -> | ||
| error $ | ||
| "wire-api:Wire/API/App.hs: The impossible happened, Category should serialize to JSON string: " | ||
| <> show a |
There was a problem hiding this comment.
[nitpick] Using error in pure code (line 74) is not ideal for error handling. This can cause runtime crashes that are difficult to debug. While this is labeled as "impossible", it's better to use a safer pattern.
Consider using fromJust with a comment explaining why it's safe, or better yet, use a total function:
categoryToText :: Category -> Text
categoryToText a = case A.toJSON a of
A.String t -> t
v -> error $ "BUG: Category should serialize to JSON string, got: " <> show vAlternatively, match on the Category constructor directly to ensure totality.
| categoryToText a = case A.toJSON a of | |
| A.String t -> t | |
| _ -> | |
| error $ | |
| "wire-api:Wire/API/App.hs: The impossible happened, Category should serialize to JSON string: " | |
| <> show a | |
| categoryToText Security = "security" | |
| categoryToText Collaboration = "collaboration" | |
| categoryToText Productivity = "productivity" | |
| categoryToText Automation = "automation" | |
| categoryToText Files = "files" | |
| categoryToText AI = "ai" | |
| categoryToText Developer = "developer" | |
| categoryToText Support = "support" | |
| categoryToText Finance = "finance" | |
| categoryToText HR = "hr" | |
| categoryToText Integration = "integration" | |
| categoryToText Compliance = "compliance" | |
| categoryToText Other = "other" |
| Sem r Apps.GetApp | ||
| getAppImpl lusr tid uid = do | ||
| void $ ensureTeamMember lusr tid | ||
| storedApp <- Store.getApp uid >>= note AppSubsystemErrorNoApp |
There was a problem hiding this comment.
getAppImpl authorizes the caller only by checking ensureTeamMember lusr tid, but then fetches the app by uid without verifying that the app belongs to the requested team (storedApp.teamId == tid). This allows any member of team tid to read app details for any app/user ID across all teams. Enforce team scoping by verifying the fetched app’s teamId matches tid, or by fetching via (user_id, team_id):
storedApp <- Store.getAppByTeam uid tid >>= note AppSubsystemErrorNoApp
when (storedApp.teamId /= tid) (throw AppSubsystemErrorNoPerm)| storedApp <- Store.getApp uid >>= note AppSubsystemErrorNoApp | |
| storedApp <- Store.getApp uid >>= note AppSubsystemErrorNoApp | |
| when (storedApp.teamId /= tid) (throw AppSubsystemErrorNoPerm) |
| runStatement uid $ | ||
| dimapPG | ||
| [maybeStatement| select (user_id :: uuid), (team_id :: uuid), (metadata :: json) | ||
| [maybeStatement| select (user_id :: uuid), (team_id :: uuid), (metadata :: json), (category :: text), (description :: text), (author :: text) |
There was a problem hiding this comment.
getAppImpl SQL selects from apps only by user_id (where user_id = ($1 :: uuid)), ignoring the captured tid. A caller who is a member of any team can provide a uid for an app in another team and receive its details. Scope the query to the team by including team_id = $2 and adjust marshalling accordingly:
select (user_id :: uuid), (team_id :: uuid), (metadata :: json), (category :: text), (description :: text), (author :: text)
from apps where user_id = ($1 :: uuid) and team_id = ($2 :: uuid)4fdc5f3 to
0b6565e
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| data AppSubsystemError | ||
| = AppSubsystemErrorNoPerm | ||
| | AppSubsystemErrorNoUser -- The user having created the app not found | ||
| | AppSubsystemErrorNoAppUser -- The user used to "enact" the app not found |
There was a problem hiding this comment.
[nitpick] The comment "The user used to 'enact' the app not found" is unclear. Consider rewording to be more precise, such as: "The user record associated with the app not found (data inconsistency)"
| | AppSubsystemErrorNoAppUser -- The user used to "enact" the app not found | |
| | AppSubsystemErrorNoAppUser -- The user record associated with the app not found (data inconsistency) |
There was a problem hiding this comment.
Renamed the error to AppSubsystemErrorAppUserNotFound 3d495ff, kept the comment (the improved comment is not clearer IMO).
| note AppSubsystemErrorNoPerm $ guard (T.hasPermission mem T.ManageApps) | ||
| app <- Store.getApp appId >>= note AppSubsystemErrorNoApp | ||
| app <- Store.getApp appId tid >>= note AppSubsystemErrorNoApp | ||
| note AppSubsystemErrorNoApp $ guard (app.teamId == tid) |
There was a problem hiding this comment.
This check is redundant because Store.getApp appId tid on line 180 already filters by tid, so app.teamId will always equal tid. This line can be removed.
| note AppSubsystemErrorNoApp $ guard (app.teamId == tid) |
| getAppImpl lusr tid uid = do | ||
| void $ ensureTeamMember lusr tid | ||
| storedApp <- Store.getApp uid tid >>= note AppSubsystemErrorNoApp | ||
| when (storedApp.teamId /= tid) $ throw AppSubsystemErrorNoPerm |
There was a problem hiding this comment.
This check is redundant because Store.getApp uid tid on line 151 already filters by tid, so storedApp.teamId will always equal tid. This line can be removed.
| when (storedApp.teamId /= tid) $ throw AppSubsystemErrorNoPerm |
| ADD COLUMN category text, | ||
| ADD COLUMN description text, | ||
| ADD COLUMN author text; |
There was a problem hiding this comment.
The migration adds columns category, description, and author as nullable (text), but the code expects non-null values. For existing apps in the database, these columns will be NULL after migration, which will cause failures when trying to retrieve them using getApp.
The migration should either:
- Add default values:
ADD COLUMN category text DEFAULT 'other', etc., or - Update existing rows to set default values after adding the columns, or
- Change the code to handle NULL values (make the fields
Maybe Textor similar in the Haskell types)
Option 1 is recommended for consistency with the API schema which requires these fields.
| ADD COLUMN category text, | |
| ADD COLUMN description text, | |
| ADD COLUMN author text; | |
| ADD COLUMN category text DEFAULT 'other' NOT NULL, | |
| ADD COLUMN description text DEFAULT '' NOT NULL, | |
| ADD COLUMN author text DEFAULT '' NOT NULL; |
There was a problem hiding this comment.
2988ba2: added the above for category and description.
The author field type will change and there will possibly be a team_id field, both UUID, change incoming.
- abstract out `ensureTeamMember` (to be used later) - rename alice to owner for clarity
- rename old NewApp to GetApp - add NewApp data type with app (GetApp) and password fields
- zero-length description and author
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Add defaults for category and description.
defGetApp is unused
It is not a branding string and it shouldn't be provided within the API call to create the app. Backend already knows the caller and fills it in automatically.
2988ba2 to
af15476
Compare
|
All the Copilot comments should be addressed (though feel free to run it again). The originating Jira ticket had a minor change: originally it had the |
fisx
left a comment
There was a problem hiding this comment.
LGTM + 2 nit-picks.
I think we're still missing golden and roundtrip tests for apps in wire-api (not this PR's problem, arguably).
| accentId = Nothing, | ||
| meta = object [] | ||
| meta = object [], | ||
| category = "other", |
There was a problem hiding this comment.
are these case insensitive? the ticket doesn't say so, and capitalizes the O here.
There was a problem hiding this comment.
So I double checked this, lower case is desirable: https://wearezeta.atlassian.net/browse/WPB-21294?focusedCommentId=148085
libs/wire-api/src/Wire/API/App.hs
Outdated
| | Other | ||
| deriving (Eq, Ord, Show, Read, Generic) | ||
|
|
||
| instance A.FromJSON Category where parseJSON = schemaParseJSON |
There was a problem hiding this comment.
this works, but we usually add this deriving clause to the data type:
deriving (FromJSON, ToJSON, S.ToSchema) via (Schema Category)| deriving (Eq, Ord, Show) | ||
|
|
||
| instance PostgresMarshall StoredApp (UUID, UUID, Value) where | ||
| instance PostgresMarshall StoredApp (UUID, UUID, Value, Text, Text, UUID) where |
There was a problem hiding this comment.
off topic for this PR, but shouldn't this belong into Wire.AppStore.Postgres?
There was a problem hiding this comment.
I agree with this, but the instances for StoredApp would then turn out to be orphans in the Postgres module, so an {-# OPTIONS_GHC -Wno-orphans #-} would be required there. I'm assuming that is why they are defined here in the current module (@pcapriotti).
There was a problem hiding this comment.
then maybe you can add a comment explaining this? but as i said, it's not really your job in this PR :)
|
@fisx Ready for another round! |
…WPB-21295: Add "get app" endpoint (#4879)" This reverts commit 9e83d2d.
Doing the two together as then the fields set can then be tested by getting the app.
Questions/notes:
should create and get double-dip on the NewApp data type, or is a second one needed?NewAppwrapsGetAppnowrestrict text length for description and author, perhaps on type level? (do we have anything already available for this?)where shouldmake postgres-schemabe added? If I understand PR guidelines then into the checklist below, so did that.Checklist
changelog.d