Security
Data access policies
New in 0.34
next
tag.Orbital supports defining data access policies against types, which are evaluated when running queries. This feature allows you to define data policies once and enforce them consistently across your organization, regardless of where data is served from.
Overview
Policies are a first-class citizen within Orbital and are defined in your Orbital project along with your types, models, and services. They can conditionally control the data that’s returned (including filtering and obfuscating values) and determine which services and operations can be invoked.
Getting Started - Configuring your JWT
Policies are often used as a form of authorization based on the user requesting the data. To use policies in this manner, you must first have configured Authentication with Orbital.
Exposing User Information
Policies access user information from the claims presented on your authentication token. Since each authentication provider is different, you need to define an Orbital type that maps data from your token into data types you can use in your policies.
Orbital’s UI will guide you through this process and create the corresponding types for you:
Alternatively, you can manually define a model that extends from com.orbitalhq.auth.AuthClaims
. You don’t need to map everything from the token, only the attributes you care about.
For example, consider the following JWT:
{
"sub": "661667e1-78ca-43e5-97dd-d1a39ee37f43",
"email_verified": true,
"allowed-origins": ["*"],
"iss": "http://xxxx",
"typ": "Bearer",
"preferred_username": "jimmy",
"realm_access": {
"roles": [
"offline_access",
"default-roles-vyne",
"uma_authorization",
"Admin"
]
},
"email": "marty@vyne.co"
}
You can cherry-pick the useful fields and define a model. Note that the model name isn’t important, but it must inherit from com.orbitalhq.auth.AuthClaims
:
type Sub inherits String
type Role inherits String
model UserInfo inherits com.orbitalhq.auth.AuthClaims {
sub : Sub
realm_access : {
roles: Role[]
}
}
Verifying Your Credentials
To verify that your credentials have been mapped correctly, the UI will show the details of the current user in the policy designer:
Defining Policies
Policies are defined in Taxi files within an Orbital project. Here’s a simple example:
model Film {
title : Title inherits String
yearReleased : YearReleased inherits Int
}
// Define a policy named `ExcludeYearReleased` which will operate against the `Film` type.
policy ExcludeYearReleased against Film {
read { // define the scope - either read or write
Film as { // return the Film type
... except { yearReleased } // but exclude some fields
}
}
}
The above policy will be invoked whenever data is returned from an operation that returns Film
data.
Inputs to policies
Policies can request data as an input, which can be referred to within the policy. For example, this policy requests information about the user making the request:
policy FilterSalary against Employee (userInfo : UserInfo) -> {
...
}
Here, UserInfo
is the type configured against the JWT token as described earlier.
You can request any data in the policy, including data loaded from additional services, as described below.
Suppressing data based on user properties
You can create policies that behave differently based on user properties.
For example, this policy suppresses the salary
field from
policy FilterSalary against Employee (userInfo : UserInfo) -> {
read {
when {
userInfo.groups.contains('ADMIN') -> Employee
else -> Employee as { ... except { salary } }
}
}
}
Policies may not alter structure
Data policies can be used to obfuscate and filter out properties, but they cannot drop fields entirely, as doing so would cause parsing exceptions in downstream systems, and mean that responses violate their own contracts.
When data is filtered (for example, using a spread operator), the resulting object contains nulls.
Policies and expressions
When models include an expression, the expression is evaluated using the input values after the relevant security policies have been applied.
This approach ensures that sensitive information is not inadvertently exposed (for example, by adjusting a policy-protected value using an expression such as: EmployeeSalary + 1
).
As a result, the input values used in the expression may differ from the original values returned by an operation.
If a policy causes an input value to become null
, the expression will also evaluate to null
.
Throwing Errors from Policies
Policies can also throw errors to completely deny access to certain data based on conditions. For example:
policy OnlyManagers against EmployeeInfo (userInfo : UserInfo) -> {
read {
when {
userInfo.groups.contains('Manager') -> EmployeeInfo
else -> throw((NotAuthorizedError) { message: 'Not Authorized' })
}
}
}
Obfuscating data
You can use policies to obfuscate the Policies can be applied to nested types as well. For example, to partially obfuscate titles for non-admin users:
policy FilterFilmTitle against Title (userInfo : UserInfo) -> {
read {
when {
userInfo.groups.contains('ADMIN') -> Title
else -> concat(left(Title, 3), "***")
}
}
}
Using External Data in Policy Decisions
Policies can load additional data from external services to make decisions. For example, to filter films based on whether the user has accepted terms and conditions:
model UserConsent {
acceptedTermsAndConditions : AcceptedTermsAndConditions inherits Boolean
}
service UserService {
operation getConsent(UserId): UserConsent
}
policy AllAccessFilms against Film (userInfo : UserInfo, acceptedTerms: AcceptedTermsAndConditions) -> {
read {
when {
acceptedTerms == false -> null
else -> Film
}
}
}
Projection and Policy Impact
When a policy modifies a field that is used in a projection, the result is affected accordingly. For instance, if a policy suppresses the title
field for non-admin users:
policy FilterYearReleased against Film (userInfo : UserInfo) -> {
read {
when {
userInfo.groups.contains('ADMIN') -> Film
else -> Film as { ... except { title } }
}
}
}
Querying and projecting the title
field for a non-admin user would result in:
find { Film } as {
name : Title
}
For non-admin users, this would return name: null
.
Understanding When Policies Are Applied
Policies defined against types or models are applied to data returned from a service before it’s made available in Orbital (either for other service calls or to return to a caller). A policy is applied to the type and all its subtypes.
Using Errors in Policies
Errors can be thrown in policies to prevent access entirely, returning an error code to the user. For example:
policy OnlyManagers against EmployeeInfo (userInfo : UserInfo) -> {
read {
when {
userInfo.groups.contains('Manager') -> EmployeeInfo
else -> throw((NotAuthorizedError) { message: 'Not Authorized' })
}
}
}
Read more about throwing errors.
Streaming queries
Data policies can also be applied to streaming queries, which are running continuously in the background.
Instead of executing with the requesting users permissions (as request / response queries do), persistent streaming queries execute with a system account - the Executor user.
Configuring the Executor user
The Executor User is a standard system account defined by your Identity Provider (IDP). Assign roles as you would with any other user, as discussed in our docs on Authentication.
Orbital authenticates this role using the OAuth2 Client Credentials flow with a client-id
and client-secret
. Pass these
to the Orbital instance at startup with the following configuration settings:
Parameter | Description |
---|---|
vyne.security.openIdp.executorRoleClientId | The user to authenticate with. Note - this can be different from the standard authentication client configured with clientId |
vyne.security.openIdp.executorRoleClientSecret | The Client Secret to authenticate with |
vyne.security.openIdp.issuerUrl | The URL of the IDP. Note that Orbital’s server will connect to this URL, so ensure it’s accessible from the server |
Example configuration:
--vyne.security.openIdp.executorRoleClientId=TheSchuylerSisters
--vyne.security.openIdp.executorRoleClientSecret=AngelicaElizaAndPeggy
Troubleshooting
IssuerUrl connectivity issues
The issuerUrl
setting is used by both standard Authentication (to authenticate users logging in to Orbital), as
well as by Orbital to fetch user credentials for the Executor user.
- User authentication will perform a browser-side redirect to the IssuerUrl - so the URL must be accessible from your browser.
- Executor User authentication performs requests from Orbital’s server - so the URL must be accessible from your server.
Normally, this is not a problem. However, if you’re running everything locally (eg., using Docker or Docker Compose) you may need to use host.docker.internal
as the issuerUrl
DNS name,
(docker docs) or set Docker to use the Host Network.
This is generally not an issue in production (and the above workarounds are not suitable for production), as the network is normally more well defined.
Observers vs Executors
Persistent Streams are always executed under the permissions of the Executor User. However, these streams can also be observed by other users, through published http or websocket endpoint.
In this scenario, policies are applied twice:
- First, the stream is executed using the permissions of the Executor user
- Then, when being observed, the results of the stream are then re-evaluated using the permissions of the user observing the stream
As a result, the observed output may differ from the actual data being emitted by the stream.