Skip to content

Commit

Permalink
go/registry: Add per-role runtime admission policy support
Browse files Browse the repository at this point in the history
  • Loading branch information
kostko committed Aug 29, 2023
1 parent a51b2b2 commit 38c1023
Show file tree
Hide file tree
Showing 5 changed files with 443 additions and 82 deletions.
237 changes: 237 additions & 0 deletions go/consensus/cometbft/apps/registry/admission.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package registry

import (
beacon "github.com/oasisprotocol/oasis-core/go/beacon/api"
"github.com/oasisprotocol/oasis-core/go/common/node"
"github.com/oasisprotocol/oasis-core/go/consensus/cometbft/api"
registryState "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/apps/registry/state"
registry "github.com/oasisprotocol/oasis-core/go/registry/api"
)

// policyFn is an admission policy verification function.
type policyFn func(
*api.Context,
*registryState.MutableState,
*node.Node,
*registry.Runtime,
beacon.EpochTime,
) error

// admissionPolicyFns are the global admission policy verification functions.
var admissionPolicyFns = []policyFn{
// Whitelist.
verifyRuntimeWhitelistAdmissionPolicy,
// Per-role.
verifyRuntimePerRoleAdmissionPolicy,
}

// rolePolicyFn is a per-role admission policy verification function.
type rolePolicyFn func(
*api.Context,
*registryState.MutableState,
*node.Node,
*registry.Runtime,
beacon.EpochTime,
node.RolesMask,
*registry.PerRoleAdmissionPolicy,
) error

// perRoleAdmissionPolicyFns are the per-role admission policy verification functions.
var perRoleAdmissionPolicyFns = []rolePolicyFn{
// Whitelist.
verifyRuntimePerRoleWhitelistAdmissionPolicy,
}

func verifyRuntimeAdmissionPolicy(
ctx *api.Context,
state *registryState.MutableState,
newNode *node.Node,
rt *registry.Runtime,
epoch beacon.EpochTime,
) error {
// Process all admission policy functions.
for _, policyFn := range admissionPolicyFns {
if err := policyFn(ctx, state, newNode, rt, epoch); err != nil {
return err
}
}

return nil
}

func verifyRuntimeWhitelistAdmissionPolicy(
ctx *api.Context,
state *registryState.MutableState,
newNode *node.Node,
rt *registry.Runtime,
epoch beacon.EpochTime,
) error {
if rt.AdmissionPolicy.EntityWhitelist == nil {
return nil // No whitelist policy specified.
}

wcfg, entIsWhitelisted := rt.AdmissionPolicy.EntityWhitelist.Entities[newNode.EntityID]
if !entIsWhitelisted {
ctx.Logger().Debug("RegisterNode: node's entity not in a runtime's whitelist",
"entity_id", newNode.EntityID,
"runtime_id", rt.ID,
"node_id", newNode.ID,
)
return registry.ErrForbidden
}
if len(wcfg.MaxNodes) == 0 {
return nil // Any amount of nodes allowed.
}

// Map is present and non-empty, check per-role restrictions
// on the maximum number of nodes per entity.

// Iterate over all valid roles (each entry in the map can
// only have a single role).
for _, role := range node.Roles() {
if !newNode.HasRoles(role) {
// Skip unset roles.
continue
}

maxNodes, exists := wcfg.MaxNodes[role]
if !exists {
// No such role found in whitelist.
ctx.Logger().Debug("RegisterNode: runtime's whitelist does not allow nodes with given role",
"role", role.String(),
"runtime_id", rt.ID,
"node_id", newNode.ID,
)
return registry.ErrForbidden
}
if maxNodes == 0 {
// No nodes of this type are allowed.
ctx.Logger().Debug("RegisterNode: runtime's whitelist does not allow nodes with given role",
"role", role.String(),
"runtime_id", rt.ID,
)
return registry.ErrForbidden
}

if err := verifyNodeCountWithRoleForRuntime(ctx, state, newNode, rt, epoch, role, int(maxNodes)); err != nil {
return err
}
}

return nil
}

// verifyNodeCountWithRoleForRuntime verifies that the number of nodes registered by the specified
// entity for the specified runtime with the specified role is at most the specified maximum.
func verifyNodeCountWithRoleForRuntime(
ctx *api.Context,
state *registryState.MutableState,
newNode *node.Node,
rt *registry.Runtime,
epoch beacon.EpochTime,
role node.RolesMask,
maxNodes int,
) error {
// Count existing nodes owned by entity.
nodes, err := state.GetEntityNodes(ctx, newNode.EntityID)
if err != nil {
ctx.Logger().Error("RegisterNode: failed to query entity nodes",
"err", err,
"entity_id", newNode.EntityID,
)
return err
}

var curNodes int
for _, n := range nodes {
if n.ID.Equal(newNode.ID) || n.IsExpired(uint64(epoch)) || !n.HasRuntime(rt.ID) {
// Skip existing node when re-registering. Also skip
// expired nodes and nodes that haven't registered
// for the same runtime.
continue
}

if n.HasRoles(role) {
curNodes++
}

// The check is inside the for loop, so we can stop as
// soon as possible once we're over the limit.
if curNodes+1 > maxNodes {
// Too many nodes with given role already registered.
ctx.Logger().Debug("RegisterNode: too many nodes with given role already registered for runtime",
"role", role.String(),
"runtime_id", rt.ID,
"node_id", newNode.ID,
"num_registered_nodes", curNodes,
)
return registry.ErrForbidden
}
}

return nil
}

