From 5d1cdc002661d30f6f6989120543c44f0e8f0cd3 Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Mon, 6 Nov 2023 15:23:50 -0500 Subject: [PATCH 01/13] Convert RegTokenInfo to a j2s Schema. --- Schema/AdminToken.json | 25 --------- docs/CHANGELOG.md | 11 ++-- src/RegToken.c | 96 ++++++++--------------------------- src/Routes/RouteAdminTokens.c | 20 ++++---- src/Routes/RoutePrivileges.c | 6 +-- src/Routes/RouteRegister.c | 2 +- src/User.c | 27 ++++------ src/include/RegToken.h | 56 +------------------- src/include/User.h | 4 +- 9 files changed, 55 insertions(+), 192 deletions(-) delete mode 100644 Schema/AdminToken.json diff --git a/Schema/AdminToken.json b/Schema/AdminToken.json deleted file mode 100644 index 7c6ba7d..0000000 --- a/Schema/AdminToken.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "header": "Schema\/AdminToken.h", - "types": { - "TokenRequest": { - "fields": { - "name": { "type": "string" }, - "max_uses": { "type": "integer" }, - "lifetime": { "type": "integer" } - }, - "type": "struct" - }, - "TokenInfo": { - "fields": { - "name": { "type": "string" }, - "created_by": { "type": "string" }, - "created_on": { "type": "integer" }, - "expires_on": { "type": "integer" }, - "used": { "type": "integer" }, - "uses": { "type": "integer" } - }, - "type": "struct" - } - }, - "guard": "TELODENDRIA_ADMINTOKEN_H" -} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b28df2e..69f1e5b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -29,13 +29,16 @@ Matrix clients. (#35) ### New Features -- Implemented `/_telodendria/admin/v1/deactivate/[localpart]` for admins to be able to -deactivate users. - Moved all administrator API endpoints to `/_telodendria/admin/v1`, because later revisions of the administrator API may break clients, so we want a way to give those breaking revisions new endpoints. -- Implemented [a few](https://git.telodendria.io/Telodendria/Telodendria/issues/26) endpoints -for admins to manage tokens remotely +- Implemented `/_telodendria/admin/v1/deactivate/[localpart]` for admins to be able to +deactivate users. +- Implemented the following APIs for managing registration tokens: + - **GET** `/_telodendria/admin/tokens` + - **GET** `/_telodendria/admin/tokens/[token]` + - **POST** `/_telodendria/admin/tokens` + - **DELETE** `/_telodendria/admin/tokens/[token]` ## v0.3.0 diff --git a/src/RegToken.c b/src/RegToken.c index 3f8f66d..d842bab 100644 --- a/src/RegToken.c +++ b/src/RegToken.c @@ -30,10 +30,10 @@ #include #include #include -#include #include +#include -#include +#include int RegTokenValid(RegTokenInfo * token) @@ -101,8 +101,7 @@ RegTokenDelete(RegTokenInfo * token) { return 0; } - Free(token->name); - Free(token->owner); + RegTokenInfoFree(token); Free(token); return 1; } @@ -115,6 +114,8 @@ RegTokenGetInfo(Db * db, char *token) DbRef *tokenRef; HashMap *tokenJson; + char *errp = NULL; + if (!RegTokenExists(db, token)) { return NULL; @@ -128,36 +129,25 @@ RegTokenGetInfo(Db * db, char *token) tokenJson = DbJson(tokenRef); ret = Malloc(sizeof(RegTokenInfo)); + if (!RegTokenInfoFromJson(tokenJson, ret, &errp)) + { + Log(LOG_ERR, "RegTokenGetInfo(): Database decoding error: %s", errp); + RegTokenFree(ret); + return NULL; + } + ret->db = db; ret->ref = tokenRef; - ret->owner = - StrDuplicate(JsonValueAsString(HashMapGet(tokenJson, "created_by"))); - ret->name = StrDuplicate(token); - - ret->expires = - JsonValueAsInteger(HashMapGet(tokenJson, "expires_on")); - ret->created = - JsonValueAsInteger(HashMapGet(tokenJson, "created_on")); - - ret->uses = - JsonValueAsInteger(HashMapGet(tokenJson, "uses")); - ret->used = - JsonValueAsInteger(HashMapGet(tokenJson, "used")); - - ret->grants = - UserDecodePrivileges(HashMapGet(tokenJson, "grants")); - return ret; } void -RegTokenFree(RegTokenInfo * tokeninfo) +RegTokenFree(RegTokenInfo *tokeninfo) { if (tokeninfo) { - Free(tokeninfo->name); - Free(tokeninfo->owner); + RegTokenInfoFree(tokeninfo); Free(tokeninfo); } } @@ -169,6 +159,9 @@ RegTokenClose(RegTokenInfo * tokeninfo) return 0; } + /* Write object to database. */ + DbJsonSet(tokeninfo->ref, RegTokenInfoToJson(tokeninfo)); + return DbUnlock(tokeninfo->db, tokeninfo->ref); } static int @@ -200,43 +193,10 @@ RegTokenVerify(char *token) return 1; } -HashMap * -RegTokenJSON(RegTokenInfo * info) -{ - TokenInfo tokinfo; - - if (!info) - { - return NULL; - } - - /* TODO: Consider adding the tokinfo property into - * the RegTokenInfo struct to make that easier. */ - tokinfo.name = info->name; - tokinfo.created_on = info->created; - tokinfo.expires_on = info->expires; - - tokinfo.uses = info->uses; - tokinfo.used = info->used; - - tokinfo.uses = Int64Sub(info->uses, info->used); - if (Int64Eq(info->uses, Int64Neg(Int64Create(0, 1)))) - { - /* If uses == -1(infinite uses), just set it too - * to -1 */ - tokinfo.uses = info->uses; - } - tokinfo.created_by = info->owner; - - - return TokenInfoToJson(&tokinfo); -} - RegTokenInfo * RegTokenCreate(Db * db, char *name, char *owner, UInt64 expires, Int64 uses, int privileges) { RegTokenInfo *ret; - HashMap *tokenJson; UInt64 timestamp = UtilServerTs(); @@ -269,26 +229,12 @@ RegTokenCreate(Db * db, char *name, char *owner, UInt64 expires, Int64 uses, int return NULL; } ret->name = StrDuplicate(name); - ret->owner = StrDuplicate(owner); + ret->created_by = StrDuplicate(owner); ret->used = Int64Create(0, 0); ret->uses = uses; - ret->created = timestamp; - ret->expires = expires; - ret->grants = privileges; - - /* Write user info to database. */ - tokenJson = DbJson(ret->ref); - HashMapSet(tokenJson, "created_by", - JsonValueString(ret->owner)); - HashMapSet(tokenJson, "created_on", - JsonValueInteger(ret->created)); - HashMapSet(tokenJson, "expires_on", - JsonValueInteger(ret->expires)); - HashMapSet(tokenJson, "used", - JsonValueInteger(ret->used)); - HashMapSet(tokenJson, "uses", - JsonValueInteger(ret->uses)); - HashMapSet(tokenJson, "grants", UserEncodePrivileges(privileges)); + ret->created_on = timestamp; + ret->expires_on = expires; + ret->grants = UserEncodePrivileges(privileges); return ret; } diff --git a/src/Routes/RouteAdminTokens.c b/src/Routes/RouteAdminTokens.c index 2f9e768..da9e535 100644 --- a/src/Routes/RouteAdminTokens.c +++ b/src/Routes/RouteAdminTokens.c @@ -28,7 +28,6 @@ #include #include -#include #include #include @@ -54,7 +53,7 @@ ROUTE_IMPL(RouteAdminTokens, path, argp) RegTokenInfo *info; - TokenRequest *req; + RegTokenAdminRequest *req; size_t i; @@ -109,9 +108,8 @@ ROUTE_IMPL(RouteAdminTokens, path, argp) HashMap *jsoninfo; info = RegTokenGetInfo(db, tokenname); - jsoninfo = RegTokenJSON(info); + jsoninfo = RegTokenInfoToJson(info); - RegTokenClose(info); RegTokenFree(info); ArrayAdd(tokensarray, JsonValueObject(jsoninfo)); @@ -131,7 +129,7 @@ ROUTE_IMPL(RouteAdminTokens, path, argp) goto finish; } - response = RegTokenJSON(info); + response = RegTokenInfoToJson(info); RegTokenClose(info); RegTokenFree(info); @@ -144,14 +142,14 @@ ROUTE_IMPL(RouteAdminTokens, path, argp) response = MatrixErrorCreate(M_NOT_JSON, NULL); goto finish; } - req = Malloc(sizeof(TokenRequest)); + req = Malloc(sizeof(RegTokenAdminRequest)); req->max_uses = Int64Neg(Int64Create(0, 1)); req->lifetime = Int64Create(0, 0); req->name = NULL; - if (!TokenRequestFromJson(request, req, &msg)) + if (!RegTokenAdminRequestFromJson(request, req, &msg)) { - TokenRequestFree(req); + RegTokenAdminRequestFree(req); Free(req); HttpResponseStatus(args->context, HTTP_BAD_REQUEST); @@ -175,7 +173,7 @@ ROUTE_IMPL(RouteAdminTokens, path, argp) { RegTokenClose(info); RegTokenFree(info); - TokenRequestFree(req); + RegTokenAdminRequestFree(req); Free(req); HttpResponseStatus(args->context, HTTP_BAD_REQUEST); @@ -183,11 +181,11 @@ ROUTE_IMPL(RouteAdminTokens, path, argp) response = MatrixErrorCreate(M_INVALID_PARAM, msg); goto finish; } - response = RegTokenJSON(info); + response = RegTokenInfoToJson(info); RegTokenClose(info); RegTokenFree(info); - TokenRequestFree(req); + RegTokenAdminRequestFree(req); Free(req); break; case HTTP_DELETE: diff --git a/src/Routes/RoutePrivileges.c b/src/Routes/RoutePrivileges.c index 44ca311..addaba0 100644 --- a/src/Routes/RoutePrivileges.c +++ b/src/Routes/RoutePrivileges.c @@ -98,15 +98,15 @@ ROUTE_IMPL(RoutePrivileges, path, argp) switch (HttpRequestMethodGet(args->context)) { case HTTP_POST: - privileges = UserDecodePrivileges(val); + privileges = UserDecodePrivileges(JsonValueAsArray(val)); break; case HTTP_PUT: privileges = UserGetPrivileges(user); - privileges |= UserDecodePrivileges(val); + privileges |= UserDecodePrivileges(JsonValueAsArray(val)); break; case HTTP_DELETE: privileges = UserGetPrivileges(user); - privileges &= ~UserDecodePrivileges(val); + privileges &= ~UserDecodePrivileges(JsonValueAsArray(val)); break; default: /* Impossible */ diff --git a/src/Routes/RouteRegister.c b/src/Routes/RouteRegister.c index 5422c58..3527219 100644 --- a/src/Routes/RouteRegister.c +++ b/src/Routes/RouteRegister.c @@ -276,7 +276,7 @@ ROUTE_IMPL(RouteRegister, path, argp) if (info) { - UserSetPrivileges(user, info->grants); + UserSetPrivileges(user, UserDecodePrivileges(info->grants)); RegTokenClose(info); RegTokenFree(info); } diff --git a/src/User.c b/src/User.c index 02b1e80..effb8ba 100644 --- a/src/User.c +++ b/src/User.c @@ -768,7 +768,7 @@ UserSetPrivileges(User * user, int privileges) return 1; } - val = UserEncodePrivileges(privileges); + val = JsonValueArray(UserEncodePrivileges(privileges)); if (!val) { return 0; @@ -779,31 +779,26 @@ UserSetPrivileges(User * user, int privileges) } int -UserDecodePrivileges(JsonValue * val) +UserDecodePrivileges(Array * arr) { int privileges = USER_NONE; size_t i; - Array *arr; - if (!val) + if (!arr) { goto finish; } - if (JsonValueType(val) == JSON_ARRAY) + for (i = 0; i < ArraySize(arr); i++) { - arr = JsonValueAsArray(val); - for (i = 0; i < ArraySize(arr); i++) + JsonValue *val = ArrayGet(arr, i); + if (!val || JsonValueType(val) != JSON_STRING) { - val = ArrayGet(arr, i); - if (!val || JsonValueType(val) != JSON_STRING) - { - continue; - } - - privileges |= UserDecodePrivilege(JsonValueAsString(val)); + continue; } + + privileges |= UserDecodePrivilege(JsonValueAsString(val)); } finish: @@ -851,7 +846,7 @@ UserDecodePrivilege(const char *p) } } -JsonValue * +Array * UserEncodePrivileges(int privileges) { Array *arr = ArrayCreate(); @@ -883,7 +878,7 @@ UserEncodePrivileges(int privileges) #undef A finish: - return JsonValueArray(arr); + return arr; } UserId * diff --git a/src/include/RegToken.h b/src/include/RegToken.h index 01b5fbd..309a2c2 100644 --- a/src/include/RegToken.h +++ b/src/include/RegToken.h @@ -42,54 +42,7 @@ #include #include -/** - * This structure describes a registration token that is in the - * database. - */ -typedef struct RegTokenInfo -{ - Db *db; - DbRef *ref; - - /* - * The token itself. - */ - char *name; - - /* - * Who created this token. Note that this can be NULL if the - * token was created by Telodendria itself. - */ - char *owner; - - /* - * How many times the token was used. - */ - Int64 used; - - /* - * How many uses are allowed. - */ - Int64 uses; - - /* - * Timestamp when this token was created. - */ - UInt64 created; - - /* - * Timestamp when this token expires, or 0 if it does not - * expire. - */ - UInt64 expires; - - /* - * A bit field describing the privileges this token grants. See - * the User API documentation for the privileges supported. - */ - int grants; - -} RegTokenInfo; +#include /** * ``Use'' the specified registration token by increasing the used @@ -106,12 +59,6 @@ extern void RegTokenUse(RegTokenInfo *); */ extern int RegTokenExists(Db *, char *); -/** - * Returns a JSON object corresponding to a valid TokenInfo (see - * #26) - */ -extern HashMap * RegTokenJSON(RegTokenInfo *); - /** * Delete the specified registration token from the database. */ @@ -138,7 +85,6 @@ RegTokenCreate(Db *, char *, char *, UInt64, Int64, int); * .Fn RegTokenClose . */ extern void RegTokenFree(RegTokenInfo *); - /** * Return a boolean value indicating whether or not the specified token * is valid. A registration token is only valid if it has not expired diff --git a/src/include/User.h b/src/include/User.h index ec266ec..bd30803 100644 --- a/src/include/User.h +++ b/src/include/User.h @@ -286,13 +286,13 @@ extern int UserSetPrivileges(User *, int); * Decode the JSON that represents the user privileges into a packed * bit field for simple manipulation. */ -extern int UserDecodePrivileges(JsonValue *); +extern int UserDecodePrivileges(Array *); /** * Encode the packed bit field that represents user privileges as a * JSON value. */ -extern JsonValue *UserEncodePrivileges(int); +extern Array *UserEncodePrivileges(int); /** * Convert a string privilege into its bit in the bit field. This is -- 2.43.4 From 6e7f170768914cb238d647c1fc3c0ec98a6aa99c Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Mon, 6 Nov 2023 20:42:39 -0500 Subject: [PATCH 02/13] Start working on #15. --- src/Routes/RouteConfig.c | 50 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/Routes/RouteConfig.c b/src/Routes/RouteConfig.c index 26a471e..1a31bb9 100644 --- a/src/Routes/RouteConfig.c +++ b/src/Routes/RouteConfig.c @@ -39,6 +39,7 @@ ROUTE_IMPL(RouteConfig, path, argp) HashMap *request = NULL; Config *newConf; + HashMap *newJson = NULL; (void) path; @@ -121,7 +122,54 @@ ROUTE_IMPL(RouteConfig, path, argp) JsonFree(request); break; case HTTP_PUT: - /* TODO: Support incremental changes to the config */ + request = JsonDecode(HttpServerStream(args->context)); + if (!request) + { + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_NOT_JSON, NULL); + break; + } + + newJson = JsonDuplicate(DbJson(config->ref)); + JsonMerge(newJson, request); + + newConf = ConfigParse(newJson); + + /* TODO: Don't leak newJson. */ + + if (!newConf) + { + HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); + response = MatrixErrorCreate(M_UNKNOWN, NULL); + break; + } + + if (newConf->ok) + { + if (DbJsonSet(config->ref, newJson)) + { + response = HashMapCreate(); + /* + * TODO: Apply configuration and set this only if a main + * component was reconfigured, such as the listeners. + */ + HashMapSet(response, "restart_required", JsonValueBoolean(1)); + } + else + { + HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); + response = MatrixErrorCreate(M_UNKNOWN, NULL); + } + } + else + { + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_BAD_JSON, newConf->err); + } + + ConfigFree(newConf); + JsonFree(request); + break; default: HttpResponseStatus(args->context, HTTP_BAD_REQUEST); response = MatrixErrorCreate(M_UNRECOGNIZED, NULL); -- 2.43.4 From a1bcbcf45421d42a28e1c36a965329b9ab4d645c Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Tue, 7 Nov 2023 00:27:59 -0500 Subject: [PATCH 03/13] Add RegToken schema --- Schema/RegToken.json | 63 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 Schema/RegToken.json diff --git a/Schema/RegToken.json b/Schema/RegToken.json new file mode 100644 index 0000000..48f7090 --- /dev/null +++ b/Schema/RegToken.json @@ -0,0 +1,63 @@ +{ + "header": "Schema\/RegToken.h", + "include": [ + "Cytoplasm\/Db.h" + ], + "types": { + "RegTokenAdminRequest": { + "fields": { + "name": { + "type": "string" + }, + "max_uses": { + "type": "integer" + }, + "lifetime": { + "type": "integer" + } + }, + "type": "struct" + }, + "Db *": { + "type": "extern" + }, + "DbRef *": { + "type": "extern" + }, + "RegTokenInfo": { + "fields": { + "db": { + "type": "Db *", + "ignore": true + }, + "ref": { + "type": "DbRef *", + "ignore": true + }, + "name": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "created_on": { + "type": "integer" + }, + "expires_on": { + "type": "integer" + }, + "used": { + "type": "integer" + }, + "uses": { + "type": "integer" + }, + "grants": { + "type": "[string]" + } + }, + "type": "struct" + } + }, + "guard": "TELODENDRIA_ADMINTOKEN_H" +} \ No newline at end of file -- 2.43.4 From 4b90800a2bb41a31fc788b3e3cb9619a5cb785a2 Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Tue, 7 Nov 2023 01:26:26 -0500 Subject: [PATCH 04/13] Fix leak in RouteConfig, update documentation. Closes #15. --- docs/CHANGELOG.md | 2 ++ docs/user/admin/config.md | 22 +++++++++++++++++++++- src/Routes/RouteConfig.c | 5 ++--- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b9b7a3a..6e8015c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -31,6 +31,8 @@ Matrix clients. (#35) - Implemented `/_telodendria/admin/v1/deactivate/[localpart]` for admins to be able to deactivate users. +- Added a **PUT** option to `/_telodendria/admin/v1/config` that gives the ability to change +only a subset of the configuration. - Moved all administrator API endpoints to `/_telodendria/admin/v1`, because later revisions of the administrator API may break clients, so we want a way to give those breaking revisions new endpoints. diff --git a/docs/user/admin/config.md b/docs/user/admin/config.md index 0466403..8394f7b 100644 --- a/docs/user/admin/config.md +++ b/docs/user/admin/config.md @@ -4,7 +4,7 @@ As mentioned in [Setup](../setup.md), Telodendria's configuration is intended to be managed via the configuration API. Consult the [Configuration](../config.md) document for a complete list of supported configuration options. This document simply describes the API used to -update the configuration. +update the configuration described in that document. ## API Endpoints @@ -40,3 +40,23 @@ configuration with the new one. |-------|------|-------------| | `restart_required` | `Boolean` | Whether or not the process needs to be restarted to finish applying the configuration. If this is `true`, then the restart endpoint should be used at a convenient time to apply the configuration. +### **PUT** `/_telodendria/admin/config` + +Update the currently installed configuration instead of completely replacing it. This endpoint +validates the request body, merges it on top of the current configuration, validates the resulting +configuration, then updates it in the database. This is useful when only one or two properties +in the configuration needs to be changed. + +| Requires Token | Rate Limited | +|----------------|--------------| +| Yes | Yes | + +| Response Code | Description | +|---------------|-------------| +| 200 | The new configuration was successfully installed.| + +#### 200 Response Format + +| Field | Type | Description | +|-------|------|-------------| +| `restart_required` | `Boolean` | Whether or not the process needs to be restarted to finish applying the configuration. If this is `true`, then the restart endpoint should be used at a convenient time to apply the configuration. diff --git a/src/Routes/RouteConfig.c b/src/Routes/RouteConfig.c index 1a31bb9..5f0c91e 100644 --- a/src/Routes/RouteConfig.c +++ b/src/Routes/RouteConfig.c @@ -135,8 +135,6 @@ ROUTE_IMPL(RouteConfig, path, argp) newConf = ConfigParse(newJson); - /* TODO: Don't leak newJson. */ - if (!newConf) { HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); @@ -169,10 +167,11 @@ ROUTE_IMPL(RouteConfig, path, argp) ConfigFree(newConf); JsonFree(request); + JsonFree(newJson); break; default: HttpResponseStatus(args->context, HTTP_BAD_REQUEST); - response = MatrixErrorCreate(M_UNRECOGNIZED, NULL); + response = MatrixErrorCreate(M_UNRECOGNIZED, "Unknown request method."); break; } -- 2.43.4 From 647778ef826a38257e638bc028020f6c75c6e17e Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Wed, 8 Nov 2023 08:34:24 -0500 Subject: [PATCH 05/13] Fix runtime errors as a result of not encoding privileges as a JSON value. --- src/Routes/RoutePrivileges.c | 2 +- src/User.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Routes/RoutePrivileges.c b/src/Routes/RoutePrivileges.c index addaba0..4ffa480 100644 --- a/src/Routes/RoutePrivileges.c +++ b/src/Routes/RoutePrivileges.c @@ -124,7 +124,7 @@ ROUTE_IMPL(RoutePrivileges, path, argp) /* Fall through */ case HTTP_GET: response = HashMapCreate(); - HashMapSet(response, "privileges", UserEncodePrivileges(UserGetPrivileges(user))); + HashMapSet(response, "privileges", JsonValueArray(UserEncodePrivileges(UserGetPrivileges(user)))); break; default: HttpResponseStatus(args->context, HTTP_BAD_REQUEST); diff --git a/src/User.c b/src/User.c index effb8ba..d13ab90 100644 --- a/src/User.c +++ b/src/User.c @@ -749,7 +749,7 @@ UserGetPrivileges(User * user) return USER_NONE; } - return UserDecodePrivileges(HashMapGet(DbJson(user->ref), "privileges")); + return UserDecodePrivileges(JsonValueAsArray(HashMapGet(DbJson(user->ref), "privileges"))); } int -- 2.43.4 From d60aac6d3a7a685c3768adbc15ea0361dc3714ac Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Wed, 8 Nov 2023 08:43:23 -0500 Subject: [PATCH 06/13] Fix leak in RegTokenClose(). --- src/RegToken.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/RegToken.c b/src/RegToken.c index d842bab..fa041d3 100644 --- a/src/RegToken.c +++ b/src/RegToken.c @@ -154,13 +154,17 @@ RegTokenFree(RegTokenInfo *tokeninfo) int RegTokenClose(RegTokenInfo * tokeninfo) { + HashMap *json; + if (!tokeninfo) { return 0; } /* Write object to database. */ - DbJsonSet(tokeninfo->ref, RegTokenInfoToJson(tokeninfo)); + json = RegTokenInfoToJson(tokeninfo); + DbJsonSet(tokeninfo->ref, json); /* Copies json into internal structure. */ + JsonFree(json); return DbUnlock(tokeninfo->db, tokeninfo->ref); } -- 2.43.4 From 9b3b62ff30f161d03c470edcfab6a8dec63d79ff Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Wed, 8 Nov 2023 09:29:23 -0500 Subject: [PATCH 07/13] [Broken] Revert changes to UserEncodePrivileges() and UserDecodePrivileges() --- src/RegToken.c | 1 + src/Routes/RoutePrivileges.c | 8 ++++---- src/Routes/RouteRegister.c | 1 + src/User.c | 16 +++++++++------- src/include/User.h | 4 ++-- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/RegToken.c b/src/RegToken.c index fa041d3..2b318f6 100644 --- a/src/RegToken.c +++ b/src/RegToken.c @@ -238,6 +238,7 @@ RegTokenCreate(Db * db, char *name, char *owner, UInt64 expires, Int64 uses, int ret->uses = uses; ret->created_on = timestamp; ret->expires_on = expires; + /* TODO: #37 type mismatch: UserEncodePrivileges() returns a JsonValue */ ret->grants = UserEncodePrivileges(privileges); return ret; diff --git a/src/Routes/RoutePrivileges.c b/src/Routes/RoutePrivileges.c index 4ffa480..44ca311 100644 --- a/src/Routes/RoutePrivileges.c +++ b/src/Routes/RoutePrivileges.c @@ -98,15 +98,15 @@ ROUTE_IMPL(RoutePrivileges, path, argp) switch (HttpRequestMethodGet(args->context)) { case HTTP_POST: - privileges = UserDecodePrivileges(JsonValueAsArray(val)); + privileges = UserDecodePrivileges(val); break; case HTTP_PUT: privileges = UserGetPrivileges(user); - privileges |= UserDecodePrivileges(JsonValueAsArray(val)); + privileges |= UserDecodePrivileges(val); break; case HTTP_DELETE: privileges = UserGetPrivileges(user); - privileges &= ~UserDecodePrivileges(JsonValueAsArray(val)); + privileges &= ~UserDecodePrivileges(val); break; default: /* Impossible */ @@ -124,7 +124,7 @@ ROUTE_IMPL(RoutePrivileges, path, argp) /* Fall through */ case HTTP_GET: response = HashMapCreate(); - HashMapSet(response, "privileges", JsonValueArray(UserEncodePrivileges(UserGetPrivileges(user)))); + HashMapSet(response, "privileges", UserEncodePrivileges(UserGetPrivileges(user))); break; default: HttpResponseStatus(args->context, HTTP_BAD_REQUEST); diff --git a/src/Routes/RouteRegister.c b/src/Routes/RouteRegister.c index 3527219..7f51744 100644 --- a/src/Routes/RouteRegister.c +++ b/src/Routes/RouteRegister.c @@ -276,6 +276,7 @@ ROUTE_IMPL(RouteRegister, path, argp) if (info) { + /* TODO: #37 type mismatch: info->grants is a string array. */ UserSetPrivileges(user, UserDecodePrivileges(info->grants)); RegTokenClose(info); RegTokenFree(info); diff --git a/src/User.c b/src/User.c index d13ab90..1996255 100644 --- a/src/User.c +++ b/src/User.c @@ -749,7 +749,7 @@ UserGetPrivileges(User * user) return USER_NONE; } - return UserDecodePrivileges(JsonValueAsArray(HashMapGet(DbJson(user->ref), "privileges"))); + return UserDecodePrivileges(HashMapGet(DbJson(user->ref), "privileges")); } int @@ -768,7 +768,7 @@ UserSetPrivileges(User * user, int privileges) return 1; } - val = JsonValueArray(UserEncodePrivileges(privileges)); + val = UserEncodePrivileges(privileges); if (!val) { return 0; @@ -779,17 +779,19 @@ UserSetPrivileges(User * user, int privileges) } int -UserDecodePrivileges(Array * arr) +UserDecodePrivileges(JsonValue * val) { int privileges = USER_NONE; - + Array *arr; size_t i; - if (!arr) + if (!val || JsonValueType(val) != JSON_ARRAY) { goto finish; } + arr = JsonValueAsArray(val); + for (i = 0; i < ArraySize(arr); i++) { JsonValue *val = ArrayGet(arr, i); @@ -846,7 +848,7 @@ UserDecodePrivilege(const char *p) } } -Array * +JsonValue * UserEncodePrivileges(int privileges) { Array *arr = ArrayCreate(); @@ -878,7 +880,7 @@ UserEncodePrivileges(int privileges) #undef A finish: - return arr; + return JsonValueArray(arr); } UserId * diff --git a/src/include/User.h b/src/include/User.h index bd30803..ec266ec 100644 --- a/src/include/User.h +++ b/src/include/User.h @@ -286,13 +286,13 @@ extern int UserSetPrivileges(User *, int); * Decode the JSON that represents the user privileges into a packed * bit field for simple manipulation. */ -extern int UserDecodePrivileges(Array *); +extern int UserDecodePrivileges(JsonValue *); /** * Encode the packed bit field that represents user privileges as a * JSON value. */ -extern Array *UserEncodePrivileges(int); +extern JsonValue *UserEncodePrivileges(int); /** * Convert a string privilege into its bit in the bit field. This is -- 2.43.4 From edee1288d8fd9aae7e44721cbf9354c50a34ad29 Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Wed, 8 Nov 2023 09:45:49 -0500 Subject: [PATCH 08/13] Remove unused global variables. --- src/Main.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Main.c b/src/Main.c index ecba436..4b25c9e 100644 --- a/src/Main.c +++ b/src/Main.c @@ -128,10 +128,6 @@ start: httpServers = NULL; restart = 0; - /* For getopt() */ - opterr = 1; - optind = 1; - /* Local variables */ exit = EXIT_SUCCESS; flags = 0; -- 2.43.4 From a5c5dee37553401b461cbe80088a46c63f022ce4 Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Wed, 8 Nov 2023 09:50:14 -0500 Subject: [PATCH 09/13] Revert "[Broken] Revert changes to UserEncodePrivileges() and UserDecodePrivileges()" This reverts commit 9b3b62ff30f161d03c470edcfab6a8dec63d79ff. --- src/RegToken.c | 1 - src/Routes/RoutePrivileges.c | 8 ++++---- src/Routes/RouteRegister.c | 1 - src/User.c | 16 +++++++--------- src/include/User.h | 4 ++-- 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/RegToken.c b/src/RegToken.c index 2b318f6..fa041d3 100644 --- a/src/RegToken.c +++ b/src/RegToken.c @@ -238,7 +238,6 @@ RegTokenCreate(Db * db, char *name, char *owner, UInt64 expires, Int64 uses, int ret->uses = uses; ret->created_on = timestamp; ret->expires_on = expires; - /* TODO: #37 type mismatch: UserEncodePrivileges() returns a JsonValue */ ret->grants = UserEncodePrivileges(privileges); return ret; diff --git a/src/Routes/RoutePrivileges.c b/src/Routes/RoutePrivileges.c index 44ca311..4ffa480 100644 --- a/src/Routes/RoutePrivileges.c +++ b/src/Routes/RoutePrivileges.c @@ -98,15 +98,15 @@ ROUTE_IMPL(RoutePrivileges, path, argp) switch (HttpRequestMethodGet(args->context)) { case HTTP_POST: - privileges = UserDecodePrivileges(val); + privileges = UserDecodePrivileges(JsonValueAsArray(val)); break; case HTTP_PUT: privileges = UserGetPrivileges(user); - privileges |= UserDecodePrivileges(val); + privileges |= UserDecodePrivileges(JsonValueAsArray(val)); break; case HTTP_DELETE: privileges = UserGetPrivileges(user); - privileges &= ~UserDecodePrivileges(val); + privileges &= ~UserDecodePrivileges(JsonValueAsArray(val)); break; default: /* Impossible */ @@ -124,7 +124,7 @@ ROUTE_IMPL(RoutePrivileges, path, argp) /* Fall through */ case HTTP_GET: response = HashMapCreate(); - HashMapSet(response, "privileges", UserEncodePrivileges(UserGetPrivileges(user))); + HashMapSet(response, "privileges", JsonValueArray(UserEncodePrivileges(UserGetPrivileges(user)))); break; default: HttpResponseStatus(args->context, HTTP_BAD_REQUEST); diff --git a/src/Routes/RouteRegister.c b/src/Routes/RouteRegister.c index 7f51744..3527219 100644 --- a/src/Routes/RouteRegister.c +++ b/src/Routes/RouteRegister.c @@ -276,7 +276,6 @@ ROUTE_IMPL(RouteRegister, path, argp) if (info) { - /* TODO: #37 type mismatch: info->grants is a string array. */ UserSetPrivileges(user, UserDecodePrivileges(info->grants)); RegTokenClose(info); RegTokenFree(info); diff --git a/src/User.c b/src/User.c index 1996255..d13ab90 100644 --- a/src/User.c +++ b/src/User.c @@ -749,7 +749,7 @@ UserGetPrivileges(User * user) return USER_NONE; } - return UserDecodePrivileges(HashMapGet(DbJson(user->ref), "privileges")); + return UserDecodePrivileges(JsonValueAsArray(HashMapGet(DbJson(user->ref), "privileges"))); } int @@ -768,7 +768,7 @@ UserSetPrivileges(User * user, int privileges) return 1; } - val = UserEncodePrivileges(privileges); + val = JsonValueArray(UserEncodePrivileges(privileges)); if (!val) { return 0; @@ -779,19 +779,17 @@ UserSetPrivileges(User * user, int privileges) } int -UserDecodePrivileges(JsonValue * val) +UserDecodePrivileges(Array * arr) { int privileges = USER_NONE; - Array *arr; + size_t i; - if (!val || JsonValueType(val) != JSON_ARRAY) + if (!arr) { goto finish; } - arr = JsonValueAsArray(val); - for (i = 0; i < ArraySize(arr); i++) { JsonValue *val = ArrayGet(arr, i); @@ -848,7 +846,7 @@ UserDecodePrivilege(const char *p) } } -JsonValue * +Array * UserEncodePrivileges(int privileges) { Array *arr = ArrayCreate(); @@ -880,7 +878,7 @@ UserEncodePrivileges(int privileges) #undef A finish: - return JsonValueArray(arr); + return arr; } UserId * diff --git a/src/include/User.h b/src/include/User.h index ec266ec..bd30803 100644 --- a/src/include/User.h +++ b/src/include/User.h @@ -286,13 +286,13 @@ extern int UserSetPrivileges(User *, int); * Decode the JSON that represents the user privileges into a packed * bit field for simple manipulation. */ -extern int UserDecodePrivileges(JsonValue *); +extern int UserDecodePrivileges(Array *); /** * Encode the packed bit field that represents user privileges as a * JSON value. */ -extern JsonValue *UserEncodePrivileges(int); +extern Array *UserEncodePrivileges(int); /** * Convert a string privilege into its bit in the bit field. This is -- 2.43.4 From 80db737a289b6285fd627e170c3d8af9567154ac Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Wed, 8 Nov 2023 09:55:47 -0500 Subject: [PATCH 10/13] Make RegToken.grants a JSON array. It isn't every touched by anything other than UserDecodePrivileges() and UserEncodePrivileges(). --- Schema/RegToken.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Schema/RegToken.json b/Schema/RegToken.json index 48f7090..12154ad 100644 --- a/Schema/RegToken.json +++ b/Schema/RegToken.json @@ -53,7 +53,7 @@ "type": "integer" }, "grants": { - "type": "[string]" + "type": "array" } }, "type": "struct" -- 2.43.4 From f87cce0f4c4c418c3b524ed02199fa93a759319e Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Wed, 8 Nov 2023 12:36:01 -0500 Subject: [PATCH 11/13] Clean up behavior of RouteAdminTokens(). This makes it accept keys with the same names as what is returned back to the caller, allowing for a more consistent experience. --- Schema/Filter.json | 4 ++-- Schema/RegToken.json | 18 ++---------------- src/Routes/RouteAdminTokens.c | 36 +++++++++++++++-------------------- 3 files changed, 19 insertions(+), 39 deletions(-) diff --git a/Schema/Filter.json b/Schema/Filter.json index ee6a233..20f8f0a 100644 --- a/Schema/Filter.json +++ b/Schema/Filter.json @@ -1,4 +1,5 @@ { + "guard": "TELODENDRIA_SCHEMA_FILTER_H", "header": "Schema\/Filter.h", "types": { "FilterRoom": { @@ -116,6 +117,5 @@ }, "type": "struct" } - }, - "guard": "TELODENDRIA_SCHEMA_FILTER_H" + } } diff --git a/Schema/RegToken.json b/Schema/RegToken.json index 12154ad..2cb56ff 100644 --- a/Schema/RegToken.json +++ b/Schema/RegToken.json @@ -1,23 +1,10 @@ { + "guard": "TELODENDRIA_SCHEMA_REGTOKEN_H", "header": "Schema\/RegToken.h", "include": [ "Cytoplasm\/Db.h" ], "types": { - "RegTokenAdminRequest": { - "fields": { - "name": { - "type": "string" - }, - "max_uses": { - "type": "integer" - }, - "lifetime": { - "type": "integer" - } - }, - "type": "struct" - }, "Db *": { "type": "extern" }, @@ -58,6 +45,5 @@ }, "type": "struct" } - }, - "guard": "TELODENDRIA_ADMINTOKEN_H" + } } \ No newline at end of file diff --git a/src/Routes/RouteAdminTokens.c b/src/Routes/RouteAdminTokens.c index da9e535..6752599 100644 --- a/src/Routes/RouteAdminTokens.c +++ b/src/Routes/RouteAdminTokens.c @@ -38,8 +38,6 @@ ROUTE_IMPL(RouteAdminTokens, path, argp) HashMap *response = NULL; char *token; - char *username; - char *name; char *msg; Db *db = args->matrixArgs->db; @@ -53,13 +51,10 @@ ROUTE_IMPL(RouteAdminTokens, path, argp) RegTokenInfo *info; - RegTokenAdminRequest *req; + RegTokenInfo *req; size_t i; - Int64 maxuses; - Int64 lifetime; - if (method != HTTP_GET && method != HTTP_POST && method != HTTP_DELETE) { msg = "Route only supports GET, POST, and DELETE"; @@ -120,6 +115,7 @@ ROUTE_IMPL(RouteAdminTokens, path, argp) DbListFree(tokens); break; } + info = RegTokenGetInfo(db, ArrayGet(path, 0)); if (!info) { @@ -142,14 +138,13 @@ ROUTE_IMPL(RouteAdminTokens, path, argp) response = MatrixErrorCreate(M_NOT_JSON, NULL); goto finish; } - req = Malloc(sizeof(RegTokenAdminRequest)); - req->max_uses = Int64Neg(Int64Create(0, 1)); - req->lifetime = Int64Create(0, 0); - req->name = NULL; - if (!RegTokenAdminRequestFromJson(request, req, &msg)) + req = Malloc(sizeof(RegTokenInfo)); + memset(req, 0, sizeof(RegTokenInfo)); + + if (!RegTokenInfoFromJson(request, req, &msg)) { - RegTokenAdminRequestFree(req); + RegTokenInfoFree(req); Free(req); HttpResponseStatus(args->context, HTTP_BAD_REQUEST); @@ -163,29 +158,28 @@ ROUTE_IMPL(RouteAdminTokens, path, argp) req->name = StrRandom(16); } - username = UserGetName(user); - name = req->name; - maxuses = req->max_uses; - lifetime = req->lifetime; - - info = RegTokenCreate(db, name, username, maxuses, lifetime, 0); + /* Create the actual token that will be stored. */ + info = RegTokenCreate(db, req->name, UserGetName(user), + req->expires_on, req->uses, + UserDecodePrivileges(req->grants)); if (!info) { RegTokenClose(info); RegTokenFree(info); - RegTokenAdminRequestFree(req); + RegTokenInfoFree(req); Free(req); HttpResponseStatus(args->context, HTTP_BAD_REQUEST); - msg = "Cannot create token"; + msg = "Cannot create token."; response = MatrixErrorCreate(M_INVALID_PARAM, msg); goto finish; } + response = RegTokenInfoToJson(info); RegTokenClose(info); RegTokenFree(info); - RegTokenAdminRequestFree(req); + RegTokenInfoFree(req); Free(req); break; case HTTP_DELETE: -- 2.43.4 From 38a59d890f6c8686b8b6d5f44dcdd43ce3ca7887 Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Wed, 8 Nov 2023 13:42:41 -0500 Subject: [PATCH 12/13] Add documentation for RouteAdminTokens. --- docs/user/admin/tokens.md | 106 ++++++++++++++++++++++++++++++++++ src/Routes/RouteAdminTokens.c | 10 ++-- 2 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 docs/user/admin/tokens.md diff --git a/docs/user/admin/tokens.md b/docs/user/admin/tokens.md new file mode 100644 index 0000000..18baf01 --- /dev/null +++ b/docs/user/admin/tokens.md @@ -0,0 +1,106 @@ +# Administrator API: Registration Tokens + +Telodendria implements registration tokens as specified by the Matrix +specification. These tokens can be used for registration using the +`m.login.registration_token` login type. This API provides a Telodendria +administrator with a mechanism for generating and managing these tokens, +which allows controlled registration on the homeserver. + +It is generally safer than completely open registration to use +registration tokens that either expire after a short period of time, or +have a limited number of uses. + +## Registration Token + +A registration token is represented by the following `RegToken` JSON +object: + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `String` | The token identifier; what is used when registering. | +| `created_by` | `String` | The localpart of the user that created this token. | +| `created_on` | `Integer` | A timestamp of when the token was created. | +| `expires_on` | `Integer` | An expiration stamp, or 0 if the token never expires. | +| `used` | `Integer` | The number of times the token has been used. | +| `uses` | `Integer` | The total number of allowed uses, or -1 for unlimited. | +| `grants` | `[String]` | An array of privileges to grant users that register with this token as described in [Privileges](privileges.md). | + +All endpoints in this API will operate on some variation of this +structure. The remaining number of uses can be computed by performing +the subtraction: `uses - used`. `used` should never be greater than +`uses` or less than `0`. + +Example: + +```json +{ + "name": "q34jgapo8uq34hg", + "created_by": "admin", + "created_on": 1699467640000, + "expires_on": 0, + "used": 3, + "uses": 5 +} +``` + +## API Endpoints + +### **GET** `/_telodendria/admin/v1/tokens` + +Get a list of all registration tokens and information about them. + +#### 200 Response Format + +| Field | Type | Description | +|-------|------|-------------| +| `tokens` | `[RegToken]` | An array of registration tokens. | + +### **GET** `/_telodendria/admin/v1/tokens/[name]` + +Get information about the specified registration token. + +#### Request Parameters + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `String` | The name of the token, as it would be used to register a user. | + +#### 200 Response Format + +This endpoint returns a `RegToken` object that represents the server's +record of the registration token. + +### **POST** `/_telodendria/admin/v1/tokens` + +Create a new registration token. + +#### Request Format + +This endpoint accepts a `RegToken` object, as described above. If no +`name` is provided, one will be randomly generated. Note that the fields +`created_by`, `created_on`, and `used` are ignored and set by the server +when this request is made. All other fields may be set by the request +body. + +#### 200 Response Format + +If the creation of the registration token was successful, a `RegToken` +that represents the server's record of it is returned. + +### **DELETE** `/_telodendria/admin/v1/tokens/[name]` + +Delete the specified registration token. It will no longer be usable for +the registration of users. Any users that have completed the +`m.login.registration_token` step but have not yet created their account +should still be able to do so until their user-interactive auth session +expires. + +#### Request Parameters + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `String` | The name of the token, as it would be used to register a user. | + +#### 200 Response Format + +On success, this endpoint returns an empty JSON object. \ No newline at end of file diff --git a/src/Routes/RouteAdminTokens.c b/src/Routes/RouteAdminTokens.c index 6752599..daca0ff 100644 --- a/src/Routes/RouteAdminTokens.c +++ b/src/Routes/RouteAdminTokens.c @@ -107,6 +107,8 @@ ROUTE_IMPL(RouteAdminTokens, path, argp) RegTokenClose(info); RegTokenFree(info); + + /* TODO: The JsonValue returned here gets leaked. */ ArrayAdd(tokensarray, JsonValueObject(jsoninfo)); } @@ -192,11 +194,11 @@ ROUTE_IMPL(RouteAdminTokens, path, argp) } info = RegTokenGetInfo(db, ArrayGet(path, 0)); RegTokenDelete(info); - /* As this is a No Content, let's not set any data in the - * response */ - HttpResponseStatus(args->context, HTTP_NO_CONTENT); + + response = HashMapCreate(); + break; default: - /* Fallthrough, as those are naturally kept out beforehand */ + /* Should not be possible. */ break; } finish: -- 2.43.4 From e97faa9761227e9df99b181512c0e9e998d51e0d Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Wed, 8 Nov 2023 13:44:17 -0500 Subject: [PATCH 13/13] Add link to registration token endpoints to admin documentation. --- docs/user/admin/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/user/admin/README.md b/docs/user/admin/README.md index 827ef10..dc62647 100644 --- a/docs/user/admin/README.md +++ b/docs/user/admin/README.md @@ -19,6 +19,7 @@ request. - [Configuration](config.md) - [Server Statistics](stats.md) - [Process Control](proc.md) +- [Registration Tokens](tokens.md) ## API Conventions -- 2.43.4