Skip to content

Commit

Permalink
Set role definition sample (#6237)
Browse files Browse the repository at this point in the history
  • Loading branch information
achamayou authored Jun 7, 2024
1 parent 992f4ba commit 3ba5e01
Show file tree
Hide file tree
Showing 3 changed files with 242 additions and 13 deletions.
40 changes: 27 additions & 13 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
)

Expand Down
60 changes: 60 additions & 0 deletions samples/constitutions/roles/set_role_definition.js
Original file line number Diff line number Diff line change
@@ -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));
}
}
},
),
);
155 changes: 155 additions & 0 deletions tests/programmability.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import os
import json
from infra.runner import ConcurrentRunner
from governance_js import action, proposal, ballot_yes

import npm_tests

Expand All @@ -25,6 +26,35 @@
}
"""

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()
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 3ba5e01

Please sign in to comment.