Skip to content

Commit 2aef1c4

Browse files
authored
Merge pull request #74 from unisoncomputing/cp/org-restrictions
Enact non-commercial org restrictions
2 parents f2a4d1e + a549d6b commit 2aef1c4

11 files changed

+152
-6
lines changed

src/Share/Web/Errors.hs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module Share.Web.Errors
1818
MissingExpectedEntity (..),
1919
Unimplemented (..),
2020
BadRequest (..),
21+
Forbidden (..),
2122
InvalidParam (..),
2223
NotAuthorized (..),
2324
ErrorID (..),
@@ -314,6 +315,15 @@ instance ToServerError BadRequest where
314315
instance Loggable BadRequest where
315316
toLog (BadRequest msg) = withSeverity UserFault . textLog $ msg
316317

318+
data Forbidden = Forbidden Text
319+
deriving (Eq, Show)
320+
321+
instance ToServerError Forbidden where
322+
toServerError (Forbidden msg) = (ErrorID "forbidden", err403 {errBody = BL.fromStrict . Text.encodeUtf8 $ msg})
323+
324+
instance Loggable Forbidden where
325+
toLog (Forbidden msg) = withSeverity UserFault . textLog $ msg
326+
317327
data InvalidParam = InvalidParam {paramName :: Text, param :: Text, parseError :: Text}
318328

319329
instance ToServerError InvalidParam where

