-
Notifications
You must be signed in to change notification settings - Fork 112
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
go/registry: Add per-role runtime admission policy support
- Loading branch information
Showing
5 changed files
with
443 additions
and
82 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.