Skip to content

Enact non-commercial org restrictions #74

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 21, 2025
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
10 changes: 10 additions & 0 deletions src/Share/Web/Errors.hs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module Share.Web.Errors
MissingExpectedEntity (..),
Unimplemented (..),
BadRequest (..),
Forbidden (..),
InvalidParam (..),
NotAuthorized (..),
ErrorID (..),
Expand Down Expand Up @@ -314,6 +315,15 @@ instance ToServerError BadRequest where
instance Loggable BadRequest where
toLog (BadRequest msg) = withSeverity UserFault . textLog $ msg

data Forbidden = Forbidden Text
deriving (Eq, Show)

instance ToServerError Forbidden where
toServerError (Forbidden msg) = (ErrorID "forbidden", err403 {errBody = BL.fromStrict . Text.encodeUtf8 $ msg})

instance Loggable Forbidden where
toLog (Forbidden msg) = withSeverity UserFault . textLog $ msg

data InvalidParam = InvalidParam {paramName :: Text, param :: Text, parseError :: Text}

instance ToServerError InvalidParam where
Expand Down
11 changes: 10 additions & 1 deletion src/Share/Web/Share/Projects/Impl.hs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import Share.Postgres.Projects.Queries qualified as ProjectsQ
import Share.Postgres.Queries qualified as Q
import Share.Postgres.Releases.Queries qualified as RQ
import Share.Prelude
import Share.Project (Project (..))
import Share.Project (Project (..), ProjectVisibility (ProjectPrivate))
import Share.Release qualified as Release
import Share.User (User (..))
import Share.Utils.API ((:++) (..))
Expand All @@ -48,6 +48,8 @@ import Share.Web.Share.Branches.Impl (branchesServer, getProjectBranchReadmeEndp
import Share.Web.Share.Contributions.Impl (contributionsByProjectServer)
import Share.Web.Share.Diffs.Impl qualified as Diffs
import Share.Web.Share.Diffs.Types (ShareNamespaceDiffResponse (..), ShareNamespaceDiffStatus (..), ShareTermDiffResponse (..), ShareTypeDiffResponse (..))
import Share.Web.Share.Orgs.Queries qualified as OrgQ
import Share.Web.Share.Orgs.Types (Org (..))
import Share.Web.Share.Projects.API qualified as API
import Share.Web.Share.Projects.Types
import Share.Web.Share.Releases.Impl (getProjectReleaseReadmeEndpoint, releasesServer)
Expand Down Expand Up @@ -320,6 +322,13 @@ updateProjectEndpoint (AuthN.MaybeAuthedUserID callerUserId) userHandle projectS
addRequestTag "project-id" (IDs.toText projectId)
AuthZ.permissionGuard $ AuthZ.checkProjectUpdate callerUserId projectId
let UpdateProjectRequest {summary, tags, visibility} = req
when (visibility == Just ProjectPrivate) $ do
mayOrg <- PG.runTransaction (OrgQ.orgByUserHandle userHandle)
case mayOrg of
Just Org {isCommercial = False} -> do
respondError $ Forbidden "Please upgrade to a commercial org to enable private projects."
_ -> pure ()

success <- PG.runTransaction $ Q.updateProject projectId summary tags visibility
when (not success) $ respondError (EntityMissing (ErrorID "missing-project") "Project could not be found")
pure ()
Expand Down
12 changes: 9 additions & 3 deletions src/Share/Web/UCM/Projects/Impl.hs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import Share.Web.Authorization qualified as AuthZ
import Share.Web.Errors (InternalServerError (..), respondError)
import Share.Web.Errors qualified as Errors
import Share.Web.Share.Contributions.MergeDetection qualified as MergeDetection
import Share.Web.Share.Orgs.Queries qualified as OrgQ
import Share.Web.Share.Orgs.Types (Org (..))
import Share.Web.UCM.Sync.HashJWT qualified as HashJWT
import Share.Web.UCM.Sync.Impl qualified as SyncQ
import Unison.Server.Orphans ()
Expand Down Expand Up @@ -87,10 +89,14 @@ getProjectEndpoint (AuthN.MaybeAuthedUserID callerUserId) mayUcmProjectId mayUcm
createProjectEndpoint :: Maybe Session -> UCMProjects.CreateProjectRequest -> WebApp UCMProjects.CreateProjectResponse
createProjectEndpoint (AuthN.MaybeAuthedUserID callerUserId) (UCMProjects.CreateProjectRequest {projectName}) = toResponse do
ProjectShortHand {userHandle, projectSlug} <- lift $ parseParam @ProjectShortHand "projectName" projectName
User {user_id = targetUserId} <- pgT do
UserQ.userByHandle userHandle `orThrow` UCMProjects.CreateProjectResponseNotFound (UCMProjects.NotFound "User not found")
(User {user_id = targetUserId}, mayOrg) <- pgT do
user@(User {user_id}) <- UserQ.userByHandle userHandle `orThrow` UCMProjects.CreateProjectResponseNotFound (UCMProjects.NotFound "User not found")
mayOrg <- OrgQ.orgByUserId user_id
pure (user, mayOrg)
AuthZ.checkProjectCreate callerUserId targetUserId `ifUnauthorized` UCMProjects.CreateProjectResponseUnauthorized
let visibility = ProjectPrivate
let visibility = case mayOrg of
Nothing -> ProjectPrivate
Just (Org {isCommercial}) -> if isCommercial then ProjectPrivate else ProjectPublic
let summary = Nothing
let tags = mempty
projectId <- lift $ PGO.createProject targetUserId projectSlug summary tags visibility
Expand Down
24 changes: 24 additions & 0 deletions transcripts/share-apis/orgs/commercial-org-projects.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"body": [
{
"createdAt": "<TIMESTAMP>",
"isFaved": false,
"numFavs": 0,
"owner": {
"handle": "@acme",
"name": "ACME",
"type": "organization"
},
"slug": "proj",
"summary": null,
"tags": [],
"updatedAt": "<TIMESTAMP>",
"visibility": "private"
}
],
"status": [
{
"status_code": 200
}
]
}
6 changes: 6 additions & 0 deletions transcripts/share-apis/orgs/create-org-projects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Create new projects in both the `@acme` and `@noncom` organizations.

```ucm
scratch/main> push @acme/proj
scratch/main> push @noncom/proj
```
19 changes: 19 additions & 0 deletions transcripts/share-apis/orgs/create-org-projects.output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Create new projects in both the `@acme` and `@noncom` organizations.

``` ucm
scratch/main> push @acme/proj

Uploaded 1 entities.

I just created @acme/proj on http://localhost:5424

View it here: @acme/proj/main on http://localhost:5424

scratch/main> push @noncom/proj

Uploaded 1 entities.

I just created @noncom/proj on http://localhost:5424

View it here: @noncom/proj/main on http://localhost:5424
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"body": "Please upgrade to a commercial org to enable private projects.",
"status": [
{
"status_code": 403
}
]
}
24 changes: 24 additions & 0 deletions transcripts/share-apis/orgs/non-commercial-org-projects.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"body": [
{
"createdAt": "<TIMESTAMP>",
"isFaved": false,
"numFavs": 0,
"owner": {
"handle": "@noncom",
"name": "Noncom",
"type": "organization"
},
"slug": "proj",
"summary": null,
"tags": [],
"updatedAt": "<TIMESTAMP>",
"visibility": "public"
}
],
"status": [
{
"status_code": 200
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"body": {
"isCommercial": false,
"orgId": "ORG-<UUID>",
"user": {
"avatarUrl": "https://example.com/peace.png",
"handle": "noncom",
"name": "Noncom",
"userId": "U-<UUID>"
}
},
"status": [
{
"status_code": 200
}
]
}
27 changes: 25 additions & 2 deletions transcripts/share-apis/orgs/run.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,19 @@ fetch "$unauthorized_user" POST org-create-unauthorized '/orgs' '{
"avatarUrl": "https://example.com/anvil.png",
"owner": "unauthorized",
"email": "[email protected]",
"isCommercial": false
"isCommercial": true
}'

# Admin can create an org and assign any owner.
fetch "$admin_user" POST org-create-by-admin '/orgs' '{
fetch "$admin_user" POST org-create-by-admin-non-commercial '/orgs' '{
"name": "Noncom",
"handle": "noncom",
"avatarUrl": "https://example.com/peace.png",
"owner": "transcripts",
"isCommercial": false
}'

fetch "$admin_user" POST org-create-by-admin-commercial '/orgs' '{
"name": "ACME",
"handle": "acme",
"avatarUrl": "https://example.com/anvil.png",
Expand Down Expand Up @@ -74,3 +82,18 @@ fetch "$transcripts_user" DELETE org-remove-members '/orgs/acme/members' '{
}'

fetch "$transcripts_user" GET org-get-members-after-removing '/orgs/acme/members'

# Create projects in each org.
transcript_ucm transcript create-org-projects.md

# Get projects for each org
# Commercial projects should be private by default
fetch "$transcripts_user" GET commercial-org-projects '/users/acme/projects'

# Non-commercial projects must be public by default
fetch "$transcripts_user" GET non-commercial-org-projects '/users/noncom/projects'

# Updating a non-commercial org's project to private should fail
fetch "$transcripts_user" PATCH non-com-project-privatization '/users/noncom/projects/proj' '{
"visibility": "private"
}'
Loading