src/Share/Web/Share/Projects/Impl.hs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import Share.Postgres.Projects.Queries qualified as ProjectsQ
3131
import Share.Postgres.Queries qualified as Q
3232
import Share.Postgres.Releases.Queries qualified as RQ
3333
import Share.Prelude
34-
import Share.Project (Project (..))
34+
import Share.Project (Project (..), ProjectVisibility (ProjectPrivate))
3535
import Share.Release qualified as Release
3636
import Share.User (User (..))
3737
import Share.Utils.API ((:++) (..))
@@ -48,6 +48,8 @@ import Share.Web.Share.Branches.Impl (branchesServer, getProjectBranchReadmeEndp
4848
import Share.Web.Share.Contributions.Impl (contributionsByProjectServer)
4949
import Share.Web.Share.Diffs.Impl qualified as Diffs
5050
import Share.Web.Share.Diffs.Types (ShareNamespaceDiffResponse (..), ShareNamespaceDiffStatus (..), ShareTermDiffResponse (..), ShareTypeDiffResponse (..))
51+
import Share.Web.Share.Orgs.Queries qualified as OrgQ
52+
import Share.Web.Share.Orgs.Types (Org (..))
5153
import Share.Web.Share.Projects.API qualified as API
5254
import Share.Web.Share.Projects.Types
5355
import Share.Web.Share.Releases.Impl (getProjectReleaseReadmeEndpoint, releasesServer)
@@ -320,6 +322,13 @@ updateProjectEndpoint (AuthN.MaybeAuthedUserID callerUserId) userHandle projectS
320322
addRequestTag "project-id" (IDs.toText projectId)
321323
AuthZ.permissionGuard $ AuthZ.checkProjectUpdate callerUserId projectId
322324
let UpdateProjectRequest {summary, tags, visibility} = req
325+
when (visibility == Just ProjectPrivate) $ do
326+
mayOrg <- PG.runTransaction (OrgQ.orgByUserHandle userHandle)
327+
case mayOrg of
328+
Just Org {isCommercial = False} -> do
329+
respondError $ Forbidden "Please upgrade to a commercial org to enable private projects."
330+
_ -> pure ()
331+
323332
success <- PG.runTransaction $ Q.updateProject projectId summary tags visibility
324333
when (not success) $ respondError (EntityMissing (ErrorID "missing-project") "Project could not be found")
325334
pure ()

src/Share/Web/UCM/Projects/Impl.hs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import Share.Web.Authorization qualified as AuthZ
3838
import Share.Web.Errors (InternalServerError (..), respondError)
3939
import Share.Web.Errors qualified as Errors
4040
import Share.Web.Share.Contributions.MergeDetection qualified as MergeDetection
41+
import Share.Web.Share.Orgs.Queries qualified as OrgQ
42+
import Share.Web.Share.Orgs.Types (Org (..))
4143
import Share.Web.UCM.Sync.HashJWT qualified as HashJWT
4244
import Share.Web.UCM.Sync.Impl qualified as SyncQ
4345
import Unison.Server.Orphans ()
@@ -87,10 +89,14 @@ getProjectEndpoint (AuthN.MaybeAuthedUserID callerUserId) mayUcmProjectId mayUcm
8789
createProjectEndpoint :: Maybe Session -> UCMProjects.CreateProjectRequest -> WebApp UCMProjects.CreateProjectResponse
8890
createProjectEndpoint (AuthN.MaybeAuthedUserID callerUserId) (UCMProjects.CreateProjectRequest {projectName}) = toResponse do
8991
ProjectShortHand {userHandle, projectSlug} <- lift $ parseParam @ProjectShortHand "projectName" projectName
90-
User {user_id = targetUserId} <- pgT do
91-
UserQ.userByHandle userHandle `orThrow` UCMProjects.CreateProjectResponseNotFound (UCMProjects.NotFound "User not found")
92+
(User {user_id = targetUserId}, mayOrg) <- pgT do
93+
user@(User {user_id}) <- UserQ.userByHandle userHandle `orThrow` UCMProjects.CreateProjectResponseNotFound (UCMProjects.NotFound "User not found")
94+
mayOrg <- OrgQ.orgByUserId user_id
95+
pure (user, mayOrg)
9296
AuthZ.checkProjectCreate callerUserId targetUserId `ifUnauthorized` UCMProjects.CreateProjectResponseUnauthorized
93-
let visibility = ProjectPrivate
97+
let visibility = case mayOrg of
98+
Nothing -> ProjectPrivate
99+
Just (Org {isCommercial}) -> if isCommercial then ProjectPrivate else ProjectPublic
94100
let summary = Nothing
95101
let tags = mempty
96102
projectId <- lift $ PGO.createProject targetUserId projectSlug summary tags visibility
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"body": [
3+
{
4+
"createdAt": "<TIMESTAMP>",
5+
"isFaved": false,
6+
"numFavs": 0,
7+
"owner": {
8+
"handle": "@acme",
9+
"name": "ACME",
10+
"type": "organization"
11+
},
12+
"slug": "proj",
13+
"summary": null,
14+
"tags": [],
15+
"updatedAt": "<TIMESTAMP>",
16+
"visibility": "private"
17+
}
18+
],
19+
"status": [
20+
{
21+
"status_code": 200
22+
}
23+
]
24+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Create new projects in both the `@acme` and `@noncom` organizations.
2+
3+
```ucm
4+
scratch/main> push @acme/proj
5+
scratch/main> push @noncom/proj
6+
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Create new projects in both the `@acme` and `@noncom` organizations.
2+
3+
``` ucm
4+
scratch/main> push @acme/proj
5+
6+
Uploaded 1 entities.
7+
8+
I just created @acme/proj on http://localhost:5424
9+
10+
View it here: @acme/proj/main on http://localhost:5424
11+
12+
scratch/main> push @noncom/proj
13+
14+
Uploaded 1 entities.
15+
16+
I just created @noncom/proj on http://localhost:5424
17+
18+
View it here: @noncom/proj/main on http://localhost:5424
19+
```
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"body": "Please upgrade to a commercial org to enable private projects.",
3+
"status": [
4+
{
5+
"status_code": 403
6+
}
7+
]
8+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"body": [
3+
{
4+
"createdAt": "<TIMESTAMP>",
5+
"isFaved": false,
6+
"numFavs": 0,
7+
"owner": {
8+
"handle": "@noncom",
9+
"name": "Noncom",
10+
"type": "organization"
11+
},
12+
"slug": "proj",
13+
"summary": null,
14+
"tags": [],
15+
"updatedAt": "<TIMESTAMP>",
16+
"visibility": "public"
17+
}
18+
],
19+
"status": [
20+
{
21+
"status_code": 200
22+
}
23+
]
24+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"body": {
3+
"isCommercial": false,
4+
"orgId": "ORG-<UUID>",
5+
"user": {
6+
"avatarUrl": "https://example.com/peace.png",
7+
"handle": "noncom",
8+
"name": "Noncom",
9+
"userId": "U-<UUID>"
10+
}
11+
},
12+
"status": [
13+
{
14+
"status_code": 200
15+
}
16+
]
17+
}

transcripts/share-apis/orgs/run.zsh

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,19 @@ fetch "$unauthorized_user" POST org-create-unauthorized '/orgs' '{
1616
"avatarUrl": "https://example.com/anvil.png",
1717
"owner": "unauthorized",
1818
"email": "[email protected]",
19-
"isCommercial": false
19+
"isCommercial": true
2020
}'
2121

2222
# Admin can create an org and assign any owner.
23-
fetch "$admin_user" POST org-create-by-admin '/orgs' '{
23+
fetch "$admin_user" POST org-create-by-admin-non-commercial '/orgs' '{
24+
"name": "Noncom",
25+
"handle": "noncom",
26+
"avatarUrl": "https://example.com/peace.png",
27+
"owner": "transcripts",
28+
"isCommercial": false
29+
}'
30+
31+
fetch "$admin_user" POST org-create-by-admin-commercial '/orgs' '{
2432
"name": "ACME",
2533
"handle": "acme",
2634
"avatarUrl": "https://example.com/anvil.png",
@@ -74,3 +82,18 @@ fetch "$transcripts_user" DELETE org-remove-members '/orgs/acme/members' '{
7482
}'
7583

7684
fetch "$transcripts_user" GET org-get-members-after-removing '/orgs/acme/members'
85+
86+
# Create projects in each org.
87+
transcript_ucm transcript create-org-projects.md
88+
89+
# Get projects for each org
90+
# Commercial projects should be private by default
91+
fetch "$transcripts_user" GET commercial-org-projects '/users/acme/projects'
92+
93+
# Non-commercial projects must be public by default
94+
fetch "$transcripts_user" GET non-commercial-org-projects '/users/noncom/projects'
95+
96+
# Updating a non-commercial org's project to private should fail
97+
fetch "$transcripts_user" PATCH non-com-project-privatization '/users/noncom/projects/proj' '{
98+
"visibility": "private"
99+
}'

0 commit comments

Comments
 (0)