This commit is contained in:
Amaury Chamayou 2024-06-07 15:58:45 +01:00 коммит произвёл GitHub
Родитель 992f4bad6b
Коммит 3ba5e0133b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
3 изменённых файлов: 242 добавлений и 13 удалений

Просмотреть файл

@ -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)