Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: admin APIs for token authenticated registration #3101

Merged
merged 28 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
80ceb18
CreateNewToken API: Initial Changes
santhoshivan23 Jun 3, 2023
6205ffb
refactoring
santhoshivan23 Jun 4, 2023
f5039be
refactoring
santhoshivan23 Jun 4, 2023
2c339a6
refactoring, implement db layer
santhoshivan23 Jun 5, 2023
6cd6af1
refactoring
santhoshivan23 Jun 5, 2023
fe2464f
Move DB Layer to UserAPI
santhoshivan23 Jun 6, 2023
8972c4b
format admin.go
santhoshivan23 Jun 6, 2023
99fa964
handle cases when request field is not present
santhoshivan23 Jun 6, 2023
4b73df5
Implement ListTokens
santhoshivan23 Jun 7, 2023
356edeb
Merge branch 'main' into token-registration
santhoshivan23 Jun 7, 2023
86d2aa4
implement filter by valid query param
santhoshivan23 Jun 8, 2023
31f3125
Get and Delete APIs
santhoshivan23 Jun 8, 2023
5e0da6a
implement update api
santhoshivan23 Jun 10, 2023
19fc59d
Merge branch 'main' into token-registration
santhoshivan23 Jun 12, 2023
f6dbc84
added sqlite support
santhoshivan23 Jun 12, 2023
44beddc
remove unused code
santhoshivan23 Jun 12, 2023
6ea96a0
addressed review comments
santhoshivan23 Jun 14, 2023
8c3a773
Merge branch 'main' into token-registration
santhoshivan23 Jun 14, 2023
9b83df2
add back dendrite
santhoshivan23 Jun 15, 2023
36d13b1
Merge branch 'main' into token-registration
santhoshivan23 Jun 15, 2023
0990429
added test cases
santhoshivan23 Jun 17, 2023
2c5a56b
Merge branch 'main' into token-registration
santhoshivan23 Jun 19, 2023
c5bb8f6
addressed review comments
santhoshivan23 Jun 21, 2023
f52fd59
Merge branch 'token-registration' of github.com:santhoshivan23/dendri…
santhoshivan23 Jun 21, 2023
dbc3b47
Merge branch 'main' into token-registration
santhoshivan23 Jun 21, 2023
2fcc16f
type safety
santhoshivan23 Jun 21, 2023
5346ce7
addressed review comments
santhoshivan23 Jun 21, 2023
04ca62a
lint and refactor
santhoshivan23 Jun 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
638 changes: 638 additions & 0 deletions clientapi/admin_test.go

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions clientapi/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,11 @@ type ExtraPublicRoomsProvider interface {
// Rooms returns the extra rooms. This is called on-demand by clients, so cache appropriately.
Rooms() []fclient.PublicRoom
}

type RegistrationToken struct {
Token *string `json:"token"`
UsesAllowed *int32 `json:"uses_allowed"`
Pending *int32 `json:"pending"`
Completed *int32 `json:"completed"`
ExpiryTime *int64 `json:"expiry_time"`
}
242 changes: 242 additions & 0 deletions clientapi/routing/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"errors"
"fmt"
"net/http"
"regexp"
"strconv"
"time"

"github.com/gorilla/mux"
Expand All @@ -16,14 +18,254 @@ import (
"github.com/matrix-org/util"
"github.com/nats-io/nats.go"
"github.com/sirupsen/logrus"
"golang.org/x/exp/constraints"

clientapi "github.com/matrix-org/dendrite/clientapi/api"
"github.com/matrix-org/dendrite/internal/httputil"
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/dendrite/setup/jetstream"
"github.com/matrix-org/dendrite/userapi/api"
userapi "github.com/matrix-org/dendrite/userapi/api"
)

var validRegistrationTokenRegex = regexp.MustCompile("^[[:ascii:][:digit:]_]*$")

func AdminCreateNewRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
if !cfg.RegistrationRequiresToken {
return util.JSONResponse{
Code: http.StatusForbidden,
JSON: spec.Forbidden("Registration via tokens is not enabled on this homeserver"),
}
}
request := struct {
Token string `json:"token"`
UsesAllowed *int32 `json:"uses_allowed,omitempty"`
ExpiryTime *int64 `json:"expiry_time,omitempty"`
Length int32 `json:"length"`
}{}

if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON(fmt.Sprintf("Failed to decode request body: %s", err)),
}
}

token := request.Token
usesAllowed := request.UsesAllowed
expiryTime := request.ExpiryTime
length := request.Length

if len(token) == 0 {
if length == 0 {
// length not provided in request. Assign default value of 16.
length = 16
}
// token not present in request body. Hence, generate a random token.
if length <= 0 || length > 64 {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("length must be greater than zero and not greater than 64"),
}
}
token = util.RandomString(int(length))
}

if len(token) > 64 {
//Token present in request body, but is too long.
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("token must not be longer than 64"),
}
}

isTokenValid := validRegistrationTokenRegex.Match([]byte(token))
if !isTokenValid {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("token must consist only of characters matched by the regex [A-Za-z0-9-_]"),
}
}
// At this point, we have a valid token, either through request body or through random generation.
if usesAllowed != nil && *usesAllowed < 0 {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("uses_allowed must be a non-negative integer or null"),
}
}
if expiryTime != nil && spec.Timestamp(*expiryTime).Time().Before(time.Now()) {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("expiry_time must not be in the past"),
}
}
pending := int32(0)
completed := int32(0)
// If usesAllowed or expiryTime is 0, it means they are not present in the request. NULL (indicating unlimited uses / no expiration will be persisted in DB)
registrationToken := &clientapi.RegistrationToken{
Token: &token,
UsesAllowed: usesAllowed,
Pending: &pending,
Completed: &completed,
ExpiryTime: expiryTime,
}
created, err := userAPI.PerformAdminCreateRegistrationToken(req.Context(), registrationToken)
if !created {
return util.JSONResponse{
Code: http.StatusConflict,
JSON: map[string]string{
"error": fmt.Sprintf("token: %s already exists", token),
},
}
}
if err != nil {
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: err,
}
}
return util.JSONResponse{
Code: 200,
JSON: map[string]interface{}{
"token": token,
"uses_allowed": getReturnValue(usesAllowed),
"pending": pending,
"completed": completed,
"expiry_time": getReturnValue(expiryTime),
},
}
}

func getReturnValue[t constraints.Integer](in *t) any {
if in == nil {
return nil
}
return *in
}

func AdminListRegistrationTokens(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
queryParams := req.URL.Query()
returnAll := true
valid := true
validQuery, ok := queryParams["valid"]
if ok {
returnAll = false
validValue, err := strconv.ParseBool(validQuery[0])
if err != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("invalid 'valid' query parameter"),
}
}
valid = validValue
}
tokens, err := userAPI.PerformAdminListRegistrationTokens(req.Context(), returnAll, valid)
if err != nil {
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.ErrorUnknown,
}
}
return util.JSONResponse{
Code: 200,
JSON: map[string]interface{}{
"registration_tokens": tokens,
},
}
}

func AdminGetRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
if err != nil {
return util.ErrorResponse(err)
}
tokenText := vars["token"]
token, err := userAPI.PerformAdminGetRegistrationToken(req.Context(), tokenText)
if err != nil {
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: spec.NotFound(fmt.Sprintf("token: %s not found", tokenText)),
}
}
return util.JSONResponse{
Code: 200,
JSON: token,
}
}

func AdminDeleteRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
if err != nil {
return util.ErrorResponse(err)
}
tokenText := vars["token"]
err = userAPI.PerformAdminDeleteRegistrationToken(req.Context(), tokenText)
if err != nil {
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: err,
}
}
return util.JSONResponse{
Code: 200,
JSON: map[string]interface{}{},
}
}

func AdminUpdateRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
if err != nil {
return util.ErrorResponse(err)
}
tokenText := vars["token"]
request := make(map[string]*int64)
if err = json.NewDecoder(req.Body).Decode(&request); err != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON(fmt.Sprintf("Failed to decode request body: %s", err)),
}
}
newAttributes := make(map[string]interface{})
usesAllowed, ok := request["uses_allowed"]
if ok {
// Only add usesAllowed to newAtrributes if it is present and valid
if !(usesAllowed == nil || *usesAllowed >= 0) {
santhoshivan23 marked this conversation as resolved.
Show resolved Hide resolved
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("uses_allowed must be a non-negative integer or null"),
}
}
newAttributes["usesAllowed"] = usesAllowed
}
expiryTime, ok := request["expiry_time"]
if ok {
// Only add expiryTime to newAtrributes if it is present and valid
if !(expiryTime == nil || *expiryTime > time.Now().UnixNano()/int64(time.Millisecond)) {
santhoshivan23 marked this conversation as resolved.
Show resolved Hide resolved
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("expiry_time must not be in the past"),
}
}
newAttributes["expiryTime"] = expiryTime
}
if len(newAttributes) == 0 {
// No attributes to update. Return existing token
return AdminGetRegistrationToken(req, cfg, userAPI)
}
updatedToken, err := userAPI.PerformAdminUpdateRegistrationToken(req.Context(), tokenText, newAttributes)
if err != nil {
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: spec.NotFound(fmt.Sprintf("token: %s not found", tokenText)),
}
}
return util.JSONResponse{
Code: 200,
JSON: *updatedToken,
}
}

func AdminEvacuateRoom(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
if err != nil {
Expand Down
30 changes: 30 additions & 0 deletions clientapi/routing/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,36 @@ func Setup(
}),
).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)
}
dendriteAdminRouter.Handle("/admin/registrationTokens/new",
httputil.MakeAdminAPI("admin_registration_tokens_new", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
return AdminCreateNewRegistrationToken(req, cfg, userAPI)
}),
).Methods(http.MethodPost, http.MethodOptions)

dendriteAdminRouter.Handle("/admin/registrationTokens",
httputil.MakeAdminAPI("admin_list_registration_tokens", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
return AdminListRegistrationTokens(req, cfg, userAPI)
}),
).Methods(http.MethodGet, http.MethodOptions)

dendriteAdminRouter.Handle("/admin/registrationTokens/{token}",
httputil.MakeAdminAPI("admin_get_registration_token", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
switch req.Method {
case http.MethodGet:
return AdminGetRegistrationToken(req, cfg, userAPI)
case http.MethodPut:
return AdminUpdateRegistrationToken(req, cfg, userAPI)
case http.MethodDelete:
return AdminDeleteRegistrationToken(req, cfg, userAPI)
default:
return util.MatrixErrorResponse(
404,
string(spec.ErrorNotFound),
"unknown method",
)
}
}),
).Methods(http.MethodGet, http.MethodPut, http.MethodDelete, http.MethodOptions)

