Skip to content

WPB-21294: Add fields to apps: category, description, creator; WPB-21295: Add "get app" endpoint#4879

Merged
eyeinsky merged 25 commits intodevelopfrom
ml/WPB-21294--app-reg
Dec 9, 2025
Merged

WPB-21294: Add fields to apps: category, description, creator; WPB-21295: Add "get app" endpoint#4879
eyeinsky merged 25 commits intodevelopfrom
ml/WPB-21294--app-reg

Conversation

@eyeinsky
Copy link
Contributor

@eyeinsky eyeinsky commented Nov 28, 2025

  • WPB-21294: Add new fields to apps (category, description, creator)
  • WPB-21295: Add "get app" endpoint

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? NewApp wraps GetApp now
  • restrict text length for description and author, perhaps on type level? (do we have anything already available for this?)
  • where should make postgres-schema be added? If I understand PR guidelines then into the checklist below, so did that.

Checklist

  • Add a new entry in an appropriate subdirectory of changelog.d
  • Read and follow the PR guidelines

@zebot zebot added the ok-to-test Approved for running tests in CI, overrides not-ok-to-test if both labels exist label Nov 28, 2025
@eyeinsky eyeinsky force-pushed the ml/WPB-21294--app-reg branch from 25f1188 to b3b42dc Compare November 28, 2025 08:17
@eyeinsky eyeinsky changed the title Ml/wpb 21294 app reg WPB-21294: Add fields to apps: category, description, author; add "get app" endpoint Nov 28, 2025
@eyeinsky eyeinsky changed the title WPB-21294: Add fields to apps: category, description, author; add "get app" endpoint WPB-21294: Add fields to apps: category, description, author; WPB-21295: Add "get app" endpoint Nov 28, 2025
@eyeinsky eyeinsky marked this pull request as ready for review November 28, 2025 08:25
@eyeinsky eyeinsky requested review from a team as code owners November 28, 2025 08:25
@eyeinsky eyeinsky force-pushed the ml/WPB-21294--app-reg branch 2 times, most recently from 50be8c0 to 572b93f Compare December 1, 2025 16:54
@battermann battermann requested a review from Copilot December 2, 2025 08:31
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 NewApp to wrap GetApp with password authentication, adding category, description, and author fields
  • Added GET /teams/:tid/apps/:id endpoint 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: getAppImpl does 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 any uid, as long as they are a member of the team specified in tid.

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.

Comment on lines 66 to 74
-- 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"
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The integration test only verifies that bob (a team member) can retrieve the app. There's no test coverage for:

  1. Whether users outside the team are properly blocked from accessing the app
  2. 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

Copilot uses AI. Check for mistakes.
Comment on lines 42 to 43
description :: Text, -- max 300
author :: Text -- max 255, same as handle
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Text

This 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.

Suggested change
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

Copilot uses AI. Check for mistakes.
instance A.ToJSON Category where toJSON = schemaToJSON

categoryFromText :: Text -> Either Text Category
categoryFromText = either (Left . TS.pack) Right . A.eitherDecode . A.encode . A.String
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: " <> t

This is more efficient and clearer in intent.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines 71 to 76
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
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 v

Alternatively, match on the Category constructor directly to ensure totality.

Suggested change
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"

Copilot uses AI. Check for mistakes.
Sem r Apps.GetApp
getAppImpl lusr tid uid = do
void $ ensureTeamMember lusr tid
storedApp <- Store.getApp uid >>= note AppSubsystemErrorNoApp
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Suggested change
storedApp <- Store.getApp uid >>= note AppSubsystemErrorNoApp
storedApp <- Store.getApp uid >>= note AppSubsystemErrorNoApp
when (storedApp.teamId /= tid) (throw AppSubsystemErrorNoPerm)

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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)"

Suggested change
| AppSubsystemErrorNoAppUser -- The user used to "enact" the app not found
| AppSubsystemErrorNoAppUser -- The user record associated with the app not found (data inconsistency)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
note AppSubsystemErrorNoApp $ guard (app.teamId == tid)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup! 6d7b62d

getAppImpl lusr tid uid = do
void $ ensureTeamMember lusr tid
storedApp <- Store.getApp uid tid >>= note AppSubsystemErrorNoApp
when (storedApp.teamId /= tid) $ throw AppSubsystemErrorNoPerm
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
when (storedApp.teamId /= tid) $ throw AppSubsystemErrorNoPerm

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 2 to 4
ADD COLUMN category text,
ADD COLUMN description text,
ADD COLUMN author text;
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Add default values: ADD COLUMN category text DEFAULT 'other', etc., or
  2. Update existing rows to set default values after adding the columns, or
  3. Change the code to handle NULL values (make the fields Maybe Text or similar in the Haskell types)

Option 1 is recommended for consistency with the API schema which requires these fields.

Suggested change
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;

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

eyeinsky and others added 11 commits December 5, 2025 12:13
- 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.
@eyeinsky eyeinsky force-pushed the ml/WPB-21294--app-reg branch from 2988ba2 to af15476 Compare December 5, 2025 11:58
@eyeinsky
Copy link
Contributor Author

eyeinsky commented Dec 5, 2025

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 author field in the POST payload, but now we call it creator and fill it in in the backend, We currently just save it in the database and don't return it in the "get app" API endpoint. (The "get app" endpoint is also preliminary, it's there to help writing tests. A more fitting shape for the frontend will be specified in a future ticket.)

@eyeinsky eyeinsky changed the title WPB-21294: Add fields to apps: category, description, author; WPB-21295: Add "get app" endpoint WPB-21294: Add fields to apps: category, description, creator; WPB-21295: Add "get app" endpoint Dec 5, 2025
Copy link
Contributor

@fisx fisx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these case insensitive? the ticket doesn't say so, and capitalizes the O here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I double checked this, lower case is desirable: https://wearezeta.atlassian.net/browse/WPB-21294?focusedCommentId=148085

| Other
deriving (Eq, Ord, Show, Read, Generic)

instance A.FromJSON Category where parseJSON = schemaParseJSON
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this works, but we usually add this deriving clause to the data type:

  deriving (FromJSON, ToJSON, S.ToSchema) via (Schema Category)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 , a284adc

deriving (Eq, Ord, Show)

instance PostgresMarshall StoredApp (UUID, UUID, Value) where
instance PostgresMarshall StoredApp (UUID, UUID, Value, Text, Text, UUID) where
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

off topic for this PR, but shouldn't this belong into Wire.AppStore.Postgres?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then maybe you can add a comment explaining this? but as i said, it's not really your job in this PR :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did that too ;) edca279

@eyeinsky
Copy link
Contributor Author

eyeinsky commented Dec 8, 2025

@fisx Ready for another round!

Copy link
Contributor

@fisx fisx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perfect! :-)

@eyeinsky eyeinsky merged commit 9e83d2d into develop Dec 9, 2025
10 checks passed
@eyeinsky eyeinsky deleted the ml/WPB-21294--app-reg branch December 9, 2025 10:47
akshaymankar added a commit that referenced this pull request Dec 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ok-to-test Approved for running tests in CI, overrides not-ok-to-test if both labels exist

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants