Closed fractlrao closed 2 years ago
(dataflow :Kernel/AppInit
{:Acme/User
{:UserName? "admin"}
:as :AdminUser}
;; ^ admin user may also be created here.
;; Create roles for admin, officer and user.
{:Kernel/Role
{:Name "admin-role"
:Assign :AdminUser}
:as :AdminRole}
{:Kernel/Role
{:Name "officer-role"}}
{:Kernel/Role
{:Name "user-role"}}
;; Admin can perform CRUD on all objects in the app model.
;; The `:Delete` action is locked, meaning it can only be overridden by the admin.
{:Kernel/Policy
{:Intercept "RBAC"
:Resource :Acme/*
:Spec {:actions [:Upsert :Lookup :Delete]
:role :AdminRole
:lock [:actions :Delete]}}}
;; From this point onwards, only admin can create/lookup/delete policies.
{:Kernel/Policy
{:Intercept "RBAC"
:Resource :Kernel/Policy
:Spec {:actions [:Upsert :Lookup :Delete]
:role :AdminRole
:lock :all}}})
A new officer is assigned upsert
and lookup
permissions on some app entities.
An officer also may do policy assignments. The policies on :Acme/Customer
will be
inherited by :Acme/Account
because of the contains
relationship.
(dataflow :Acme/CreateOfficer
{:Acme/User
{:Username :Acme/CreateOfficer.Username
...}
:as :Officer}
{:Kernel/Role
{:Name "officer-role"
:Assign :Officer}
:as :OfficerRole}
{:Kernel/Policy
{:Intercept "RBAC"
:Resource [:Acme/User :Acme/Customer]
:Spec {:actions [:Upsert :Lookup]
:role :OfficerRole}}}
;; an officer can create/lookup policies
{:Kernel/Policy
{:Intercept "RBAC"
:Resource :Kernel/Policy
:Spec {:actions [:Upsert :Lookup]
:role :OfficerRole}})
Every dataflow is executed in the context of a "role" - this has to be automatically inferred by the evaluator by looking at the auth-id
in the event-context and loading any attached roles.
The following dataflow tries to assign a user CRUD on :Acme/Customer
. The :Delete
action for this will succeed only if the current "role" is that of admin. This is because the path :action->:Delete
is locked by the admin-role. If UpdatePolicy
is executed by an officer, then the policy for :Delete
is ignored and will revert to the globally set policy.
(dataflow :Acme/UpdatePolicy
{:Kernel/Role
{:Name "user-role"
:Assign :Acme/UpdatePolicy.User}
:as :UserRole}
{:Kernel/Policy
{:Intercept "RBAC"
:Resource [:Acme/Customer]
:Spec {:actions [:Upsert :Lookup :Delete]
:role :UserRole}}})
I think this approach for controlling parts of a spec from being overridden at lower-levels should work for other use cases as well - like for controlling the view-config of entities.
The above approach looks good for the RBAC use case. Some minor comments:
:role
should be a vector and can be either a role or a user. e.g., a :Bid
created by a particular officer should be editable only by that officer and not by other officers with the same roleIt would help to assign roles to users, instead of assigning users to role. This can be done via :UserRole
(:Role
:contains
:UserRole
)
(dataflow :Acme/CreateOfficer
;; Get role
{:Kernel/Role
{:Name? "officer-role"}
:as :OfficerRole}
;; Create user
{:Acme/User
{:Username :Acme/CreateOfficer.Username
…
:Role :OfficerRole}
:as :Officer}
;; Assign role to officer
{:Acme/UserRole
{:User :Officer
…
:Role :OfficerRole}}
{:Kernel/Policy
{:Intercept "RBAC"
:Resource [:Acme/User :Acme/Customer]
:Spec {:actions [:Upsert :Lookup]
:role :OfficerRole}}}
;; an officer can create/lookup policies
{:Kernel/Policy
{:Intercept "RBAC"
:Resource :Kernel/Policy
:Spec {:actions [:Upsert :Lookup]
:role :OfficerRole}}})
:Supervisor
for a given department is allowed to do anything that an :Officer
is allowed to do.@fractlrao
(dataflow :Acme/CreateBid
;; Creating a new bid will fail if the user does not have officer-role,
;; this is globally-defined policy as described earler.
{:Acme/Bid
{:BidBy :Acme/CreateBid.UserId
...} :as :NewBid}
{:Acme/User {:Id? :Acme/CreateBid.UserId} :as :User}
;; Only the user who created the bid can update and query it.
{:Kernel/Policy
{:Intercept "RBAC"
:Resource :NewBid
:Spec {:actions [:Upsert :Lookup]
:role :User}}})
:Role
. I felt it is better to maintain assignment separately from the structure of user-defined entities. :Kernel/Policy
can be used to control UI generation for entities, events etc. An example is shown below:
(entity :Acme/Transaction
{:Debit {:ref :Acme/Account}
:Credit {:ref :Acme/Account}
:Amount :Kernel/Decimal
:CreatedBy {:ref :Acme/User}
:Date :Kernel/DateTime})
(dataflow :Kernel/AppInit
;; ...
{:Kernel/Policy
{:Intercept "views"
:Resource :Acme/Transaction
:Spec {:instance :Acme/ViewTransaction
:styles {:table ...}}}}
The same policy could be defined as part of the entity definition. The compiler will lift the spec into :Kernel/Policy
instance, which will be auto-evaluated at app-init time:
(entity :Acme/Transaction
{:Debit {:ref :Acme/Account}
...
:policies
{:views {:instance :Acme/ViewTransaction
:styles {:table ...}}}})
@fractlrao Given the facility to assign roles directly to user ids, we may no longer need the :Kernel/Role
entity.
The RBAC spec can support more granularity, as shown below:
{:Kernel/Policy
{:Intercept "RBAC"
:Resource :Acme/User
:Spec {:actions {:Upsert [:FirstName :LastName :Address]
:Lookup [:FirstName :LastName :Address :Email]}
:assign-to :SomeUser}}}
{:Kernel/Policy
{:Intercept "RBAC"
:Resource :Acme/Department
:Spec {:actions {:Upsert [[:Title :when [:= DeptNo 101]]]}
:assign-to :SomeUser}}}
We already have a rules compiler for RBAC, which can be reused for the second scenario.
A "Kernel/Rule
entity may be used to define reusable rules as part of the workflow. This will also lead to persisted policies taking less space on disk.
(dataflow :Acme/DefineSupervisorRole
{:Kernel/Rule
{:Name "dept-supervisor"
:Rule :Acme/DefineSupervisorRole.Rule ;; {:actions {:Upsert [[:Title :when [:= DeptNo 101]]]}}
}})
(dataflow :Acme/AssignSupervisor
{:Kernel/Rule {:Name? "dept-supervisor"} :as :Rule}
{:Acme/User {:Id? :Acme/AssignSupervisor.UserId} :as :User}
{:Kernel/Policy
{:Intercept "RBAC"
:Resource :Acme/Department
:Spec {:rule :Rule :assign-to :User}}})
We can still retain the ability to define rules directly in the policy instance.
@vmatv8, thank you - being able to support more granular policies and reusable rules is helpful.
We will still need the :Kernel/Role
(or equivalent) capability to be able to manage permissions for a group of users. There are 2 use cases:
:Employee
with :Type
set to supervisor
) and officers (entity :Employee
with :Type
set to officer
). All supervisors might be allowed to perform some admin actions, where as officers aren’t. It would help to have a single :Kernel/Policy
entry for the supervisor role, instead of one entry per role.:Officer
is allowed to modify only their profile/bids/…Would it be possible to enforce policy based on :Employee
type being supervisor
vs officer
for (1)?
@fractlrao I think we should model RBAC using the following constructs:
(entity :Kernel/Role
{:Name {:type :Kernel/String :unique true}})
(entity
:Kernel/RoleAssignment
{:Role {:ref :Kernel/Role.Name}
:Assignee :Kernel/Entity})
(entity :Kernel/Policy
{:Intercept :Kernel/Keyword
:Resource :Kernel/Path
:Spec :Kernel/Edn})
Application-level RBAC management can be done as,
(entity :Acme/User
{:Username :Kernel/String
:Password :Kernel/Password})
(dataflow :Kernel/AppInit
{:Kernel/Role
{:Name "supervisor"} :as :Supervisor})
{:Kernel/Role
{:Name "officer"} :as :Officer}
{:Kernel/Policy
{:Intercept "RBAC"
:Resource :* ;; all resources
:Spec {:actions [:Upsert :Delete :Lookup]
:role :Supervisor}}}
{:Kernel/Policy
{:Intercept "RBAC"
:Resource :Acme/Department
:Spec {:actions [:Lookup]
:role :Officer}}}
{:Kernel/Policy
{:Intercept "RBAC"
:Resource :Acme/Product
:Spec {:actions [:Lookup :Upsert]
:role :Officer}}}
(dataflow :Acme/CreateSupervisor
{:Acme/User
{:Username :Acme/CreateSupervisor.Username
...} :as :U}
{:Kernel/RoleAssignment
{:Role "supervisor"
:Assignee :U}})
(dataflow :Acme/CreateOfficer
{:Acme/User
{:Username :Acme/CreateOfficer.Username
...} :as :U}
{:Kernel/RoleAssignment
{:Role "officer"
:Assignee :U}})
The implementation will require a per-thread execution context which stores a reference to the user that initiated the request (obtained from the auth-info of the API request). Each CRUD operation will check the roles assigned to this user, merge any granular policies defined at different levels (contains
hierarchy) and apply the most restrictive rule to the operation. An example is shown below:
(entity :E1 { ... })
(entity :E2 { ... {:meta {:contains [:E1]}}})
(dataflow :Kernel/AppInit
{:Kernel/Role
{:Name "role-1"} :as :R1}
{:Kernel/Role
{:Name "role-2"} :as :R2}
;; Anyone with role :R1 has full CRUD on :E2 and :E1 (by way of contains).
{:Kernel/Policy
{:Intercept "RBAC"
:Resource :E2
:Spec {:actions [:Upsert :Delete :Lookup]
:role :R1}}}
;; Role :R2 will remove the :Delete permission for `:E1`
{:Kernel/Policy
{:Intercept "RBAC"
:Resource :E1
:Spec {:actions [:Upsert :Lookup]
:role :R2}}})
(dataflow :CreateUser
{:User {:Username :CreateUser,Name ...} :as :U}
{:Kernel/RoleAssignment
{:Role "role-1" :Assignee :U}}
[:match :User.DeptNo
101 {:Kernel/RoleAssignment {:Role "role-2" :Assignee :U}}])
Users belonging to department 101
has limited permissions on :E1
. The RBAC mechanism will find this minimal allowable permission by merging the two roles assigned to the user.
fractl now supports a
:contains
construct (in the context of UI). On the backend, for RBAC, this construct should imply cascading of permissions.For example,
Customer-a
containsAccount-a1
andAccount-a2
. Permissions a user/role has onCustomer-a
(create, read, etc) should cascade down toAccount-a1
andAccount-a2