Skip to content

Commit c79e604

Browse files
authored
doc: fix the-complete-guide space-user create rules (#415)
* doc: fix the-complete-guide space-user create rules * update
1 parent be49a30 commit c79e604

File tree

2 files changed

+91
-37
lines changed

2 files changed

+91
-37
lines changed

docs/reference/zmodel-language.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1518,7 +1518,21 @@ A field can also contain an arbitrary number of policy rules. The logic of combi
15181518

15191519
Please note the difference between model-level and field-level rules. Model-level access are by-default denied, while field-level access are by-default allowed.
15201520

1521-
### Pre-update vs. post-update
1521+
### "Create" rules
1522+
1523+
The "create" policy rules should be understood as: **if an entity were to be created, would it satisfy the rules**. Or, in other words, the rules are checked "post create".
1524+
1525+
An entity creating process works like the following:
1526+
1527+
1. Initiate a transaction and create the entity.
1528+
2. In the same transaction, try to read the created entity with the "create" rules as filter, and see if it succeeds.
1529+
3. If the read fails, the transaction is rolled back; otherwise it's committed.
1530+
1531+
The "post-create check" semantic allows the rules to access relations of the entity being created since they are only accessible after the create happens. For simple cases, ZenStack may apply optimizations to reject a create request without initiating a transaction, but generally speaking the "post-create check" semantic is the correct way to think about it.
1532+
1533+
We may introduce a "pre-create" policy type in the future.
1534+
1535+
### "Pre-update" vs." post-update" rules
15221536

15231537
When an access policy rule is applied to a mutate operation, the entities under operation have a "pre" and "post" state. For a "create" rule, its "pre" state is empty, so the rule implicitly refers to the "post" state. For a "delete" rule, its "post" state is empty, so the rule implicitly refers to the "pre" state.
15241538

docs/the-complete-guide/part1/4-access-policy/4.4-relations.md

Lines changed: 76 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@ Accessing *-to-one relation is straightforward. You simply reference the field d
1818

1919
```zmodel
2020
model List {
21-
id Int
22-
private Boolean
21+
id Int
22+
private Boolean
2323
}
2424
2525
model Todo {
26-
id Int
27-
list List @relation(...)
26+
id Int
27+
list List @relation(...)
2828
29-
// `list` references a to-one relation
30-
@@allow('update', !list.private)
29+
// `list` references a to-one relation
30+
@@allow('update', !list.private)
3131
}
3232
```
3333

@@ -79,58 +79,64 @@ model Space {
7979
}
8080
8181
model SpaceUser {
82-
...
82+
...
83+
84+
// require login
85+
@@deny('all', auth() == null)
8386
84-
// require login
85-
@@deny('all', auth() == null)
87+
// space owner can add any one
88+
@@allow('create', space.owner == auth())
89+
90+
// space admin can add anyone but not himself
91+
@@allow('create', auth() != user && space.members?[user == auth() && role == 'ADMIN'])
8692
87-
// space owner and admins have full access
88-
@@allow('all', space.owner == auth() || space.members?[user == auth() && role == 'ADMIN'])
93+
// space admin can update/delete
94+
@@allow('update,delete', space.members?[user == auth() && role == 'ADMIN'])
8995
90-
// user can read members of spaces that he's a member of
91-
@@allow('read', space.members?[user == auth()])
96+
// user can read members of spaces that he's a member of
97+
@@allow('read', space.members?[user == auth()])
9298
}
9399
94100
model User {
95-
...
101+
...
96102
97-
// everyone can sign up
98-
@@allow('create', true)
103+
// everyone can sign up
104+
@@allow('create', true)
99105
100-
// full access by oneself
101-
@@allow('all', auth() == this)
106+
// full access by oneself
107+
@@allow('all', auth() == this)
102108
103-
// can be read by users sharing any space
104-
@@allow('read', spaces?[space.members?[user == auth()]])
109+
// can be read by users sharing any space
110+
@@allow('read', spaces?[space.members?[user == auth()]])
105111
}
106112
107113
model List {
108-
...
114+
...
109115
110-
// require login
111-
@@deny('all', auth() == null)
116+
// require login
117+
@@deny('all', auth() == null)
112118
113-
// can be read by space members if not private
114-
@@allow('read', owner == auth() || (space.members?[user == auth()] && !private))
119+
// can be read by space members if not private
120+
@@allow('read', owner == auth() || (space.members?[user == auth()] && !private))
115121
116-
// when create, owner must be set to current user, and user must be in the space
117-
@@allow('create,update', owner == auth() && space.members?[user == auth()])
122+
// when create, owner must be set to current user, and user must be in the space
123+
@@allow('create,update', owner == auth() && space.members?[user == auth()])
118124
119-
// can be deleted by owner
120-
@@allow('delete', owner == auth())
125+
// can be deleted by owner
126+
@@allow('delete', owner == auth())
121127
}
122128
123129
model Todo {
124-
...
130+
...
125131
126-
// require login
127-
@@deny('all', auth() == null)
132+
// require login
133+
@@deny('all', auth() == null)
128134
129-
// owner has full access
130-
@@allow('all', list.owner == auth())
135+
// owner has full access
136+
@@allow('all', list.owner == auth())
131137
132-
// space members have full access if the parent List is not private
133-
@@allow('all', list.space.members?[user == auth()] && !list.private)
138+
// space members have full access if the parent List is not private
139+
@@allow('all', list.space.members?[user == auth()] && !list.private)
134140
}
135141
```
136142

@@ -163,3 +169,37 @@ db.list.findMany()
163169
```
164170

165171
We queried with user Joey and now can get the `List` created by Rachel because Joey is a member of the same space.
172+
173+
:::info A note about "create" rules
174+
175+
You might have noticed we have two "create" rules in the `SpaceUser` model.
176+
177+
```zmodel
178+
// space owner can add any one
179+
@@allow('create', space.owner == auth())
180+
181+
// space admin can add anyone but not himself
182+
@@allow('create', auth() != user
183+
&& space.members?[user == auth() && role == 'ADMIN'])
184+
```
185+
186+
One might be tempting to use one simple rule like the following to allow space admins to create new memberships.
187+
188+
```zmodel
189+
@@allow('create', space.members?[user == auth() && role == 'ADMIN'])
190+
```
191+
192+
However, it'll be problematic due to "create" rule's semantic - **if an entity were to be created, would it satisfy the rules**? Or, in more details, a create process works like the following:
193+
194+
1. Initiate a transaction and create the entity.
195+
2. In the same transaction, try to read the created entity with the "create" rules as filter, and see if it succeeds.
196+
3. If the read fails, the transaction is rolled back; otherwise it's committed.
197+
198+
The "post-create check" semantic allows the rules to access relations of the entity being created. For simple cases, ZenStack may apply optimizations to reject a create request without initiating a transaction, but generally speaking the "post-create check" semantic is the correct way to think about it. We may introduce a "pre-create" policy type in the future.
199+
200+
With these in mind, if we were to use the simple rule, a user can add himself to any space as "ADMIN", because after the create happens, the new membership would satisfy the rule. By splitting the rule into two, we can prevent this from happening:
201+
202+
- Space owner can add anyone (including himself) into the space.
203+
- Space admin can add anyone but not himself.
204+
205+
:::

0 commit comments

Comments
 (0)