Permissions
Who may do what is decided by your server, in rules it declares by name pattern: who joins a room and what they may send there, who calls whom, who runs a group call, who hosts a live stream and who keeps its chat. A rule is a function of yours, with your database at hand. WordPlus Cloud enforces its answer, and a page can neither skip a rule nor change what it answered.
How a rule runs
- A page asks your server. Joining a room, placing or answering a call, joining a group call or opening a stream, the page asks the routes of the package on your server (
/api/realtimeby default). The page asks for every room it joins at the same moment in one request. - Your server finds the user the way your app does, with
user( request )(the session in Django,auth()in Laravel, your own in Next.js), and runs the rule for that user and name.userisnull(None) for a visitor who is not signed in. - The answer travels signed. Your server signs it into a token with your secret, and WordPlus Cloud lets the page do what the token says, nothing more.
- It runs again on renewal. A token lasts 6 hours by default (
tokenTtl, at most a day, and 6 hours at most for a call). The page renews it before it expires, and your rule runs again then. To take access away at once, see below.
- A name matches the pattern with the most characters of its own (
order:{id}before{kind}:{id}). A{part}matches anything but:and.. A name no pattern matches is refused withno_rule. - In Node a rule may be async. In PHP and Python it is a plain function, which queries your database the usual way. In Laravel,
routes/channels.phpdeclares the rooms, as for any broadcasting driver.
Rooms
- Node
- PHP
- Python
// The chat of two people: both of them in it, and a supervisor who may read along but send nothing.
realtime.room( 'chat:{a}:{b}', { presence: true, history: 50, emit: [ 'message', 'typing', 'read' ], webhook: [ 'message' ] },
async ( user, { a, b } ) => {
if ( [ a, b ].includes( String( user ) ) ) return true;
return ( await isSupervisor( user ) ) ? { readOnly: true } : false;
} );
// A product page: anyone, signed in or not, and a live count of who is looking.
realtime.room( 'product:{id}', { public: true, count: true } );
$realtime->room( 'chat:{a}:{b}', function ( $user, array $p ) {
if ( in_array( (string) $user, [ $p['a'], $p['b'] ], true ) ) {
return true;
}
return is_supervisor( $user ) ? [ 'readOnly' => true ] : false;
}, [ 'presence' => true, 'history' => 50, 'emit' => [ 'message', 'typing', 'read' ], 'webhook' => [ 'message' ] ] );
$realtime->room( 'product:{id}', null, [ 'public' => true, 'count' => true ] );
def may_read_chat(user, p):
if str(user) in (p["a"], p["b"]):
return True
return {"readOnly": True} if is_supervisor(user) else False
realtime.room("chat:{a}:{b}", may_read_chat, presence=True, history=50, emit=["message", "typing", "read"], webhook=["message"])
realtime.room("product:{id}", public=True, count=True)
The answer of a room rule:
| Answer | |
|---|---|
false or null | not in it |
true | in it, with what the room allows |
{ info } | in it, the others in a presence room seeing info of them (your profile() by default) |
{ emit: [ … ] } | in it, sending only these of the events the room allows |
{ readOnly: true } | in it, sending nothing |
What the room itself allows, the same for everyone in it (rooms):
| Option | Who it concerns |
|---|---|
public | anyone may join, signed in or not, and it needs no rule. Its grant names no user, so a page a CDN caches may carry it. It is not sealed, so keep it for what anyone may see |
emit | which events the pages may send to it, by name, or '*'. None by default: a room only your server sends to |
webhook | which of those your server gets too, to check and keep |
presence | whether its members are listed, each with their info, to the others in it |
count | a live count of who is in it, with no list |
history | how many of its last events a page that joins or reconnects gets |
seal | whether what it carries is sealed with its key, on by default for a room that is not public |
- The sender of an event is the user your server vouched for (
meta.from). A page cannot send as someone else, nor send an event the rule did not let it send. - What an event says comes from the page: it reaches the others without passing through your server. Check what matters in your webhook handler before you keep it.
- A user hidden from presence:
hidden( user )returningtruekeeps them out of who is online, for a user who chose not to be seen.
Calls between two people
- Node
- PHP
- Python
// A customer may call an agent, and an agent anyone. Add Person offers the other agents.
realtime.calls( 'support:{a}:{b}', async ( user, withUser, { a, b } ) => {
// The two ids in the name, as text: user 42 is "42" there, user_2abc itself.
const pair = [ a, b ];
return pair.includes( String( user ) ) && pair.includes( String( withUser ) ) && ( isStaff( user ) || isStaff( withUser ) );
}, { people: async ( user ) => ( isStaff( user ) ? await staffIds() : [] ) } );
$realtime->calls( 'support:{a}:{b}', function ( $user, $with, array $p ) {
$pair = [ $p['a'], $p['b'] ]; // the two ids in the name, as text
return in_array( (string) $user, $pair, true ) && in_array( (string) $with, $pair, true ) && ( is_staff( $user ) || is_staff( $with ) );
}, [ 'people' => fn ( $user ) => is_staff( $user ) ? staff_ids() : [] ] );
def may_call_support(user, with_user, p):
pair = [p["a"], p["b"]] # the two ids in the name, as text
return str(user) in pair and str(with_user) in pair and (is_staff(user) or is_staff(with_user))
realtime.calls("support:{a}:{b}", may_call_support, people=lambda user, call: staff_ids() if is_staff(user) else [])
- Asked twice: when
usercallswithUser, and again when the one called answers, with them asuser. Each side has to be let in. The fifth argument,side.role, says which side asks (callerorcallee), so a rule may let someone answer calls they may not place. - The answer:
false,true, or{ info }, what the other side sees ofuser(yourprofile()by default). - Adding people: either side may ring someone into the call, which goes on as a group call. Your rule decides that as any call: the one adding calling the person about the call name, then the person taking it. A customer your rule lets call only an agent can add nobody else.
people( user, call )is whom the call screen offers to add. Each one still has to pass the rule.- Who is in a call is proved by your server, with keys made from your local key: WordPlus Cloud carries the proofs and cannot make one.
Group calls
- Node
- PHP
- Python
// Office hours: the teacher hosts the room, the students watch and listen, an observer is listed to nobody.
realtime.groupCalls( 'class:{id}', async ( user, { id } ) => {
if ( await isTeacherOf( id, user ) ) return { admin: true }; // speaks, and hosts: mutes, lets speak, removes
if ( await isObserverOf( id, user ) ) return { publish: false, hidden: true };
return ( await isStudentOf( id, user ) ) ? { publish: false } : false;
}, { encrypt: true } );
$realtime->groupCalls( 'class:{id}', function ( $user, array $p ) {
if ( is_teacher_of( $p['id'], $user ) ) {
return [ 'admin' => true ]; // speaks, and hosts: mutes, lets speak, removes
}
return is_student_of( $p['id'], $user ) ? [ 'publish' => false ] : false;
}, [ 'encrypt' => true ] );
def may_join_class(user, p):
if is_teacher_of(p["id"], user):
return {"admin": True} # speaks, and hosts: mutes, lets speak, removes
return {"publish": False} if is_student_of(p["id"], user) else False
realtime.group_calls("class:{id}", may_join_class, encrypt=True)
| Answer | Default | |
|---|---|---|
info | your profile() | what the others see of them |
publish | true | sends their camera, microphone and screen |
subscribe | true | gets those of the others |
admin | false | hosts the call: the call screen gives them mute, let speak or make a listener, and remove on the tiles of the others |
hidden | false | is not listed to the others |
A host's controls are those of Better Messages, on the tile of each of the others: Mute Microphone, Block Broadcast (to listening), Allow Broadcast (to speaking) and Kick User. Each goes to your calls route, which asks this rule again for whoever clicked and acts only for a host, through realtime.call( name ).mute(), .permit() or .remove(): nothing to add on your server. A microphone muted stays the person's own to turn on again. Your own buttons, or a page without the call screen, call those from routes of yours (from your server), or group.moderate( act, user ) in the page, which goes the same way. Your server changes publish and subscribe while the call runs, with realtime.call( name ).permit( user, { publish } ): a listener made a speaker, or a speaker back to listening (speakers and listeners).
encrypt: true encrypts the media of these group calls end to end, with keys your server makes: the media servers forward what they cannot read. The group calls that come from a call between two people always are.
Live streams
- Node
- PHP
- Python
// The show of a shop: its owner hosts, staff keep the chat, customers write, and visitors watch and read.
realtime.streams( 'show:{shop}', async ( user, { shop } ) => {
if ( user === null ) return 'viewer';
if ( await isOwnerOf( shop, user ) ) return { role: 'host', src: [ 'camera', 'microphone', 'screen' ], quality: '1080p' };
if ( await isStaffOf( shop, user ) ) return { role: 'viewer', mod: true };
return ( await isCustomerOf( shop, user ) ) ? 'viewer' : { role: 'viewer', chat: 'read' };
}, { reactions: true, people: 'mods', chat: { offline: 'closed', links: false } } );
$realtime->streams( 'show:{shop}', function ( $user, array $p ) {
if ( null === $user ) {
return 'viewer';
}
if ( is_owner_of( $p['shop'], $user ) ) {
return [ 'role' => 'host', 'src' => [ 'camera', 'microphone', 'screen' ], 'quality' => '1080p' ];
}
return is_staff_of( $p['shop'], $user ) ? [ 'role' => 'viewer', 'mod' => true ] : 'viewer';
}, [ 'reactions' => true, 'people' => 'mods', 'chat' => [ 'offline' => 'closed', 'links' => false ] ] );
def show_role(user, p):
if user is None:
return "viewer"
if is_owner_of(p["shop"], user):
return {"role": "host", "src": ["camera", "microphone", "screen"], "quality": "1080p"}
return {"role": "viewer", "mod": True} if is_staff_of(p["shop"], user) else "viewer"
realtime.streams("show:{shop}", show_role, reactions=True, people="mods", chat={"offline": "closed", "links": False})
The answer for each person:
| Answer | |
|---|---|
false | not let in |
'host' or 'viewer' | the role alone |
role | host goes live, ends the stream and keeps the chat. viewer watches |
info | { name, avatar } as everyone sees them, your profile() by default. avatar is an https:// address |
chat | write or read: whether they may write in the chat |
mod | true for someone who keeps the chat without hosting |
src | what a host may send: camera, microphone, screen |
quality | the best picture a host sends: 720p or 1080p |
A visitor who is not signed in (user null) may only be a viewer: a rule that gives them more is refused with no_user.
The settings of the stream, each a value or a function of the parts of its name:
| Setting | |
|---|---|
stage | whether viewers come on stage as guests, a hand raised and the host inviting them (guests on stage): yes unless false |
reactions | whether it takes reactions |
people | who may see who is watching: mods or all |
chat.offline | open or closed: the chat while the stream is not live |
chat.links | false takes links only from those who keep the chat |
chat.fresh | true empties the chat each time the stream goes live |
What the host and the mods do in the stream (live streams):
chat.delete( id ), chat.clear() | one message, or all of them |
chat.pin( id ), chat.unpin() | the message shown above the chat |
chat.timeout( user, seconds ) | no writing for a minute to a day. They go on watching |
chat.slow( seconds ) | one message per that many seconds each, 0 to turn it off |
kick( user ) | out now. They may come back |
ban( user ) | out, and kept out |
Taking access away
| What it does | |
|---|---|
| Change your data | your rule answers no at the next renewal of the token, within its life (6 hours by default, tokenTtl) |
realtime.in( room ).kick( user ) | out of the room now, and the token they had is refused. A reload asks your rule again, so to keep them out, your rule says no for them, from your own data such as a ban list: a room has no ban of its own. With key generations kept, the room key moves on too (encryption) |
realtime.user( id ).disconnect() | out of every room, call and live stream at once, on every connection, and the tokens signed for them before refused: their pages ask your routes again, so your rules decide whether they come back (sending) |
rotateRoom( room ), rotateUser( user ) | the key of the room, or of the user, moves on without a kick: what comes from now on needs the new one, which only those your rules let in get |
hasKeys( user ) returning false | no keys at all for that user: nothing sealed opens for them |
realtime.call( name ).remove( user ), .end() | out of a group call now, or everyone, and the tokens from before do not take them back in (calls) |
kick(), ban(), timeout() in a live stream | out of the stream, with ban kept out, or kept from writing in its chat for a while (live streams) |
A shorter tokenTtl makes a change in your data count sooner, at the price of more requests to your routes.
What WordPlus Cloud enforces, and what stays yours
| WordPlus Cloud enforces | Yours to check |
|---|---|
| who is in each room, call, group call and stream, as your token says | what an event from a page says, before your server keeps it from the webhook |
| which events a page may send, and that it sends as its own user | your own routes and pages, with the auth your app has |
| what a person may do in a group call and a stream: send, see, keep the chat, host | what your app does with the records it keeps |
| the limits of your plan and the rate of events from a page |
WordPlus Cloud never holds a key: the rooms, the directory of your users and what calls say of them are sealed with keys your server makes from its local key (encryption).