зеркало из https://github.com/microsoft/CCF.git
Set role definition sample (#6237)
This commit is contained in:
Родитель
992f4bad6b
Коммит
3ba5e0133b
|
@ -1124,19 +1124,6 @@ if(BUILD_TESTS)
|
|||
add_picobench(merkle_bench SRCS src/node/test/merkle_bench.cpp)
|
||||
add_picobench(hash_bench SRCS src/ds/test/hash_bench.cpp)
|
||||
|
||||
set(CONSTITUTION_ARGS
|
||||
--constitution
|
||||
${CCF_DIR}/samples/constitutions/default/actions.js
|
||||
--constitution
|
||||
${CCF_DIR}/samples/constitutions/test/test_actions.js
|
||||
--constitution
|
||||
${CCF_DIR}/samples/constitutions/default/validate.js
|
||||
--constitution
|
||||
${CCF_DIR}/samples/constitutions/test/resolve.js
|
||||
--constitution
|
||||
${CCF_DIR}/samples/constitutions/default/apply.js
|
||||
)
|
||||
|
||||
if(LONG_TESTS)
|
||||
set(ADDITIONAL_RECOVERY_ARGS --with-load)
|
||||
endif()
|
||||
|
@ -1254,6 +1241,19 @@ if(BUILD_TESTS)
|
|||
${CMAKE_SOURCE_DIR}/tests/js-launch-host-process
|
||||
)
|
||||
|
||||
set(CONSTITUTION_ARGS
|
||||
--constitution
|
||||
${CCF_DIR}/samples/constitutions/default/actions.js
|
||||
--constitution
|
||||
${CCF_DIR}/samples/constitutions/test/test_actions.js
|
||||
--constitution
|
||||
${CCF_DIR}/samples/constitutions/default/validate.js
|
||||
--constitution
|
||||
${CCF_DIR}/samples/constitutions/test/resolve.js
|
||||
--constitution
|
||||
${CCF_DIR}/samples/constitutions/default/apply.js
|
||||
)
|
||||
|
||||
add_e2e_test(
|
||||
NAME governance_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/governance.py
|
||||
|
@ -1301,8 +1301,22 @@ if(BUILD_TESTS)
|
|||
${CMAKE_SOURCE_DIR}/samples/apps/logging/js
|
||||
)
|
||||
|
||||
set(RBAC_CONSTITUTION_ARGS
|
||||
--constitution
|
||||
${CCF_DIR}/samples/constitutions/default/actions.js
|
||||
--constitution
|
||||
${CCF_DIR}/samples/constitutions/roles/set_role_definition.js
|
||||
--constitution
|
||||
${CCF_DIR}/samples/constitutions/default/validate.js
|
||||
--constitution
|
||||
${CCF_DIR}/samples/constitutions/default/resolve.js
|
||||
--constitution
|
||||
${CCF_DIR}/samples/constitutions/default/apply.js
|
||||
)
|
||||
|
||||
add_e2e_test(
|
||||
NAME programmability
|
||||
CONSTITUTION ${RBAC_CONSTITUTION_ARGS}
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/programmability.py
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
// A simple wrapper for set usage in the KV in the constitution
|
||||
class KVSet {
|
||||
#map;
|
||||
|
||||
constructor(map) {
|
||||
this.#map = map;
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return this.#map.has(key);
|
||||
}
|
||||
|
||||
add(key) {
|
||||
this.#map.set(key, new ArrayBuffer(8));
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
this.#map.delete(key);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.#map.clear();
|
||||
}
|
||||
|
||||
asSetOfStrings() {
|
||||
let set = new Set();
|
||||
this.#map.forEach((_, key) => set.add(ccf.bufToStr(key)));
|
||||
return set;
|
||||
}
|
||||
}
|
||||
|
||||
actions.set(
|
||||
"set_role_definition",
|
||||
new Action(
|
||||
function (args) {
|
||||
checkType(args.role, "string", "role");
|
||||
checkType(args.actions, "array", "actions");
|
||||
for (const [i, action] of args.actions.entries()) {
|
||||
checkType(action, "string", `actions[${i}]`);
|
||||
}
|
||||
},
|
||||
function (args) {
|
||||
let roleDefinition = new KVSet(
|
||||
ccf.kv[`public:ccf.gov.roles.${args.role}`],
|
||||
);
|
||||
let oldValues = roleDefinition.asSetOfStrings();
|
||||
let newValues = new Set(args.actions);
|
||||
for (const action of oldValues) {
|
||||
if (!newValues.has(action)) {
|
||||
roleDefinition.delete(ccf.strToBuf(action));
|
||||
}
|
||||
}
|
||||
for (const action of newValues) {
|
||||
if (!oldValues.has(action)) {
|
||||
roleDefinition.add(ccf.strToBuf(action));
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
|
@ -9,6 +9,7 @@ import http
|
|||
import os
|
||||
import json
|
||||
from infra.runner import ConcurrentRunner
|
||||
from governance_js import action, proposal, ballot_yes
|
||||
|
||||
import npm_tests
|
||||
|
||||
|
@ -25,6 +26,35 @@ export function content(request) {
|
|||
}
|
||||
"""
|
||||
|
||||
TESTJS_ROLE = """
|
||||
export function content(request) {
|
||||
let raw_id = ccf.strToBuf(request.caller.id);
|
||||
let user_info = ccf.kv["public:ccf.gov.users.info"].get(raw_id);
|
||||
if (user_info !== undefined) {
|
||||
user_info = ccf.bufToJsonCompatible(user_info);
|
||||
let roles = user_info?.user_data?.roles || [];
|
||||
|
||||
for (const [_, role] of roles.entries()) {
|
||||
let role_map = ccf.kv[`public:ccf.gov.roles.${role}`];
|
||||
let endpoint_name = request.url.split("/")[2];
|
||||
if (role_map?.has(ccf.strToBuf(`/${endpoint_name}/read`)))
|
||||
{
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
payload: "Test content",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 403
|
||||
};
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def test_custom_endpoints(network, args):
|
||||
primary, _ = network.find_primary()
|
||||
|
@ -86,6 +116,130 @@ def test_custom_endpoints(network, args):
|
|||
return network
|
||||
|
||||
|
||||
def test_custom_role_definitions(network, args):
|
||||
primary, _ = network.find_primary()
|
||||
member = network.consortium.get_any_active_member()
|
||||
|
||||
# Assign a role to user0
|
||||
user = network.users[0]
|
||||
network.consortium.set_user_data(
|
||||
primary,
|
||||
user.service_id,
|
||||
user_data={"isAdmin": True, "roles": ["ContentGetter"]},
|
||||
)
|
||||
|
||||
content_endpoint_def = {
|
||||
"get": {
|
||||
"js_module": "test.js",
|
||||
"js_function": "content",
|
||||
"forwarding_required": "never",
|
||||
"redirection_strategy": "none",
|
||||
"authn_policies": ["user_cert"],
|
||||
"mode": "readonly",
|
||||
"openapi": {},
|
||||
}
|
||||
}
|
||||
|
||||
bundle_with_auth = {
|
||||
"metadata": {"endpoints": {"/content": content_endpoint_def}},
|
||||
"modules": [{"name": "test.js", "module": TESTJS_ROLE}],
|
||||
}
|
||||
|
||||
# Install app with auth/role support
|
||||
with primary.client(None, None, user.local_id) as c:
|
||||
r = c.put("/app/custom_endpoints", body=bundle_with_auth)
|
||||
assert r.status_code == http.HTTPStatus.NO_CONTENT.value, r.status_code
|
||||
|
||||
# Add role definition
|
||||
prop = member.propose(
|
||||
primary,
|
||||
proposal(
|
||||
action(
|
||||
"set_role_definition", role="ContentGetter", actions=["/content/read"]
|
||||
)
|
||||
),
|
||||
)
|
||||
member.vote(primary, prop, ballot_yes)
|
||||
|
||||
# user0 has "ContentGetter" role, which has "/content/read" should be able to access "/content"
|
||||
with primary.client("user0") as c:
|
||||
r = c.get("/app/content")
|
||||
assert r.status_code == http.HTTPStatus.OK, r.status_code
|
||||
assert r.body.json()["payload"] == "Test content", r.body.json()
|
||||
|
||||
# But user1 does not
|
||||
with primary.client("user1") as c:
|
||||
r = c.get("/app/content")
|
||||
assert r.status_code == http.HTTPStatus.FORBIDDEN, r.status_code
|
||||
|
||||
# And unauthenticated users definitely don't
|
||||
with primary.client() as c:
|
||||
r = c.get("/app/content")
|
||||
assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code
|
||||
|
||||
# Delete role definition
|
||||
prop = member.propose(
|
||||
primary,
|
||||
proposal(action("set_role_definition", role="ContentGetter", actions=[])),
|
||||
)
|
||||
member.vote(primary, prop, ballot_yes)
|
||||
|
||||
# Now user0 can't access /content anymore
|
||||
with primary.client("user0") as c:
|
||||
r = c.get("/app/content")
|
||||
assert r.status_code == http.HTTPStatus.FORBIDDEN, r.status_code
|
||||
|
||||
# Multiple definitions
|
||||
prop = member.propose(
|
||||
primary,
|
||||
proposal(
|
||||
action(
|
||||
"set_role_definition", role="ContentGetter", actions=["/content/read"]
|
||||
),
|
||||
action(
|
||||
"set_role_definition",
|
||||
role="AllContentGetter",
|
||||
actions=["/content/read", "/other_content/read"],
|
||||
),
|
||||
),
|
||||
)
|
||||
member.vote(primary, prop, ballot_yes)
|
||||
|
||||
bundle_with_auth_both = {
|
||||
"metadata": {
|
||||
"endpoints": {
|
||||
"/content": content_endpoint_def,
|
||||
"/other_content": content_endpoint_def,
|
||||
}
|
||||
},
|
||||
"modules": [{"name": "test.js", "module": TESTJS_ROLE}],
|
||||
}
|
||||
|
||||
# Install two endpoints with role auth
|
||||
with primary.client(None, None, user.local_id) as c:
|
||||
r = c.put("/app/custom_endpoints", body=bundle_with_auth_both)
|
||||
assert r.status_code == http.HTTPStatus.NO_CONTENT.value, r.status_code
|
||||
|
||||
# Assign the new role to user0
|
||||
user = network.users[0]
|
||||
network.consortium.set_user_data(
|
||||
primary,
|
||||
user.service_id,
|
||||
user_data={"isAdmin": True, "roles": ["ContentGetter", "AllContentGetter"]},
|
||||
)
|
||||
|
||||
# user0 has access both now
|
||||
with primary.client("user0") as c:
|
||||
r = c.get("/app/content")
|
||||
assert r.status_code == http.HTTPStatus.OK, r.status_code
|
||||
assert r.body.json()["payload"] == "Test content", r.body.json()
|
||||
r = c.get("/app/other_content")
|
||||
assert r.status_code == http.HTTPStatus.OK, r.status_code
|
||||
assert r.body.json()["payload"] == "Test content", r.body.json()
|
||||
|
||||
return network
|
||||
|
||||
|
||||
def deploy_npm_app_custom(network, args):
|
||||
primary, _ = network.find_nodes()
|
||||
|
||||
|
@ -123,6 +277,7 @@ def run(args):
|
|||
network.start_and_open(args)
|
||||
|
||||
network = test_custom_endpoints(network, args)
|
||||
network = test_custom_role_definitions(network, args)
|
||||
|
||||
network = npm_tests.build_npm_app(network, args)
|
||||
network = deploy_npm_app_custom(network, args)
|
||||
|
|
Загрузка…
Ссылка в новой задаче