From 50759a329838cca6f16a65c8fe74a29207e9cfeb Mon Sep 17 00:00:00 2001 From: lda Date: Wed, 15 May 2024 08:41:59 +0200 Subject: [PATCH] [UNTESTED/ADD] Finish barebones of auth rules --- src/Room.c | 260 +++++++++++++++++++++++++++++++++++++++++++- src/State.c | 34 ++++++ src/include/State.h | 15 +++ 3 files changed, 305 insertions(+), 4 deletions(-) diff --git a/src/Room.c b/src/Room.c index 7b5ad6d..9ad15e0 100644 --- a/src/Room.c +++ b/src/Room.c @@ -162,12 +162,172 @@ RoomVersionGet(Room * room) HashMap * RoomStateGet(Room * room) { + HashMap *database_state; if (!room) { return NULL; } + + /* TODO: Consider caching the deserialised result, as doing that on a + * large state would probably eat up a lot of time! */ + database_state = DbJson(room->ref); + return StateDeserialise(database_state); +} +static HashMap * +RoomStateGetID(Room * room, char *event_id) +{ + HashMap *event, *state; + if (!room || !event_id) + { + return NULL; + } - return NULL; + event = RoomEventFetch(room, event_id); + if (!event) + { + return NULL; + } + + state = StateResolve(room, event); + JsonFree(event); + + return state; +} + +#define PrepareState(room, e, type, key, n) \ + do \ + { \ + state_point = RoomStateGetID(room, e); \ + id_##n = StateGet(state_point, type, key); \ + if (id_##n) \ + { \ + goto finish; \ + } \ + n = RoomEventFetch(room, id_##n); \ + if (!n) \ + { \ + goto finish; \ + } \ + } \ + while (0) +#define FinishState(name) \ + if (state_point) \ + { \ + StateFree(state_point); \ + } \ + if (name) \ + { \ + JsonFree(name); \ + } \ + return ret +/* Verifies if user has a specific membership before [e_id] in the room. */ +static bool +RoomUserHasMembership(Room * room, char *e_id, char *user, char *mbr) +{ + HashMap *state_point; + HashMap *membership = NULL; + char *id_membership = NULL; + char *membership_value; + bool ret = false; + if (!room || !e_id || !user || !mbr) + { + return false; + } + + PrepareState(room, e_id, "m.room.member", user, membership); + + membership_value = + JsonValueAsString(JsonGet(membership, 2, "content", "membership")); + ret = StrEquals(membership_value, mbr); + +finish: + FinishState(membership); +} +static int64_t +ParsePL(JsonValue *v, int64_t def) +{ + if (!v) + { + return def; + } + if (JsonValueType(v) == JSON_INTEGER) + { + return JsonValueAsInteger(v); + } + if (JsonValueType(v) == JSON_STRING) + { + char *string = JsonValueAsString(v), *end; + int64_t value= strtoll(string, &end, 10); + if (!((*string != '\0') && (*end == '\0'))) + { + /* Invalid string: return the default. */ + return def; + } + + return value; + } + + return def; +} +/* Computes the smallest PL needed to do something somewhere */ +static int64_t +RoomMinPL(Room * room, char *e_id, char *type, char *act) +{ + HashMap *state_point, *pl = NULL; + JsonValue *val; + char *id_pl; + int64_t ret, def; + if (!room || !e_id || !act) + { + return 0; + } + + PrepareState(room, e_id, "m.room.power_levels", "", pl); + + /* Every other act has a minimum PL of 0 */ + def = 0; + if (StrEquals(act, "ban") || + StrEquals(act, "kick") || + StrEquals(act, "redact") || + StrEquals(act, "state_default") || + (StrEquals(type, "notifications") && StrEquals(act, "room"))) + { + def = 50; + } + + if (!type) + { + val = JsonGet(pl, 1, act); + } + else + { + val = JsonGet(pl, 2, type, act); + } + + ret = ParsePL(val, def); +finish: + FinishState(pl); +} +/* Finds the power level of an user before [e_id] was sent. */ +static int64_t +RoomUserPL(Room * room, char *e_id, char *user) +{ + HashMap *state_point, *pl = NULL; + char *id_pl; + int64_t ret, def; + + if (!room || !e_id || !user) + { + return 0; + } + + PrepareState(room, e_id, "m.room.power_levels", "", pl); + + def = RoomMinPL(room, e_id, NULL, "users_default"); + ret = ParsePL(JsonGet(pl, 2, "users", user), def); + +finish: + FinishState(pl); } static bool @@ -254,6 +414,8 @@ ValidAuthEventV1(PduV1 *auth_pdu, PduV1 *pdu) { char *membership = JsonValueAsString(JsonGet(pdu->content, 1, "membership")); + JsonValue *third_pid = + JsonGet(pdu->content, 1, "third_party_invite"); if (IsState(auth_pdu, "m.room.member", pdu->sender)) { /* TODO: Check if it's the latest in terms of [pdu] */ @@ -266,8 +428,20 @@ ValidAuthEventV1(PduV1 *auth_pdu, PduV1 *pdu) /* TODO: Check if it's the latest in terms of [pdu] */ return true; } - /* TODO: The rest. - * https://spec.matrix.org/v1.7/server-server-api/#auth-events-selection */ + if (StrEquals(membership, "invite") && third_pid) + { + HashMap *tpid = JsonValueAsObject(third_pid); + JsonValue *token = + JsonGet(tpid, 2, "signed", "token"); + char *token_str = JsonValueAsString(token); + if (IsState(auth_pdu, "m.room.third_party_invite", token_str)) + { + /* TODO: Check if it is the latest. */ + return true; + } + } + /* V1 simply doesn't have the concept of restricted rooms, + * so we can safely skip this one for this function. */ } return false; } @@ -286,6 +460,7 @@ ConsiderAuthEventsV1(Room * room, PduV1 pdu) { char *ignored; size_t i; + bool room_create = false; HashMap *state_keytype; state_keytype = HashMapCreate(); for (i = 0; i < ArraySize(pdu.auth_events); i++) @@ -339,12 +514,16 @@ ConsiderAuthEventsV1(Room * room, PduV1 pdu) /* Step 2.4: If there is no m.room.create event among the entries, * reject. */ + if (!room_create && IsState((&auth_pdu), "m.room.create", "")) + { + room_create = true; /* Here, we check for the opposite. */ + } HashMapFree(event); PduV1Free(&auth_pdu); } HashMapFree(state_keytype); - return true; + return room_create; /* Step 2.4 is actually done here. */ } static bool VerifyServers(char *id1, char *id2) @@ -470,12 +649,22 @@ AuthoriseMemberV1(Room * room, PduV1 pdu) return true; #undef JumpIfMembership } +static bool +AuthorisePowerLevelsV1(Room * room, PduV1 pdu) +{ + /* TODO: Implement this. */ + (void) room; + (void) pdu; + return true; +} bool AuthoriseEventV1(Room * room, PduV1 pdu) { HashMap *state, *create_event; char *create_event_id; JsonValue *federate; + int64_t pdu_pl = RoomUserPL(room, pdu.event_id, pdu.sender); + int64_t event_pl = RoomMinPL(room, pdu.event_id, "events", pdu.type); /* Step 1: If m.room.create */ if (StrEquals(pdu.type, "m.room.create")) { @@ -529,6 +718,69 @@ AuthoriseEventV1(Room * room, PduV1 pdu) { return AuthoriseMemberV1(room, pdu); } + /* Step 6: If the sender's current membership state is not join, reject. */ + if (!RoomUserHasMembership(room, pdu.event_id, pdu.sender, "join")) + { + return false; + } + /* Step 7: If type is m.room.third_party_invite */ + if (StrEquals(pdu.type, "m.room.third_party_invite")) + { + /* Allow if and only if sender's current power level is greater than + * or equal to the invite level */ + int64_t min_pl = RoomMinPL(room, pdu.event_id, NULL, "invite"); + + return pdu_pl >= min_pl; + } + + /* Step 8: If the event type's required power level is greater than the + * sender's power level, reject. */ + if (event_pl > pdu_pl) + { + return false; + } + + /* Step 9: If the event has a state_key that starts with an @ and does + * not match the sender, reject. */ + if (pdu.state_key && *pdu.state_key == '@') + { + if (!StrEquals(pdu.state_key, pdu.sender)) + { + return false; + } + } + + /* Step 10: If type is m.room.power_levels */ + if (StrEquals(pdu.type, "m.room.power_levels")) + { + return AuthorisePowerLevelsV1(room, pdu); + } + + /* Step 11: If type is m.room.redaction */ + if (StrEquals(pdu.type, "m.room.redaction")) + { + int64_t min_pl = RoomMinPL(room, pdu.event_id, NULL, "redact"); + + /* Step 11.1: If the sender's power level is greater than or equal + * to the redact level, allow. */ + if (pdu_pl >= min_pl) + { + return true; + } + + /* Step 11.2: If the domain of the event_id of the event being + * redacted is the same as the domain of the event_id of the + * m.room.redaction, allow. */ + if (pdu.redacts && VerifyServers(pdu.redacts, pdu.event_id)) + { + return true; + } + + /* Step 11.3: Otherwise, reject. */ + return false; + } + + /* Step 12: Otherwise, allow. */ return true; } static char * diff --git a/src/State.c b/src/State.c index 632d0b2..d23d42c 100644 --- a/src/State.c +++ b/src/State.c @@ -120,3 +120,37 @@ StateFree(HashMap *state) } HashMapFree(state); } +HashMap * +StateDeserialise(HashMap *json_state) +{ + HashMap *raw_state; + + char *state_type; + JsonValue *state_keys; + + if (!json_state) + { + return NULL; + } + + raw_state = HashMapCreate(); + + while (HashMapIterate(json_state, &state_type, (void **) &state_keys)) + { + HashMap *state_keys_obj = JsonValueAsObject(state_keys); + char *state_key; + JsonValue *event_id; + + while (HashMapIterate(state_keys_obj, &state_key, (void **) &event_id)) + { + char *eid_string = JsonValueAsString(event_id); + char *key_name = StrConcat(3, state_type, ",", state_key); + + HashMapSet(raw_state, key_name, StrDuplicate(eid_string)); + + Free(key_name); + } + } + + return raw_state; +} diff --git a/src/include/State.h b/src/include/State.h index 765caf4..6123e2a 100644 --- a/src/include/State.h +++ b/src/include/State.h @@ -59,4 +59,19 @@ extern HashMap * StateResolve(Room *, HashMap *); */ extern void StateFree(HashMap *); +/** + * Deserialises a state map from JSON to the internal format + * used by this API. + * + * The returned value is independent from the JSON format, + * and should be freed with + * .Fn StateFree . + */ +extern HashMap * StateDeserialise(HashMap *); +/** + * Serialises a state map from the internal format to JSON + * used for the database, for example + */ +extern HashMap * StateSerialise(HashMap *); + #endif /* TELODENDRIA_STATE_H */