VC Flavors
VC (stands for "Viewer Context") is one of Ent Framework's core abstractions. As described in VC: Viewer Context and Principal article, it represents an "acting user". More precisely, it is actually an "acting principal", since it may not necessarily be a user: for e.g. background jobs, people often use other "owning" objects, like a company or a workspace, depending on the app's business logic.
VC Principal
Early in a request cycle, you create an instance of VC and then use it everywhere else in the code to load Ents:
Every VC instance has principal
property, a raw string that identifies, who's acting. Here are some common values for it:
"10042000123456789"
, i.e. some Ent's ID: used in absolute most of the cases (like user ID or company ID). It is more a convention rather than a rule though."omni"
: if you callvc.toOmniDangerous()
, the returned VC will have that value in itsprincipal
property. (The original VC remains immutable.) Omni VCs bypass all privacy rules."guest"
: such VC is created byvc.toGuest()
call or withcreateGuestPleaseDoNotUseCreationPointsMustBeLimited()
static method. It cannot load or update anything by default, unless explicitly allowed with e.g.AllowIf(new True())
privacy rule.
When you want to get s VC with particular principal in your code, you typically derive it from some existing VC by using the methods mentioned above. This enables keeping the knowledge about the derivation chain.
Flavors
In addition to vc.principal
property, it is often times convenient to store some auxiliary information in a VC. You can do it by adding flavors, instances of classes derived from VCFlavor
:
Typically, you store any arbitrary properties in your flavor instance and then derive a new VC by attaching the flavor:
You can then read the flavor back in your code (e.g. in privacy rule predicates) to make decisions:
Notice that vc.flavor(Class)
returns an instance of Class
flavor associated with the VC, or null
if there was no such flavor attached.
VC#toString() and Flavors
Each class derived from VCFlavor
may have a toDebugString()
method overridden. When you call vc.toString()
or vc.toAnnotation()
, all the flavors in the VC are enumerated, and the values returned by toDebugString()
are glued together, so the final result looks like:
This is extremely convenient: in your query logs, you likely save the result of vc.toAnnotation()
, so with e.g. VCEmail
, you immediately see, who is sending the queries.
Example: Attaching Flavors in a Next App
In VC: Viewer Context and Principal article, we provided the code for getServerVC()
helper function that can be used in a Next app to derive the request VC. Let's amend it to include VCEmail
helper flavor.
Flavors and Security
Flavors engine is not limited to auxiliary or debug purposes only: it may also be used on the app's privacy checking critical path.
E.g. a flavor can be used as a proof of identity. In all previous examples, we used vc.toOmniDangerous()
to load the very first EntUser in our request lifecycle, to avoid the "chicken and an egg" problem ("to load a user, you need a VC that can do it, and to derive that VC, you need an EntUser instance loaded"). Once the above is done, we wrote the user's ID to vc.principal
and then assumed that the VC is allowed to behave on behalf of that user, fully trusting the value in vc.principal
.
It is not the only way to create the initial acting VC though. Ask yourself: what kind of proof do we need to load an arbitrary EntUser? How does the backend do it naturally? The answer is that you must have some kind of a secret in hands, like the user's password salted hash, or the user's token stored in a cookie, or an OAuth2 token. If you put that "proof" in a favor, then you can use it in EntUser's privacy rules to unlock the loading without ever calling to vc.toOmniDangerous()
:
So instead of using toOmniDangerous()
in your initialization code, you may just attach the proof of identity flavor to a VC:
If you want even more or security, you may store a HMAC of the cookie token in VCIdentityProof
flavor instead of the token itself, and then use HMAC verification instead of ===
operator. In that case, even if the flavor payload is leaked, you'll face no harm.
Then, in privacy rules of the rest of your Ents, you delegate checking to the privacy rules of the parent EntUser (or of a parent Ent, considering that it delegates the checks to its owning EntUser). I.e. proceed with utilizing the standard privacy chain supported by Ent Framework.
A slight downside of this approach is that you'll always be having vc.principal
equal to "guest"
in this case, but it also makes sense: until "a guest" really "proves" that the VC has permissions to load an EntUser, it can't load the Ent.
Last updated
Was this helpful?