-
Notifications
You must be signed in to change notification settings - Fork 54
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
Add the ability to add and remove users from the webui #307
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice to have an API to create users. This may get extended later, for testing purposes (both integration and load testing).
Perhaps this would be worth mentioning quickly in the testing guide as well.
I'm more concerned about the delete user button. There's a confirmation popup, but that's still a big irrevocable action and I'm not super sure we want that.
Perhaps someone with more experience using the admin website can comment on this @StanczakDominik (or ping someone else)?
I think without the delete and my suggested changes that would be a nice addition. Not even two days ago I ran into trouble trying to create some random local accounts to test something, and that would have helped.
end) | ||
|
||
# And now the users | ||
query = "DELETE FROM account_users WHERE id = ANY($1)" | ||
Ecto.Adapters.SQL.query!(Repo, query, [id_list]) | ||
Ecto.Adapters.SQL.query!(Repo, query, [int_id_list]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have problems with the delete query raising errors as some tables didn't exist in my setup (e.g. telemetry). I therefore switched to Ecto.Adapters.SQL.query instead of Ecto.Adapters.SQL.query! to not raise errors.
You should ensure your setup is correct first. There are some guide in the repo, and you might be interested in postgres setup
I have the telemetry tables locally and I didn't recall doing anything fancy to get them.
The main problem with swapping query!/3
for query/3
is that it doesn't raise an error and returns the exception as a value, but that doesn't fix the underlying problem.
If anything, one thing to fix here would be to wrap all the deletes inside a transaction, but that's irrelevant for your changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did recreate my setup with the guide once again to make sure I did not miss anything.
However, the some columns still do not exist.
I observed that all telemetry_\*_event_types
only have ["id", "name"]
.
That makes sense, considering they only define the event types, not when events happen to users.
The telemetry_\*_events
contain the user_id
, therefore I think this was originally meant.
I also changed some queries, because the columns are named differently.
I don't have any problem using query!
now
To look at the columns I used something like this:
[
"SELECT * FROM moderation_actions"
]
|> Enum.each(fn query ->
Ecto.Adapters.SQL.query!(Repo, query) |> IO.inspect()
end)
"DELETE FROM moderation_actions WHERE target_id = ANY($1)" | ||
"DELETE FROM moderation_reports WHERE reporter_id = ANY($1)", | ||
"DELETE FROM moderation_reports WHERE target_id = ANY($1)", | ||
"DELETE FROM moderation_actions WHERE responder_id = ANY($1)" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 good catch updating these missing relations.
(params["name"] || "" == "") | ||
|
||
passwordfn = fn -> if is_nil(params["password"]) or String.trim(params["password"]) == "" do "password" else params["password"] end end | ||
emailfn = fn -> if is_nil(params["email"]) or String.trim(params["email"]) == "" do UUID.uuid1() else params["email"] end end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you're going to want to setup the formatting tool, either mix format
or your IDE.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be done now
user_params = %{ | ||
"name" => params["name"], | ||
"password" => passwordfn.(), | ||
"email" => emailfn.(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason you're invoking the lambdas you just created above instead of getting values directly?
I'm thinking of
password = if ... do: "password", else: ... end
lib/teiserver_web/router.ex
Outdated
@@ -561,6 +561,8 @@ defmodule TeiserverWeb.Router do | |||
|
|||
# User stuff | |||
put("/users/gdpr_clean/:id", UserController, :gdpr_clean) | |||
get("/users/delete_user/:id", UserController, :delete_user) | |||
put("/users/delete_user/:id", UserController, :delete_user) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The routing is roughly REST-ish, so it would be better to use the delete
function (and associated http verb) to delete the user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be correct now
end | ||
|
||
@spec create_post(Plug.Conn.t(), map) :: Plug.Conn.t() | ||
def create_post(conn, params \\ %{}) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should add a guard to these functions first, that checks the user has indeed the correct permissions to perform that. Currently, the only check is done in the template rendering, but that is trivially bypassable.
Something along the lines of:
def create_post(conn, _) when not Teiserver.Account.Authlib.allow?(conn, "Server") do
# flash + redirect elsewhere
end
def create_post(conn, params \\ %{}) do
# your function
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This idea did not compile. I used the other approach mentioned further down.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes indeed, because the guards must be macros since they generate special bytecode for dispatch.
|
||
|
||
@spec delete_user(Plug.Conn.t(), map()) :: Plug.Conn.t() | ||
def delete_user(conn, %{"id" => id}) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar comment regarding authentication.
Also, if the given parameters don't have an id
key, the pattern match will fail and you're going to get a server error.
It would be better to add another catch all clause after this one to return back a 400 with a pre-rendered form (optional but nice to have)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a catch case using a put_flash
and a redirect
, hope this checks out
|
||
case Teiserver.Account.UserLib.has_access(user, conn) do | ||
{true, _} -> | ||
Teiserver.manually_delete_user(id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's my biggest problem in this change. manually_delete_user
is a mix task, and is supposed to be invoked through the terminal with the mix command, or some script, but definitely not from application code.
There is Teiserver.Account.delete_user
that is more suitable, although it doesn't remove all the linked rows.
A quick grep shows that it's likely a generated stub and isn't really used. I'd rather have this function taking care of deleting the various entities and then you can call it from there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
manually_delete_user
uses Teiserver.Admin.DeleteUserTask.delete_users
internally, which I now also use.
I could also use Teiserver.Account.delete_user
if that is better.
def delete_user(conn, %{"id" => id}) do | ||
user = Account.get_user_by_id(id) | ||
|
||
case Teiserver.Account.UserLib.has_access(user, conn) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 for the access control here. I'd recommend using that approach or the one I suggested in both functions.
@@ -1,5 +1,6 @@ | |||
<% bsname = view_colour() | |||
is_moderator = allow?(@conn, "Moderator") %> | |||
is_moderator = allow?(@conn, "Moderator") | |||
is_system = allow?(@conn, "System") %> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this permission different than the Server
you used elsewhere? I'm not on top of the permission system, so I may very well have missed something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know what I originally thought here. I switched to is_server
to have it the same way as the other.
In my opinion, this shouldn't be creating users directly and should instead go through the existing |
If you want this feature purely for testing then you could check for a config variable. For example, there are certain endpoints that only are enabled by this flag:
that is set inside |
oh you're right, it's much better there. |
Thanks for all the feedback! I tried it with Teiserver.CacheUser.register_user in a previous version, I also like the Idea of locking add/ delete user behind a flag for now, will check that one out. |
I now use Next, I will look at using |
I now added the guards, changed to using The formatting (done with IdeaI also added a commit, which locks delete and add behind |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can drop the whole configuration bits and put a disclaimer on the templates like "this is for testing/local dev only, proceed with care in production" for both create&update endpoints. That should simplify the code.
The delete endpoint could just be deleted in my opinion.
Also, I'm very sorry for the super late feedback. It slipped my mind and then I was away. Don't hesitate to ping me here or on discord (channel teiserver-spads, I'm blackmelon on discord) if you need further feedback on this one.
lib/teiserver/data/cache_user.ex
Outdated
case EmailHelper.new_user(user) do | ||
{:error, error} -> | ||
Logger.error("Error sending new user email - #{user.email} - #{error}") | ||
case Application.fetch_env(:teiserver, Teiserver.Mailer) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this check should live here. This file deals with users, and shouldn't need to know about the internals of the email system. This logic should be moved inside EmailHelper.new_user
instead. The deletion of the cache key related to the verification code should stay in this file though. You can do that when you get back a no_verify
or perhaps a new atom that could be returned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed that EmailHelper.new_user
already implements some logic to disable the verification in the webui. So I decided to remove my workaround all together.
end | ||
|
||
@spec create_post(Plug.Conn.t(), map) :: Plug.Conn.t() | ||
def create_post(conn, params \\ %{}) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes indeed, because the guards must be macros since they generate special bytecode for dispatch.
conn | ||
|> put_flash(:danger, "Invalid user name") | ||
|> redirect(to: ~p"/teiserver/admin/user") | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This redirect
doesn't actually do anything, because everything is immutable and there is no early return in elixir.
If you want to guard against missing or empty params, the usual way is to have a private function that actually do the action once you checked things like:
if is_nil(params["name"]) do
...
else
do_create_post(conn, params)
end
defp do_create_post(conn, params) do
# do the actual logic here.
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the hint. I now implemented it that way.
"password" | ||
else | ||
params["password"] | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A slightly simpler way to do that is password = Map.get(params, "password", "password")
. Map.get/3 allows you to provide a default value if not present.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have tested it and this works well. In earlier iterations I had problems detecting "" as empty to trigger the default value.
conn | ||
|> put_flash(:danger, "not in testmode") | ||
|> redirect(to: ~p"/teiserver/admin/user") | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar comment regarding this expression having no effect.
lib/teiserver_web/router.ex
Outdated
|
||
if Application.compile_env(:teiserver, Teiserver)[:test_mode] do | ||
delete("/users/delete_user/:id", UserController, :delete_user) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not a big fan of adding more config flags, especially for such a feature. I feel this is better served by permissions.
Since this feature doesn't already exist and hasn't been required (I think) for a time, I would say it's safe to assume we don't need it. If you want to delete users locally, the mix task should be fine.
I personally would prefer not having this delete user feature at all, until we get some feedback from the mods/admins that are using teiserver in anger to add it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I now removed that delete endpoint all together (for now).
lib/teiserver_web/router.ex
Outdated
@@ -617,6 +621,11 @@ defmodule TeiserverWeb.Router do | |||
get("/tools/falist", ToolController, :falist) | |||
get("/tools/test_page", ToolController, :test_page) | |||
|
|||
if Application.compile_env(:teiserver, Teiserver)[:test_mode] do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
compile_env
gets the value at compile time, and it's generally a bad idea. Because you need to recompile the app to change it. Also, this complicate matters when compiling the app from a different machine.
If you want to conditionally have these endpoints + template, a better way is to put that into the config files under config/config.exs
set to false, and inside config/test.exs
config/dev.exs
put that to true.
But, tbh, I'm not sure all of this is warranted, requiring admin/server privilege to be able to create users manually may just be enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have removed that for now. I currently think relying on the admin/ server privilege should be sufficient.
I do agree that the delete feature is not needed. But to be honest even the add user feature is not super useful too since |
Hey there, I plan to include the proposed requested changes in the next weeks. (remove the delete user endpoint, ...) However, I understand if there is no need for this feature, then the PR can probably get closed for now. |
EDIT: Actually I have now a reason to use the add user functionality: I can add my own username and password to the local dev database so I don't have to keep switching users when go from server4 to localhost. |
Adds delete buttons to user list, and to the "user action" drop down menu. Added form for user creation.
Moving user creation to Teiserver.CacheUser.register_user If no mailer defined, unset the verification code Added guards Changed the Router methods to the correct type delete: for deleting the user post: for creating a new user
@@ -40,6 +40,17 @@ | |||
) %> | |||
<% end %> | |||
|
|||
<%= if allow?(@current_user, "admin") do %> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You check for "admin" here...
@@ -210,6 +210,57 @@ defmodule TeiserverWeb.Admin.UserController do | |||
|> render("new.html") | |||
end | |||
|
|||
@spec create_form(Plug.Conn.t(), map) :: Plug.Conn.t() | |||
def create_form(conn, _) do | |||
if allow?(conn, "Server") do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... but "Server" here. I recommend using "Server" in both.
I actually had the need for this recently. I ended up using the internal function in a repl, but I think this would be a nice addition. |
I am thinking about putting it behind dev env check as well, thoughts? |
no strong opinions, bit more complex, but not much. |
Idea
In Addition to managing user by script for testing purposes. I thought it would be easier to also allow creation and deletion of users directly in the webui.
I therefore added a button to the list menu.
The button opens a form where users can be simply generated. Either create a random user or completely/ partially filling form.
In order to delete users again I also added delete buttons to the end of the users in the list.
Deletion can also be done in the user action dropdown menu.
Addition/ Deletion is only visible and usable by accounts with system permission.
(Unfortunately the icons of Fontawesome do only partially work, probably due to the wrong version)
Current problems:
I have problems with the delete query raising errors as some tables didn't exist in my setup (e.g. telemetry). I therefore switched to
Ecto.Adapters.SQL.query
instead ofEcto.Adapters.SQL.query!
to not raise errors.I also split up the deletion from moderation_reports, because the combined query would fail due to certain columns not existing.
This might not be a problem on other setups.
Also deletion of user like the spadsbot user does not work, because the spadsbot creates lobbies and more foreign key dependencies.
There might be some better ways to route to the correct form for user creation.
I would be happy to get some feedback.