dendriteAdminRouter.Handle("/admin/evacuateRoom/{roomID}",
httputil.MakeAdminAPI("admin_evacuate_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
Expand Down
5 changes: 5 additions & 0 deletions setup/config/config_clientapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ type ClientAPI struct {
// secrets)
RegistrationDisabled bool `yaml:"registration_disabled"`

// If set, requires users to submit a token during registration.
// Tokens can be managed using admin API.
RegistrationRequiresToken bool `yaml:"registration_requires_token"`

// Enable registration without captcha verification or shared secret.
// This option is populated by the -really-enable-open-registration
// command line parameter as it is not recommended.
Expand Down Expand Up @@ -56,6 +60,7 @@ type ClientAPI struct {

func (c *ClientAPI) Defaults(opts DefaultOpts) {
c.RegistrationSharedSecret = ""
c.RegistrationRequiresToken = false
c.RecaptchaPublicKey = ""
c.RecaptchaPrivateKey = ""
c.RecaptchaEnabled = false
Expand Down
6 changes: 6 additions & 0 deletions userapi/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/matrix-org/gomatrixserverlib/fclient"
"github.com/matrix-org/gomatrixserverlib/spec"

clientapi "github.com/matrix-org/dendrite/clientapi/api"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/internal/pushrules"
)
Expand Down Expand Up @@ -94,6 +95,11 @@ type ClientUserAPI interface {
QueryPushers(ctx context.Context, req *QueryPushersRequest, res *QueryPushersResponse) error
QueryPushRules(ctx context.Context, userID string) (*pushrules.AccountRuleSets, error)
QueryAccountAvailability(ctx context.Context, req *QueryAccountAvailabilityRequest, res *QueryAccountAvailabilityResponse) error
PerformAdminCreateRegistrationToken(ctx context.Context, registrationToken *clientapi.RegistrationToken) (bool, error)
PerformAdminListRegistrationTokens(ctx context.Context, returnAll bool, valid bool) ([]clientapi.RegistrationToken, error)
PerformAdminGetRegistrationToken(ctx context.Context, tokenString string) (*clientapi.RegistrationToken, error)
PerformAdminDeleteRegistrationToken(ctx context.Context, tokenString string) error
PerformAdminUpdateRegistrationToken(ctx context.Context, tokenString string, newAttributes map[string]interface{}) (*clientapi.RegistrationToken, error)
PerformAccountCreation(ctx context.Context, req *PerformAccountCreationRequest, res *PerformAccountCreationResponse) error
PerformDeviceCreation(ctx context.Context, req *PerformDeviceCreationRequest, res *PerformDeviceCreationResponse) error
PerformDeviceUpdate(ctx context.Context, req *PerformDeviceUpdateRequest, res *PerformDeviceUpdateResponse) error
Expand Down
32 changes: 32 additions & 0 deletions userapi/internal/user_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt"

clientapi "github.com/matrix-org/dendrite/clientapi/api"
"github.com/matrix-org/dendrite/clientapi/userutil"
"github.com/matrix-org/dendrite/internal/eventutil"
"github.com/matrix-org/dendrite/internal/pushgateway"
Expand Down Expand Up @@ -63,6 +64,37 @@ type UserInternalAPI struct {
Updater *DeviceListUpdater
}

func (a *UserInternalAPI) PerformAdminCreateRegistrationToken(ctx context.Context, registrationToken *clientapi.RegistrationToken) (bool, error) {
exists, err := a.DB.RegistrationTokenExists(ctx, *registrationToken.Token)
if err != nil {
return false, err
}
if exists {
return false, fmt.Errorf("token: %s already exists", *registrationToken.Token)
}
_, err = a.DB.InsertRegistrationToken(ctx, registrationToken)
if err != nil {
return false, fmt.Errorf("Error creating token: %s"+err.Error(), *registrationToken.Token)
}
return true, nil
}

func (a *UserInternalAPI) PerformAdminListRegistrationTokens(ctx context.Context, returnAll bool, valid bool) ([]clientapi.RegistrationToken, error) {
return a.DB.ListRegistrationTokens(ctx, returnAll, valid)
}

func (a *UserInternalAPI) PerformAdminGetRegistrationToken(ctx context.Context, tokenString string) (*clientapi.RegistrationToken, error) {
return a.DB.GetRegistrationToken(ctx, tokenString)
}

func (a *UserInternalAPI) PerformAdminDeleteRegistrationToken(ctx context.Context, tokenString string) error {
return a.DB.DeleteRegistrationToken(ctx, tokenString)
}

func (a *UserInternalAPI) PerformAdminUpdateRegistrationToken(ctx context.Context, tokenString string, newAttributes map[string]interface{}) (*clientapi.RegistrationToken, error) {
return a.DB.UpdateRegistrationToken(ctx, tokenString, newAttributes)
}

func (a *UserInternalAPI) InputAccountData(ctx context.Context, req *api.InputAccountDataRequest, res *api.InputAccountDataResponse) error {
local, domain, err := gomatrixserverlib.SplitID('@', req.UserID)
if err != nil {
Expand Down
Loading
Loading