func verifyRuntimePerRoleAdmissionPolicy(
ctx *api.Context,
state *registryState.MutableState,
newNode *node.Node,
rt *registry.Runtime,
epoch beacon.EpochTime,
) error {
if len(rt.AdmissionPolicy.PerRole) == 0 {
return nil // No per-role policy specified.
}

// Iterate over all valid roles (each entry in the map can only have a single role).
for _, role := range node.Roles() {
if !newNode.HasRoles(role) {
// Skip unset roles.
continue
}

rolePolicy, ok := rt.AdmissionPolicy.PerRole[role]
if !ok {
// Skip roles for which a per-role policy is not set.
continue
}

for _, policyFn := range perRoleAdmissionPolicyFns {
if err := policyFn(ctx, state, newNode, rt, epoch, role, &rolePolicy); err != nil {
return err
}
}
}

return nil
}

func verifyRuntimePerRoleWhitelistAdmissionPolicy(
ctx *api.Context,
state *registryState.MutableState,
newNode *node.Node,
rt *registry.Runtime,
epoch beacon.EpochTime,
role node.RolesMask,
rolePolicy *registry.PerRoleAdmissionPolicy,
) error {
if rolePolicy.EntityWhitelist == nil {
return nil // No per-role whitelist specified.
}

wcfg, entIsWhitelisted := rolePolicy.EntityWhitelist.Entities[newNode.EntityID]
if !entIsWhitelisted {
ctx.Logger().Debug("RegisterNode: node's entity not in a runtime's per-role whitelist",
"entity_id", newNode.EntityID,
"runtime_id", rt.ID,
"node_id", newNode.ID,
"node_roles", newNode.Roles,
)
return registry.ErrForbidden
}
if wcfg.MaxNodes == 0 {
return nil // Any amount of nodes allowed.
}

return verifyNodeCountWithRoleForRuntime(ctx, state, newNode, rt, epoch, role, int(wcfg.MaxNodes))
}
85 changes: 3 additions & 82 deletions go/consensus/cometbft/apps/registry/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,89 +235,10 @@ func (app *registryApplication) registerNode( // nolint: gocyclo
}
}

// Check runtime's whitelist.
// Verify admission policies for all node's runtimes are satisfied.
for _, rt := range paidRuntimes {
if rt.AdmissionPolicy.EntityWhitelist == nil {
continue
}
wcfg, entIsWhitelisted := rt.AdmissionPolicy.EntityWhitelist.Entities[newNode.EntityID]
if !entIsWhitelisted {
ctx.Logger().Debug("RegisterNode: node's entity not in a runtime's whitelist",
"entity_id", newNode.EntityID,
"runtime_id", rt.ID,
"node_id", newNode.ID,
)
return registry.ErrForbidden
}
if len(wcfg.MaxNodes) == 0 {
continue
}

// Map is present and non-empty, check per-role restrictions
// on the maximum number of nodes per entity.

// Iterate over all valid roles (each entry in the map can
// only have a single role).
for _, role := range node.Roles() {
if !newNode.HasRoles(role) {
// Skip unset roles.
continue
}

maxNodes, exists := wcfg.MaxNodes[role]
if !exists {
// No such role found in whitelist.
ctx.Logger().Debug("RegisterNode: runtime's whitelist does not allow nodes with given role",
"role", role.String(),
"runtime_id", rt.ID,
"node_id", newNode.ID,
)
return registry.ErrForbidden
}
if maxNodes == 0 {
// No nodes of this type are allowed.
ctx.Logger().Debug("RegisterNode: runtime's whitelist does not allow nodes with given role",
"role", role.String(),
"runtime_id", rt.ID,
)
return registry.ErrForbidden
}

// Count existing nodes owned by entity.
nodes, grr := state.GetEntityNodes(ctx, newNode.EntityID)
if grr != nil {
ctx.Logger().Error("RegisterNode: failed to query entity nodes",
"err", grr,
"entity_id", newNode.EntityID,
)
return grr
}
var curNodes uint16
for _, n := range nodes {
if n.ID.Equal(newNode.ID) || n.IsExpired(uint64(epoch)) || !n.HasRuntime(rt.ID) {
// Skip existing node when re-registering. Also skip
// expired nodes and nodes that haven't registered
// for the same runtime.
continue
}

if n.HasRoles(role) {
curNodes++
}

// The check is inside the for loop, so we can stop as
// soon as possible once we're over the limit.
if curNodes+1 > maxNodes {
// Too many nodes with given role already registered.
ctx.Logger().Error("RegisterNode: too many nodes with given role already registered for runtime",
"role", role.String(),
"runtime_id", rt.ID,
"node_id", newNode.ID,
"num_registered_nodes", curNodes,
)
return registry.ErrForbidden
}
}
if err = verifyRuntimeAdmissionPolicy(ctx, state, newNode, rt, epoch); err != nil {
return err
}
}

Expand Down
Loading

0 comments on commit 38c1023

Please sign in to comment.