From 2d30719be4300e211e01ce8c3a6a72df6ae0a809 Mon Sep 17 00:00:00 2001 From: Jordan Bancino Date: Fri, 10 Nov 2023 09:30:53 -0500 Subject: [PATCH] Implement Registration Token Administrator API (#43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request fully implements and documents all of the registration token administrator API endpoints. **NOTE:** There is a memory leak when listing all of the registration tokens. Debug this before merging.   ~~Closes~~ Supersedes #37. Closes #26. This pull request is based off of #37, which addresses #26. This pull makes a number of improvements to the logic, organization, and behavior of the API endpoints. Co-authored-by: hatkid Co-authored-by: LoaD Accumulator Co-authored-by: LoaD Accumulator Co-authored-by: lda Co-authored-by: Load Accumulator Reviewed-on: https://git.telodendria.io/Telodendria/Telodendria/pulls/43 --- Schema/Filter.json | 4 +- Schema/RegToken.json | 49 ++++++++ docs/CHANGELOG.md | 5 + docs/user/admin/README.md | 1 + docs/user/admin/tokens.md | 106 +++++++++++++++++ src/RegToken.c | 68 +++++------ src/Routes.c | 2 + src/Routes/RouteAdminTokens.c | 207 ++++++++++++++++++++++++++++++++++ src/Routes/RoutePrivileges.c | 8 +- src/Routes/RouteRegister.c | 2 +- src/User.c | 29 ++--- src/include/RegToken.h | 50 +------- src/include/Routes.h | 2 + src/include/User.h | 4 +- 14 files changed, 420 insertions(+), 117 deletions(-) create mode 100644 Schema/RegToken.json create mode 100644 docs/user/admin/tokens.md create mode 100644 src/Routes/RouteAdminTokens.c 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 new file mode 100644 index 0000000..2cb56ff --- /dev/null +++ b/Schema/RegToken.json @@ -0,0 +1,49 @@ +{ + "guard": "TELODENDRIA_SCHEMA_REGTOKEN_H", + "header": "Schema\/RegToken.h", + "include": [ + "Cytoplasm\/Db.h" + ], + "types": { + "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": "array" + } + }, + "type": "struct" + } + } +} \ No newline at end of file diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 004aad1..9a90146 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -48,6 +48,11 @@ we want a way to give those breaking revisions new endpoints. 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. +- 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/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 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/RegToken.c b/src/RegToken.c index 2a45d2d..fa041d3 100644 --- a/src/RegToken.c +++ b/src/RegToken.c @@ -30,8 +30,10 @@ #include #include #include -#include #include +#include + +#include int RegTokenValid(RegTokenInfo * token) @@ -99,8 +101,7 @@ RegTokenDelete(RegTokenInfo * token) { return 0; } - Free(token->name); - Free(token->owner); + RegTokenInfoFree(token); Free(token); return 1; } @@ -113,6 +114,8 @@ RegTokenGetInfo(Db * db, char *token) DbRef *tokenRef; HashMap *tokenJson; + char *errp = NULL; + if (!RegTokenExists(db, token)) { return NULL; @@ -126,47 +129,43 @@ 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); } } int RegTokenClose(RegTokenInfo * tokeninfo) { + HashMap *json; + if (!tokeninfo) { return 0; } + /* Write object to database. */ + json = RegTokenInfoToJson(tokeninfo); + DbJsonSet(tokeninfo->ref, json); /* Copies json into internal structure. */ + JsonFree(json); + return DbUnlock(tokeninfo->db, tokeninfo->ref); } static int @@ -202,7 +201,6 @@ RegTokenInfo * RegTokenCreate(Db * db, char *name, char *owner, UInt64 expires, Int64 uses, int privileges) { RegTokenInfo *ret; - HashMap *tokenJson; UInt64 timestamp = UtilServerTs(); @@ -235,26 +233,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.c b/src/Routes.c index d0d455b..353d5a4 100644 --- a/src/Routes.c +++ b/src/Routes.c @@ -87,6 +87,8 @@ RouterBuild(void) R("/_telodendria/admin/v1/privileges", RoutePrivileges); R("/_telodendria/admin/v1/privileges/(.*)", RoutePrivileges); R("/_telodendria/admin/v1/deactivate/(.*)", RouteAdminDeactivate); + R("/_telodendria/admin/v1/tokens/(.*)", RouteAdminTokens); + R("/_telodendria/admin/v1/tokens", RouteAdminTokens); #undef R diff --git a/src/Routes/RouteAdminTokens.c b/src/Routes/RouteAdminTokens.c new file mode 100644 index 0000000..968d98f --- /dev/null +++ b/src/Routes/RouteAdminTokens.c @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2022-2023 Jordan Bancino <@jordan:bancino.net> + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include + +#include +#include +#include +#include + +#include +#include + +ROUTE_IMPL(RouteAdminTokens, path, argp) +{ + RouteArgs *args = argp; + HashMap *request = NULL; + HashMap *response = NULL; + + char *token; + char *msg; + + Db *db = args->matrixArgs->db; + + User *user = NULL; + + HttpRequestMethod method = HttpRequestMethodGet(args->context); + + Array *tokensarray; + Array *tokens; + + RegTokenInfo *info; + + RegTokenInfo *req; + + size_t i; + + if (method != HTTP_GET && method != HTTP_POST && method != HTTP_DELETE) + { + msg = "Route only supports GET, POST, and DELETE"; + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + return MatrixErrorCreate(M_UNRECOGNIZED, msg); + } + + + response = MatrixGetAccessToken(args->context, &token); + if (response) + { + goto finish; + } + + user = UserAuthenticate(db, token); + if (!user) + { + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_UNKNOWN_TOKEN, NULL); + goto finish; + } + + if (!(UserGetPrivileges(user) & USER_ISSUE_TOKENS)) + { + msg = "User doesn't have the ISSUE_TOKENS privilege."; + HttpResponseStatus(args->context, HTTP_FORBIDDEN); + response = MatrixErrorCreate(M_FORBIDDEN, msg); + goto finish; + } + + switch (method) + { + case HTTP_GET: + if (ArraySize(path) == 0) + { + tokensarray = ArrayCreate(); + + /* Get all registration tokens */ + tokens = DbList(db, 2, "tokens", "registration"); + + response = HashMapCreate(); + + for (i = 0; i < ArraySize(tokens); i++) + { + char *tokenname = ArrayGet(tokens, i); + HashMap *jsoninfo; + + info = RegTokenGetInfo(db, tokenname); + jsoninfo = RegTokenInfoToJson(info); + + RegTokenClose(info); + RegTokenFree(info); + + ArrayAdd(tokensarray, JsonValueObject(jsoninfo)); + } + + JsonSet(response, JsonValueArray(tokensarray), 1, "tokens"); + + DbListFree(tokens); + break; + } + + info = RegTokenGetInfo(db, ArrayGet(path, 0)); + if (!info) + { + msg = "Token doesn't exist."; + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_INVALID_PARAM, msg); + goto finish; + } + + response = RegTokenInfoToJson(info); + + RegTokenClose(info); + RegTokenFree(info); + break; + case HTTP_POST: + request = JsonDecode(HttpServerStream(args->context)); + if (!request) + { + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_NOT_JSON, NULL); + goto finish; + } + + req = Malloc(sizeof(RegTokenInfo)); + memset(req, 0, sizeof(RegTokenInfo)); + + if (!RegTokenInfoFromJson(request, req, &msg)) + { + RegTokenInfoFree(req); + Free(req); + + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_BAD_JSON, msg); + goto finish; + + } + + if (!req->name) + { + req->name = StrRandom(16); + } + + /* 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); + RegTokenInfoFree(req); + Free(req); + + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + msg = "Cannot create token."; + response = MatrixErrorCreate(M_INVALID_PARAM, msg); + goto finish; + } + + response = RegTokenInfoToJson(info); + + RegTokenClose(info); + RegTokenFree(info); + RegTokenInfoFree(req); + Free(req); + break; + case HTTP_DELETE: + if (ArraySize(path) == 0) + { + msg = "No registration token given to DELETE /tokens/[token]."; + HttpResponseStatus(args->context, HTTP_BAD_REQUEST); + response = MatrixErrorCreate(M_INVALID_PARAM, msg); + goto finish; + } + info = RegTokenGetInfo(db, ArrayGet(path, 0)); + RegTokenDelete(info); + + response = HashMapCreate(); + break; + default: + /* Should not be possible. */ + break; + } +finish: + UserUnlock(user); + JsonFree(request); + return response; +} 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 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..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,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 f6cd13b..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 @@ -132,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/Routes.h b/src/include/Routes.h index f791d78..084daf8 100644 --- a/src/include/Routes.h +++ b/src/include/Routes.h @@ -105,6 +105,8 @@ ROUTE(RouteRoomAliases); ROUTE(RouteAdminDeactivate); +ROUTE(RouteAdminTokens); + #undef ROUTE #endif 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