Add AllowCreatingDeletingPlayers

This commit is contained in:
Evan Goode 2025-03-29 18:29:56 -04:00
parent d946c07ed8
commit a5c06f8827
8 changed files with 104 additions and 83 deletions

View File

@ -17,6 +17,7 @@ func TestAPI(t *testing.T) {
ts := &TestSuite{} ts := &TestSuite{}
config := testConfig() config := testConfig()
config.AllowCreatingDeletingPlayers = true
config.RegistrationExistingPlayer.Allow = false config.RegistrationExistingPlayer.Allow = false
config.DefaultAdmins = []string{"admin"} config.DefaultAdmins = []string{"admin"}
ts.Setup(config) ts.Setup(config)

104
config.go
View File

@ -93,47 +93,48 @@ type importExistingPlayerConfig struct {
} }
type Config struct { type Config struct {
AllowCapes bool AllowCapes bool
AllowChangingPlayerName bool AllowChangingPlayerName bool
AllowMultipleAccessTokens bool AllowMultipleAccessTokens bool
AllowPasswordLogin bool AllowPasswordLogin bool
AllowSkins bool AllowSkins bool
AllowTextureFromURL bool AllowTextureFromURL bool
ApplicationOwner string AllowCreatingDeletingPlayers bool
ApplicationName string ApplicationOwner string
BaseURL string ApplicationName string
BodyLimit bodyLimitConfig BaseURL string
CORSAllowOrigins []string BodyLimit bodyLimitConfig
CreateNewPlayer createNewPlayerConfig CORSAllowOrigins []string
DataDirectory string CreateNewPlayer createNewPlayerConfig
DefaultAdmins []string DataDirectory string
DefaultPreferredLanguage string DefaultAdmins []string
DefaultMaxPlayerCount int DefaultPreferredLanguage string
Domain string DefaultMaxPlayerCount int
EnableBackgroundEffect bool Domain string
EnableFooter bool EnableBackgroundEffect bool
EnableWebFrontEnd bool EnableFooter bool
FallbackAPIServers []FallbackAPIServer EnableWebFrontEnd bool
ForwardSkins bool FallbackAPIServers []FallbackAPIServer
InstanceName string ForwardSkins bool
ImportExistingPlayer importExistingPlayerConfig InstanceName string
ListenAddress string ImportExistingPlayer importExistingPlayerConfig
LogRequests bool ListenAddress string
MinPasswordLength int LogRequests bool
RegistrationOIDC []RegistrationOIDCConfig MinPasswordLength int
PreMigrationBackups bool RegistrationOIDC []RegistrationOIDCConfig
RateLimit rateLimitConfig PreMigrationBackups bool
RegistrationExistingPlayer registrationExistingPlayerConfig RateLimit rateLimitConfig
RegistrationNewPlayer registrationNewPlayerConfig RegistrationExistingPlayer registrationExistingPlayerConfig
RequestCache ristretto.Config RegistrationNewPlayer registrationNewPlayerConfig
SignPublicKeys bool RequestCache ristretto.Config
SkinSizeLimit int SignPublicKeys bool
OfflineSkins bool SkinSizeLimit int
StateDirectory string OfflineSkins bool
TokenExpireSec int StateDirectory string
TokenStaleSec int TokenExpireSec int
TransientUsers transientUsersConfig TokenStaleSec int
ValidPlayerNameRegex string TransientUsers transientUsersConfig
ValidPlayerNameRegex string
} }
var defaultRateLimitConfig = rateLimitConfig{ var defaultRateLimitConfig = rateLimitConfig{
@ -147,16 +148,17 @@ var defaultBodyLimitConfig = bodyLimitConfig{
func DefaultConfig() Config { func DefaultConfig() Config {
return Config{ return Config{
AllowCapes: true, AllowCapes: true,
AllowChangingPlayerName: true, AllowChangingPlayerName: true,
AllowPasswordLogin: true, AllowPasswordLogin: true,
AllowSkins: true, AllowSkins: true,
AllowTextureFromURL: false, AllowTextureFromURL: false,
ApplicationName: "Drasl", AllowCreatingDeletingPlayers: false,
ApplicationOwner: "Anonymous", ApplicationName: "Drasl",
BaseURL: "", ApplicationOwner: "Anonymous",
BodyLimit: defaultBodyLimitConfig, BaseURL: "",
CORSAllowOrigins: []string{}, BodyLimit: defaultBodyLimitConfig,
CORSAllowOrigins: []string{},
CreateNewPlayer: createNewPlayerConfig{ CreateNewPlayer: createNewPlayerConfig{
Allow: true, Allow: true,
AllowChoosingUUID: false, AllowChoosingUUID: false,

View File

@ -19,7 +19,7 @@ Other available options:
- `DataDirectory`: directory where Drasl's static assets are installed. String. Default value: `"/usr/share/drasl"`. - `DataDirectory`: directory where Drasl's static assets are installed. String. Default value: `"/usr/share/drasl"`.
- `ListenAddress`: IP address and port to listen on. Depending on how you configure your reverse proxy and whether you run Drasl in a container, you should consider setting the listen address to `"127.0.0.1:25585"` to ensure Drasl is only accessible through the reverse proxy. If your reverse proxy is unable to connect to Drasl, try setting this back to the default value. String. Default value: `"0.0.0.0:25585"`. - `ListenAddress`: IP address and port to listen on. Depending on how you configure your reverse proxy and whether you run Drasl in a container, you should consider setting the listen address to `"127.0.0.1:25585"` to ensure Drasl is only accessible through the reverse proxy. If your reverse proxy is unable to connect to Drasl, try setting this back to the default value. String. Default value: `"0.0.0.0:25585"`.
- `DefaultAdmins`: Usernames of the instance's permanent admins. Admin rights can be granted to other accounts using the web UI, but admins defined via `DefaultAdmins` cannot be demoted unless they are removed from the config file. Array of strings. Default value: `[]`. - `DefaultAdmins`: Usernames of the instance's permanent admins. Admin rights can be granted to other accounts using the web UI, but admins defined via `DefaultAdmins` cannot be demoted unless they are removed from the config file. Array of strings. Default value: `[]`.
- `DefaultMaxPlayerCount`: Number of players each user is allowed to own by default. Admins can increase or decrease each user's individual limit. Use `-1` to allow creating an unlimited number of players. Integer. Default value: `1`. - `DefaultMaxPlayerCount`: Number of players each user is allowed to own by default. Admins can increase or decrease each user's individual limit. Use `-1` to allow creating an unlimited number of players. Has no effect unless `AllowCreatingDeletingPlayers` is `true`. Integer. Default value: `1`.
- `PreMigrationBackups`: Back up the database to `/path/to/StateDirectory/drasl.X.YYYY-mm-ddTHH-MM-SSZ.db` (where `X` is the old database version) before migrating to a new database version. Boolean. Default value: `true`. - `PreMigrationBackups`: Back up the database to `/path/to/StateDirectory/drasl.X.YYYY-mm-ddTHH-MM-SSZ.db` (where `X` is the old database version) before migrating to a new database version. Boolean. Default value: `true`.
- `EnableBackgroundEffect`: Whether to enable the 3D background animation in the web UI. Boolean. Default value: `true`. - `EnableBackgroundEffect`: Whether to enable the 3D background animation in the web UI. Boolean. Default value: `true`.
- `EnableFooter`: Whether to enable the page footer in the web UI. Boolean. Default value: `true`. - `EnableFooter`: Whether to enable the page footer in the web UI. Boolean. Default value: `true`.
@ -110,7 +110,8 @@ Other available options:
- `TokenStaleSec`: number of seconds after which an access token will go "stale". A stale token needs to be refreshed before it can be used to log in to a Minecraft server. By default, `TokenStaleSec` is set to `0`, meaning tokens will never go stale, and you should never see an error in-game like "Failed to login: Invalid session (Try restarting your game)". To have tokens go stale after one day, for example, set this option to `86400`. Integer. Default value: `0`. - `TokenStaleSec`: number of seconds after which an access token will go "stale". A stale token needs to be refreshed before it can be used to log in to a Minecraft server. By default, `TokenStaleSec` is set to `0`, meaning tokens will never go stale, and you should never see an error in-game like "Failed to login: Invalid session (Try restarting your game)". To have tokens go stale after one day, for example, set this option to `86400`. Integer. Default value: `0`.
- `TokenExpireSec`: number of seconds after which an access token will expire. An expired token can neither be refreshed nor be used to log in to a Minecraft server. By default, `TokenExpireSec` is set to `0`, meaning tokens will never expire, and you should never have to log in again to your launcher if you've been away for a while. The security risks of non-expiring JWTs are actually quite mild; an attacker would still need access to a client's system to steal a token. But if you're concerned about security, you might, for example, set this option to `604800` to have tokens expire after one week. Integer. Default value: `0`. - `TokenExpireSec`: number of seconds after which an access token will expire. An expired token can neither be refreshed nor be used to log in to a Minecraft server. By default, `TokenExpireSec` is set to `0`, meaning tokens will never expire, and you should never have to log in again to your launcher if you've been away for a while. The security risks of non-expiring JWTs are actually quite mild; an attacker would still need access to a client's system to steal a token. But if you're concerned about security, you might, for example, set this option to `604800` to have tokens expire after one week. Integer. Default value: `0`.
- `AllowPasswordLogin`: Allow registration and login with passwords. Disable to force users to register via OIDC (see `[[RegistrationOIDC]]`). If disabled, users must use Minecraft Tokens to log in to Minecraft launchers. If this option is disabled after being previously enabled, password accounts will still have the option to link an OIDC provider to their account. Boolean. Default value: `true`. - `AllowPasswordLogin`: Allow registration and login with passwords. Disable to force users to register via OIDC (see `[[RegistrationOIDC]]`). If disabled, users must use Minecraft Tokens to log in to Minecraft launchers. If this option is disabled after being previously enabled, password accounts will still have the option to link an OIDC provider to their account. Boolean. Default value: `true`.
- `AllowChangingPlayerName`: Allow users to change their "player name" after their account has already been created. Could be useful in conjunction with `RegistrationExistingPlayer` if you want to make users register from an existing (e.g. Mojang) account but you want them to be able to choose a new player name. Admins can change the name of any player regardless of this setting. Boolean. Default value: `true`. - `AllowCreatingDeletingPlayers`: Allow users to create and delete players up to their individual max player count. The default max player count is controlled by `DefaultMaxPlayerCount`. If this option is disabled, users will only be allowed the one player that is created for them when they register. Admins can create and delete players regardless of this setting. Boolean. Default value: `false`.
- `AllowChangingPlayerName`: Allow users to change their "player name" after their account has already been created. Could be useful in conjunction with `RegistrationExistingPlayer` if you want to make users register from an existing (e.g. Mojang) account but you want them to be able to choose a new player name. Admins can change the name of any player regardless of this setting. Beware: when `AllowCreatingDeletingPlayers` is `true`, users can simply delete a player and create a new one with a new name. Boolean. Default value: `true`.
- `AllowSkins`: Allow users to upload skins. You may want to disable this option if you want to rely exclusively on `ForwardSkins`, e.g. to fully support Vanilla clients. Admins can set skins regardless of this setting. Boolean. Default value: `true`. - `AllowSkins`: Allow users to upload skins. You may want to disable this option if you want to rely exclusively on `ForwardSkins`, e.g. to fully support Vanilla clients. Admins can set skins regardless of this setting. Boolean. Default value: `true`.
- `AllowCapes`: Allow users to upload capes. Admins can set capes regardless of this setting. Boolean. Default value: `true`. - `AllowCapes`: Allow users to upload capes. Admins can set capes regardless of this setting. Boolean. Default value: `true`.
- `AllowTextureFromURL`: Allow users to specify a skin or cape by providing a URL to the texture file. Previously, this option was always allowed; now it is opt-in. Admins can do this regardless of this setting. Boolean. Default value: `false`. - `AllowTextureFromURL`: Allow users to specify a skin or cape by providing a URL to the texture file. Previously, this option was always allowed; now it is opt-in. Admins can do this regardless of this setting. Boolean. Default value: `false`.

View File

@ -13,7 +13,8 @@ Major changes:
New configuration options: New configuration options:
- `AllowPasswordLogin`: Allow registration and login with passwords. Disable to force users to register via OIDC (see `[[RegistrationOIDC]]`). If disabled, users must use Minecraft Tokens to log in to Minecraft launchers. If this option is disabled after being previously enabled, password accounts will still have the option to link an OIDC provider to their account. Boolean. Default value: `true`. - `AllowPasswordLogin`: Allow registration and login with passwords. Disable to force users to register via OIDC (see `[[RegistrationOIDC]]`). If disabled, users must use Minecraft Tokens to log in to Minecraft launchers. If this option is disabled after being previously enabled, password accounts will still have the option to link an OIDC provider to their account. Boolean. Default value: `true`.
- `DefaultMaxPlayerCount`: Number of players each user is allowed to own by default. Admins can increase or decrease each user's individual limit. Use `-1` to allow creating an unlimited number of players. Integer. Default value: `1`. - `AllowCreatingDeletingPlayers`: Allow users to create and delete players up to their individual max player count. The default max player count is controlled by `DefaultMaxPlayerCount`. If this option is disabled, users will only be allowed the one player that is created for them when they register. Admins can create and delete players regardless of this setting. Boolean. Default value: `false`.
- `DefaultMaxPlayerCount`: Number of players each user is allowed to own by default. Admins can increase or decrease each user's individual limit. Use `-1` to allow creating an unlimited number of players. Has no effect unless `AllowCreatingDeletingPlayers` is set to `true`. Integer. Default value: `1`.
- `CORSAllowOrigins`: List of origins that may access Drasl API routes. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin. Necessary for allowing browsers to access the Drasl API. Set to `["*"]` to allow all origins. Array of strings. Example value: `["https://front-end.example.com"]`. Default value: `[]`. - `CORSAllowOrigins`: List of origins that may access Drasl API routes. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin. Necessary for allowing browsers to access the Drasl API. Set to `["*"]` to allow all origins. Array of strings. Example value: `["https://front-end.example.com"]`. Default value: `[]`.
- `EnableWebFrontEnd`: Whether to enable the web UI. Boolean. Default value: `true`. - `EnableWebFrontEnd`: Whether to enable the web UI. Boolean. Default value: `true`.
- `PreMigrationBackups`: Back up the database to `/path/to/StateDirectory/drasl.X.YYYY-mm-ddTHH-MM-SSZ.db` (where `X` is the old database version) before migrating to a new database version. Boolean. Default value: `true`. - `PreMigrationBackups`: Back up the database to `/path/to/StateDirectory/drasl.X.YYYY-mm-ddTHH-MM-SSZ.db` (where `X` is the old database version) before migrating to a new database version. Boolean. Default value: `true`.

View File

@ -31,6 +31,7 @@ func setupRegistrationExistingPlayerTS(t *testing.T, requireSkinVerification boo
ts.SetupAux(auxConfig) ts.SetupAux(auxConfig)
config := testConfig() config := testConfig()
config.AllowCreatingDeletingPlayers = true
config.RegistrationNewPlayer.Allow = false config.RegistrationNewPlayer.Allow = false
config.RegistrationExistingPlayer = registrationExistingPlayerConfig{ config.RegistrationExistingPlayer = registrationExistingPlayerConfig{
Allow: true, Allow: true,
@ -206,6 +207,7 @@ func TestFront(t *testing.T) {
ts := &TestSuite{} ts := &TestSuite{}
config := testConfig() config := testConfig()
config.AllowCreatingDeletingPlayers = true
config.RegistrationNewPlayer.AllowChoosingUUID = true config.RegistrationNewPlayer.AllowChoosingUUID = true
ts.Setup(config) ts.Setup(config)
defer ts.Teardown() defer ts.Teardown()

View File

@ -102,6 +102,10 @@ func (app *App) CreatePlayer(
return Player{}, err return Player{}, err
} }
if !app.Config.AllowCreatingDeletingPlayers && !callerIsAdmin {
return Player{}, NewBadRequestUserError("You are not allowed to create new players.")
}
maxPlayerCount := app.GetMaxPlayerCount(&user) maxPlayerCount := app.GetMaxPlayerCount(&user)
if maxPlayerCount != Constants.MaxPlayerCountUnlimited && len(user.Players) >= maxPlayerCount && !callerIsAdmin { if maxPlayerCount != Constants.MaxPlayerCountUnlimited && len(user.Players) >= maxPlayerCount && !callerIsAdmin {
return Player{}, NewBadRequestUserError("You are only allowed to own %d player(s).", maxPlayerCount) return Player{}, NewBadRequestUserError("You are only allowed to own %d player(s).", maxPlayerCount)
@ -582,6 +586,10 @@ func (app *App) InvalidateUser(db *gorm.DB, user *User) error {
} }
func (app *App) DeletePlayer(caller *User, player *Player) error { func (app *App) DeletePlayer(caller *User, player *Player) error {
if !app.Config.AllowCreatingDeletingPlayers && !caller.IsAdmin {
return NewUserError(http.StatusForbidden, "You are not allowed to delete players.")
}
if caller.UUID != player.UserUUID && !caller.IsAdmin { if caller.UUID != player.UserUUID && !caller.IsAdmin {
return NewUserError(http.StatusForbidden, "You don't own that player.") return NewUserError(http.StatusForbidden, "You don't own that player.")
} }

View File

@ -151,28 +151,30 @@
<input type="submit" value="Save changes" /> <input type="submit" value="Save changes" />
</p> </p>
</form> </form>
<p> {{ if or .App.Config.AllowCreatingDeletingPlayers .User.IsAdmin }}
<details> <p>
<summary>Delete Player</summary> <details>
<form <summary>Delete Player</summary>
action="{{ .App.FrontEndURL }}/web/delete-player" <form
method="post" action="{{ .App.FrontEndURL }}/web/delete-player"
onsubmit="return confirm('Are you sure? This action is irreversible.');" method="post"
> onsubmit="return confirm('Are you sure? This action is irreversible.');"
<input hidden name="uuid" value="{{ .Player.UUID }}" /> >
<input <input hidden name="uuid" value="{{ .Player.UUID }}" />
hidden <input
name="returnUrl" hidden
value="{{ if .AdminView }} name="returnUrl"
{{ .App.FrontEndURL }}/web/user/{{ .PlayerUser.UUID }} value="{{ if .AdminView }}
{{ else }} {{ .App.FrontEndURL }}/web/user/{{ .PlayerUser.UUID }}
{{ .App.FrontEndURL }}/web/user {{ else }}
{{ end }}" {{ .App.FrontEndURL }}/web/user
/> {{ end }}"
<input type="submit" value="Delete Player" /> />
</form> <input type="submit" value="Delete Player" />
</details> </form>
</p> </details>
</p>
{{ end }}
{{ if .SkinURL }} {{ if .SkinURL }}
<script type="module"> <script type="module">

View File

@ -44,7 +44,9 @@
<tr> <tr>
<td colspan="2">Player</td> <td colspan="2">Player</td>
<td>UUID</td> <td>UUID</td>
<td>Delete Player</td> {{ if or .App.Config.AllowCreatingDeletingPlayers .User.IsAdmin }}
<td>Delete Player</td>
{{ end }}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -67,9 +69,11 @@
> >
</td> </td>
<td>{{ $player.UUID }}</td> <td>{{ $player.UUID }}</td>
<td> {{ if or $.App.Config.AllowCreatingDeletingPlayers $.User.IsAdmin }}
<input type="submit" form="delete-{{ $player.UUID }}" value="Delete" /> <td>
</td> <input type="submit" form="delete-{{ $player.UUID }}" value="Delete" />
</td>
{{ end }}
</tr> </tr>
{{ end }} {{ end }}
</tbody> </tbody>
@ -78,8 +82,8 @@
No players yet. No players yet.
{{ end }} {{ end }}
<p> <p>
{{ if (eq .MaxPlayerCount 0) }} {{ if or (and (not .User.IsAdmin) (not .App.Config.AllowCreatingDeletingPlayers)) (eq .MaxPlayerCount 0) }}
{{ if .AdminView }}{{ .TargetUser.Username }} is{{ else }}You are{{ end }} not allowed to create players. {{ if .AdminView }}{{ .TargetUser.Username }} is{{ else }}You are{{ end }} not allowed to create new players.
{{ if .AdminView }}You can override this limit since you're an admin.{{ end }} {{ if .AdminView }}You can override this limit since you're an admin.{{ end }}
{{ else if (gt .MaxPlayerCount 0) }} {{ else if (gt .MaxPlayerCount 0) }}
{{ if .AdminView }}{{ .TargetUser.Username }}'s{{ else }}Your{{ end }} account can have up to {{ .MaxPlayerCount }} player(s). {{ if .AdminView }}{{ .TargetUser.Username }}'s{{ else }}Your{{ end }} account can have up to {{ .MaxPlayerCount }} player(s).
@ -88,7 +92,7 @@
{{ if .AdminView }}{{ .TargetUser.Username }}'s{{ else }}Your{{ end }} account can have an unlimited number of players. {{ if .AdminView }}{{ .TargetUser.Username }}'s{{ else }}Your{{ end }} account can have an unlimited number of players.
{{ end }} {{ end }}
</p> </p>
{{ if or (lt (len .TargetUser.Players) .MaxPlayerCount) (lt .MaxPlayerCount 0) .AdminView }} {{ if or (and .App.Config.AllowCreatingDeletingPlayers (or (lt (len .TargetUser.Players) .MaxPlayerCount) (lt .MaxPlayerCount 0))) .AdminView }}
{{ if .App.Config.RegistrationNewPlayer.Allow }} {{ if .App.Config.RegistrationNewPlayer.Allow }}
{{ if or .User.IsAdmin .App.Config.RegistrationNewPlayer.AllowChoosingUUID }} {{ if or .User.IsAdmin .App.Config.RegistrationNewPlayer.AllowChoosingUUID }}
<h4>Create a new player</h4> <h4>Create a new player</h4>