Convert RegTokenInfo to a j2s Schema. #1

Closed
jordan wants to merge 14 commits from Telodendria:pull-37 into implement-26
16 changed files with 306 additions and 222 deletions

View File

@ -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"
}

View File

@ -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"
}
}

49
Schema/RegToken.json Normal file
View File

@ -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"
}
}
}

View File

@ -29,13 +29,18 @@ 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
- Added a **PUT** option to `/_telodendria/admin/v1/config` that gives the ability to change
only a subset of the configuration.
- 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

View File

@ -19,6 +19,7 @@ request.
- [Configuration](config.md)
- [Server Statistics](stats.md)
- [Process Control](proc.md)
- [Registration Tokens](tokens.md)
## API Conventions

View File

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

106
docs/user/admin/tokens.md Normal file
View File

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

View File

@ -128,10 +128,6 @@ start:
httpServers = NULL;
restart = 0;
/* For getopt() */
opterr = 1;
optind = 1;
/* Local variables */
exit = EXIT_SUCCESS;
flags = 0;

View File

@ -30,10 +30,10 @@
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Util.h>
#include <Cytoplasm/Str.h>
#include <User.h>
#include <Cytoplasm/Int64.h>
#include <Cytoplasm/Log.h>
#include <Schema/AdminToken.h>
#include <User.h>
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,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
@ -200,43 +197,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 +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;
}

View File

@ -28,7 +28,6 @@
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Memory.h>
#include <Schema/AdminToken.h>
#include <RegToken.h>
#include <User.h>
@ -39,8 +38,6 @@ ROUTE_IMPL(RouteAdminTokens, path, argp)
HashMap *response = NULL;
char *token;
char *username;
char *name;
char *msg;
Db *db = args->matrixArgs->db;
@ -54,13 +51,10 @@ ROUTE_IMPL(RouteAdminTokens, path, argp)
RegTokenInfo *info;
TokenRequest *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";
@ -109,11 +103,12 @@ ROUTE_IMPL(RouteAdminTokens, path, argp)
HashMap *jsoninfo;
info = RegTokenGetInfo(db, tokenname);
jsoninfo = RegTokenJSON(info);
jsoninfo = RegTokenInfoToJson(info);
RegTokenClose(info);
RegTokenFree(info);
/* TODO: The JsonValue returned here gets leaked. */
ArrayAdd(tokensarray, JsonValueObject(jsoninfo));
}
@ -122,6 +117,7 @@ ROUTE_IMPL(RouteAdminTokens, path, argp)
DbListFree(tokens);
break;
}
info = RegTokenGetInfo(db, ArrayGet(path, 0));
if (!info)
{
@ -131,7 +127,7 @@ ROUTE_IMPL(RouteAdminTokens, path, argp)
goto finish;
}
response = RegTokenJSON(info);
response = RegTokenInfoToJson(info);
RegTokenClose(info);
RegTokenFree(info);
@ -144,14 +140,13 @@ ROUTE_IMPL(RouteAdminTokens, path, argp)
response = MatrixErrorCreate(M_NOT_JSON, NULL);
goto finish;
}
req = Malloc(sizeof(TokenRequest));
req->max_uses = Int64Neg(Int64Create(0, 1));
req->lifetime = Int64Create(0, 0);
req->name = NULL;
if (!TokenRequestFromJson(request, req, &msg))
req = Malloc(sizeof(RegTokenInfo));
memset(req, 0, sizeof(RegTokenInfo));
if (!RegTokenInfoFromJson(request, req, &msg))
{
TokenRequestFree(req);
RegTokenInfoFree(req);
Free(req);
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
@ -165,29 +160,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);
TokenRequestFree(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 = RegTokenJSON(info);
response = RegTokenInfoToJson(info);
RegTokenClose(info);
RegTokenFree(info);
TokenRequestFree(req);
RegTokenInfoFree(req);
Free(req);
break;
case HTTP_DELETE:
@ -200,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:

View File

@ -39,6 +39,7 @@ ROUTE_IMPL(RouteConfig, path, argp)
HashMap *request = NULL;
Config *newConf;
HashMap *newJson = NULL;
(void) path;
@ -121,10 +122,56 @@ 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);
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);
JsonFree(newJson);
break;
default:
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
response = MatrixErrorCreate(M_UNRECOGNIZED, NULL);
response = MatrixErrorCreate(M_UNRECOGNIZED, "Unknown request method.");
break;
}

View File

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

View File

@ -276,7 +276,7 @@ ROUTE_IMPL(RouteRegister, path, argp)
if (info)
{
UserSetPrivileges(user, info->grants);
UserSetPrivileges(user, UserDecodePrivileges(info->grants));
RegTokenClose(info);
RegTokenFree(info);
}

View File

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

View File

@ -42,54 +42,7 @@
#include <Cytoplasm/Db.h>
#include <Cytoplasm/Int64.h>
/**
* 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 <Schema/RegToken.h>
/**
* ``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

View File

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