diff --git a/.travis.yml b/.travis.yml index 4170a96a..a993fe6b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,15 @@ language: elixir elixir: - - 1.10.2 + - 1.10.4 otp_release: - - 22.1.8 + - 23.0.3 services: - postgresql env: - MIX_ENV=test before_script: + # create .env file on Travis-CI: + - echo "export MIX_ENV=test" > .env - mix ecto.setup script: - mix do deps.get, coveralls.json diff --git a/README.md b/README.md index c0b83831..5b66a9b9 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ you can setup in ***5 minutes***. +![wake-sleeping-heroku-app](https://dwylauth.herokuapp.com/ping) @@ -173,7 +174,6 @@ And for sending emails you will need the `SECRET_KEY_BASE` and `EMAIL_APP_URL` defined. - ### 4. Create and migrate your database: > Ensure that PostgreSQL is running @@ -190,7 +190,7 @@ mix ecto.setup mix phoenix.server ``` -> It may take a couple of minutes to compile the app the first time. ⏳ +> It may take a minute to compile the app the first time. ⏳ Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. diff --git a/elixir_buildpack.config b/elixir_buildpack.config index e650d238..488a1dc3 100644 --- a/elixir_buildpack.config +++ b/elixir_buildpack.config @@ -1,8 +1,8 @@ # Elixir version -elixir_version=1.10 +elixir_version=1.10.4 # Erlang version # available versions https://github.com/HashNuke/heroku-buildpack-elixir-otp-builds/blob/master/otp-versions -erlang_version=22.2.7 +erlang_version=23.0.3 # always_rebuild=true diff --git a/lib/auth/apikey.ex b/lib/auth/apikey.ex index c45d6c6d..ac24d043 100644 --- a/lib/auth/apikey.ex +++ b/lib/auth/apikey.ex @@ -1,4 +1,7 @@ defmodule Auth.Apikey do + @moduledoc """ + Defines apikeys schema and CRUD functions + """ use Ecto.Schema import Ecto.Query, warn: false import Ecto.Changeset @@ -9,57 +12,79 @@ defmodule Auth.Apikey do schema "apikeys" do field :client_secret, :binary field :client_id, :binary - field :description, :string - field :name, :string - field :url, :binary field :person_id, :id field :status, :id + belongs_to :app, Auth.App timestamps() end - @doc false - def changeset(apikey, attrs) do - apikey - |> cast(attrs, [:client_id, :client_secret, :name, :description, :url, :person_id]) - |> validate_required([:client_secret]) + @doc """ + `encrypt_encode/1` does exactly what it's name suggests, + AES Encrypts a string of plaintext and then Base58 encodes it. + We encode it using Base58 so it's human-friendly (readable). + """ + def encrypt_encode(plaintext) do + plaintext |> Fields.AES.encrypt() |> Base58.encode() end - def change_apikey(%Apikey{} = apikey) do - Apikey.changeset(apikey, %{}) + @doc """ + `create_api_key/1` uses the `encrypt_encode/1` to create an API Key + that is just two strings joined with a forwardslash ("/"). + This allows us to use a *single* environment variable. + """ + def create_api_key(id) do + encrypt_encode(id) <> "/" <> encrypt_encode(id) end - def create_apikey(attrs \\ %{}) do - %Apikey{} - |> Apikey.changeset(attrs) - |> Repo.insert() + @doc """ + `decode_decrypt/1` accepts a `key` and attempts to Base58.decode + followed by AES.decrypt it. If decode or decrypt fails, return 0 (zero). + """ + def decode_decrypt(key) do + try do + key |> Base58.decode() |> Fields.AES.decrypt() |> String.to_integer() + rescue + ArgumentError -> + 0 + + ArithmeticError -> + 0 + end end - def list_apikeys_for_person(person_id) do - query = - from( - a in __MODULE__, - where: a.person_id == ^person_id - ) - - Repo.all(query) + def decrypt_api_key(key) do + key |> String.split("/") |> List.first() |> decode_decrypt() end - @doc """ - Gets a single apikey. - - Raises `Ecto.NoResultsError` if the Apikey does not exist. - - ## Examples + def changeset(apikey, attrs) do + apikey + |> cast(attrs, [:client_id, :client_secret, :status, :person_id]) + |> put_assoc(:app, Map.get(attrs, "app")) + end - iex> get_apikey!(123) - %Apikey{} + def create_apikey(app) do + attrs = %{ + "client_secret" => encrypt_encode(app.id), + "client_id" => encrypt_encode(app.id), + "person_id" => app.person_id, + "status" => 3, + "app" => app + } - iex> get_apikey!(456) - ** (Ecto.NoResultsError) + %Apikey{} + |> Apikey.changeset(attrs) + |> Repo.insert() + end - """ - def get_apikey!(id), do: Repo.get!(__MODULE__, id) + def get_apikey_by_app_id(app_id) do + from( + a in __MODULE__, + where: a.app_id == ^app_id + ) + |> Repo.one() + |> Repo.preload(:app) + end @doc """ Updates a apikey. @@ -78,20 +103,4 @@ defmodule Auth.Apikey do |> changeset(attrs) |> Repo.update() end - - @doc """ - Deletes a apikey. - - ## Examples - - iex> delete_apikey(apikey) - {:ok, %Apikey{}} - - iex> delete_apikey(apikey) - {:error, %Ecto.Changeset{}} - - """ - def delete_apikey(%Apikey{} = apikey) do - Repo.delete(apikey) - end end diff --git a/lib/auth/app.ex b/lib/auth/app.ex new file mode 100644 index 00000000..17b7ca92 --- /dev/null +++ b/lib/auth/app.ex @@ -0,0 +1,155 @@ +defmodule Auth.App do + @moduledoc """ + Schema and helper functions for creating/managing Apps. + """ + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query, warn: false + alias Auth.Repo + # https://stackoverflow.com/a/47501059/1148249 + alias __MODULE__ + + schema "apps" do + field :desc, :binary + field :end, :naive_datetime + field :name, :binary + field :url, :binary + field :person_id, :id + field :status, :id + has_many :apikeys, Auth.Apikey + + timestamps() + end + + @doc false + def changeset(app, attrs) do + app + |> cast(attrs, [:name, :desc, :url, :end, :person_id, :status]) + |> validate_required([:name, :url]) + end + + @doc """ + Returns the list of apps. + + ## Examples + + iex> list_apps() + [%App{}, ...] + + """ + def list_apps do + Repo.all(App) + end + + # Returning all apps when person_id == 1 (superadmin) means + #  the superadmin can always see/manage all apps as necessary. + # Later we could refactor this function to use RBAC.has_role_any/2. + def list_apps(conn) when is_map(conn) do + case conn.assigns.person.id == 1 do + true -> Auth.App.list_apps() + false -> Auth.App.list_apps(conn.assigns.person.id) + end + end + + def list_apps(person_id) do + App + |> where([a], a.status != 6 and a.person_id == ^person_id) + |> Repo.all() + end + + @doc """ + Gets a single app. + + Raises `Ecto.NoResultsError` if the App does not exist. + + ## Examples + + iex> get_app!(123) + %App{} + + iex> get_app!(456) + ** (Ecto.NoResultsError) + + """ + def get_app!(id) do + App + |> where([a], a.id == ^id and a.status != 6) + |> Repo.one() + |> Repo.preload(:apikeys) + end + + @doc """ + Creates a app. + + ## Examples + + iex> create_app(%{field: value}) + {:ok, %App{}} + + iex> create_app(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_app(attrs \\ %{}) do + case %App{} |> App.changeset(attrs) |> Repo.insert() do + {:ok, app} -> + # Create API Key for App https://github.com/dwyl/auth/issues/97 + Auth.Apikey.create_apikey(app) + + # return the App with the API Key preloaded: + {:ok, get_app!(app.id)} + + {:error, err} -> + {:error, err} + end + end + + @doc """ + Updates a app. + + ## Examples + + iex> update_app(app, %{field: new_value}) + {:ok, %App{}} + + iex> update_app(app, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_app(%App{} = app, attrs) do + app + # |> IO.inspect(label: "update_app/2:109") + |> App.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a app. + + ## Examples + + iex> delete_app(app) + {:ok, %App{}} + + iex> delete_app(app) + {:error, %Ecto.Changeset{}} + + """ + def delete_app(%App{} = app) do + # "soft delete" for autiting purposes: + update_app(app, %{status: 6}) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking app changes. + + ## Examples + + iex> change_app(app) + %Ecto.Changeset{data: %App{}} + + """ + def change_app(%App{} = app, attrs \\ %{}) do + App.changeset(app, attrs) + end +end diff --git a/lib/auth/login_log.ex b/lib/auth/login_log.ex index cf9eca1a..bc13bb13 100644 --- a/lib/auth/login_log.ex +++ b/lib/auth/login_log.ex @@ -1,4 +1,7 @@ defmodule Auth.LoginLog do + @moduledoc """ + Defines login_logs schema and CRUD functions + """ use Ecto.Schema import Ecto.Changeset alias Auth.Repo diff --git a/lib/auth/people_roles.ex b/lib/auth/people_roles.ex new file mode 100644 index 00000000..8e6bd7d3 --- /dev/null +++ b/lib/auth/people_roles.ex @@ -0,0 +1,95 @@ +defmodule Auth.PeopleRoles do + @moduledoc """ + Defines people_roles schema and fuction to grant roles to a person. + """ + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query + alias Auth.Repo + # https://stackoverflow.com/a/47501059/1148249 + alias __MODULE__ + + schema "people_roles" do + belongs_to :person, Auth.Person + belongs_to :role, Auth.Role + field :granter_id, :integer + field :revoked, :utc_datetime + field :revoker_id, :integer + + timestamps() + end + + @doc """ + list_people_roles/0 returns the list of people_roles with all people data. + This is useful for displaying the data in a admin overview table. + """ + def list_people_roles do + Repo.all(from pr in __MODULE__, preload: [:person, :role]) + end + + @doc """ + get_record/2 returns the record where the person was granted a role. + """ + def get_record(person_id, role_id) do + Repo.one( + from(pr in __MODULE__, + where: pr.person_id == ^person_id and pr.role_id == ^role_id, + preload: [:person, :role] + ) + ) + end + + @doc """ + get_by_id!/1 returns the record with the given people_roles.id. + """ + def get_by_id(id) do + Repo.one( + from(pr in __MODULE__, + where: pr.id == ^id, + preload: [:person, :role] + ) + ) + end + + @doc """ + get_roles_for_person/1 returns the list of roles for a given person.id + """ + def get_roles_for_person(person_id) do + Repo.all( + from(pr in __MODULE__, + where: pr.person_id == ^person_id, + preload: [:role] + ) + ) + end + + @doc """ + insert/3 grants a role to the given person + granter_id is the id of the person (admin) granting the role + grantee_id is the person.id of the person being granted the role + role_id is the role.id (int, e.g: 4) of th role being granted. + """ + def insert(granter_id, grantee_id, role_id) do + %PeopleRoles{} + |> cast(%{granter_id: granter_id}, [:granter_id]) + |> put_assoc(:person, Auth.Person.get_person_by_id(grantee_id)) + |> put_assoc(:role, Auth.Role.get_role!(role_id)) + |> Repo.insert() + end + + @doc """ + revoke/3 grants a role to the given person + revoker_id is the id of the person (admin) granting the role + person_id is the person.id of the person being granted the role + role_id is the role.id (int, e.g: 4) of th role being granted. + """ + def revoke(revoker_id, people_roles_id) do + # get the people_role record that needs to be updated (revoked) + get_by_id(people_roles_id) + |> cast( + %{revoker_id: revoker_id, revoked: DateTime.utc_now()}, + [:revoker_id, :revoked] + ) + |> Repo.update() + end +end diff --git a/lib/auth/permission.ex b/lib/auth/permission.ex new file mode 100644 index 00000000..0b01d219 --- /dev/null +++ b/lib/auth/permission.ex @@ -0,0 +1,120 @@ +defmodule Auth.Permission do + @moduledoc """ + Defines permissions schema and CRUD functions + """ + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query, warn: false + alias Auth.Repo + # alias the Struct so we can use it below + alias Auth.Permission + + schema "permissions" do + field :desc, :string + field :name, :string + field :person_id, :id + + timestamps() + end + + @doc false + def changeset(permission, attrs) do + permission + |> cast(attrs, [:name, :desc]) + |> validate_required([:name, :desc]) + end + + @doc """ + Returns the list of permissions. + + ## Examples + + iex> list_permissions() + [%Permission{}, ...] + + """ + def list_permissions do + Repo.all(__MODULE__) + end + + @doc """ + Gets a single permission. + + Raises `Ecto.NoResultsError` if the Permission does not exist. + + ## Examples + + iex> get_permission!(123) + %Permission{} + + iex> get_permission!(456) + ** (Ecto.NoResultsError) + + """ + def get_permission!(id), do: Repo.get!(__MODULE__, id) + + @doc """ + Creates a permission. + + ## Examples + + iex> create_permission(%{field: value}) + {:ok, %Permission{}} + + iex> create_permission(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_permission(attrs \\ %{}) do + %Permission{} + |> Permission.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a permission. + + ## Examples + + iex> update_permission(permission, %{field: new_value}) + {:ok, %Permission{}} + + iex> update_permission(permission, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_permission(%Permission{} = permission, attrs) do + permission + |> Permission.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a permission. + + ## Examples + + iex> delete_permission(permission) + {:ok, %Permission{}} + + iex> delete_permission(permission) + {:error, %Ecto.Changeset{}} + + """ + def delete_permission(%Permission{} = permission) do + Repo.delete(permission) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking permission changes. + + ## Examples + + iex> change_permission(permission) + %Ecto.Changeset{data: %Permission{}} + + """ + def change_permission(%Permission{} = permission, attrs \\ %{}) do + Permission.changeset(permission, attrs) + end +end diff --git a/lib/auth/person.ex b/lib/auth/person.ex index 04878b54..a48adff1 100644 --- a/lib/auth/person.ex +++ b/lib/auth/person.ex @@ -1,6 +1,10 @@ defmodule Auth.Person do + @moduledoc """ + Defines Person schema and CRUD functions + """ use Ecto.Schema import Ecto.Changeset + import Ecto.Query alias Auth.Repo # https://stackoverflow.com/a/47501059/1148249 alias __MODULE__ @@ -20,6 +24,10 @@ defmodule Auth.Person do field :status, :id field :tag, :id field :key_id, :integer + field :app_id, :integer + # many_to_many :roles, Auth.Role, join_through: "people_roles" + # has_many :roles, through: [:people_roles, :role] + many_to_many :roles, Auth.Role, join_through: Auth.PeopleRoles has_many :statuses, Auth.Status # has_many :sessions, Auth.Session, on_delete: :delete_all @@ -30,6 +38,9 @@ defmodule Auth.Person do Default attributes validation for Person """ def changeset(person, attrs) do + # IO.inspect(person, label: "changeset > person") + # IO.inspect(attrs, label: "changeset > attrs") + # IO.inspect(roles, label: "changeset > roles") person |> cast(attrs, [ :id, @@ -44,7 +55,8 @@ defmodule Auth.Person do :picture, :username, :auth_provider, - :status + :status, + :app_id ]) |> validate_required([:email]) |> put_email_hash() @@ -56,6 +68,10 @@ defmodule Auth.Person do %Person{} |> changeset(person) |> put_email_status_verified() + # assign default role of "subscriber": + |> put_assoc(:roles, [Auth.Role.get_role!(6)]) + + # other roles can be assigned in the UI case get_person_by_email(person.changes.email) do nil -> @@ -111,7 +127,7 @@ defmodule Auth.Person do end def create_github_person(profile) do - transform_github_profile_data_to_person(profile) |> upsert_person() + upsert_person(transform_github_profile_data_to_person(profile)) end @doc """ @@ -154,8 +170,8 @@ defmodule Auth.Person do end def create_google_person(profile) do - transform_google_profile_data_to_person(profile) - |> upsert_person() + person = upsert_person(transform_google_profile_data_to_person(profile)) + Map.replace!(person, :roles, RBAC.transform_role_list_to_string(person.roles)) end # @doc """ @@ -174,7 +190,7 @@ defmodule Auth.Person do end def get_status_verified do - status = Auth.Status.upsert_status("verified") + status = Auth.Status.upsert_status(%{"text" => "verified"}) status.id end @@ -190,12 +206,14 @@ defmodule Auth.Person do def verify_person_by_id(id) do person = get_person_by_id(id) - %{email: person.email, status: get_status_verified()} |> upsert_person() + upsert_person(%{email: person.email, status: get_status_verified()}) end def get_person_by_id(id) do __MODULE__ |> Repo.get_by(id: id) + |> Repo.preload(:roles) + |> Repo.preload(:statuses) end defp put_pass_hash(changeset) do @@ -211,11 +229,19 @@ defmodule Auth.Person do @doc """ `get_person_by_email/1` returns the person based on email address. """ - def get_person_by_email(email) do + def get_person_by_email(email) when not is_nil(email) do __MODULE__ |> Repo.get_by(email_hash: email) + |> Repo.preload([:statuses, :roles]) end + # def get_person_by_email(email) do + # IO.inspect(email, label: "email") + # __MODULE__ + # |> Repo.get_by(email_hash: email) + # |> Repo.preload([:statuses, :roles]) + # end + @doc """ `upsert_person/1` inserts or updates a person record. """ @@ -227,12 +253,9 @@ defmodule Auth.Person do # existing person ep -> merged = Map.merge(AuthPlug.Helpers.strip_struct_metadata(ep), person) - - {:ok, person} = - changeset(%Person{id: ep.id}, merged) - |> Repo.update() - - person + {:ok, person} = Repo.update(changeset(%Person{id: ep.id}, merged)) + # ensure that the preloads are returned: + get_person_by_email(person.email) end end @@ -248,4 +271,41 @@ defmodule Auth.Person do 0 end end + + @doc """ + `list_people/0` lists all people in the system. + Used for displaying the table of authenticated people. + """ + def list_people do + Repo.all(from(pr in __MODULE__, preload: [:roles, :statuses])) + # keeping this query commented here for now in case I decide to use it + # instead of having to call PeopleView.status_string/2 + # query = """ + # SELECT DISTINCT ON (s.status_id, s.person_id) s.id, s.message_id, + # s.updated_at, s.template, st.text as status, s.person_id + # FROM sent s + # JOIN status as st on s.status_id = st.id + # WHERE s.message_id IS NOT NULL + # """ + # {:ok, result} = Repo.query(query) + + # # create List of Maps from the result.rows: + # Enum.map(result.rows, fn([id, mid, iat, t, s, pid]) -> + # # e = Fields.AES.decrypt(e) + # # e = case e !== :error and e =~ "@" do + # # true -> e |> String.split("@") |> List.first + # # false -> e + # # end + # %{ + # id: id, + # message_id: mid, + # updated_at: NaiveDateTime.truncate(iat, :second), + # template: t, + # status: s, + # person_id: pid, + # email: "" + # } + # end) + # |> Enum.sort(&(&1.id > &2.id)) + end end diff --git a/lib/auth/role.ex b/lib/auth/role.ex new file mode 100644 index 00000000..c2532cd3 --- /dev/null +++ b/lib/auth/role.ex @@ -0,0 +1,179 @@ +defmodule Auth.Role do + @moduledoc """ + Defines roles schema and CRUD functions + """ + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query, warn: false + alias Auth.Repo + # https://stackoverflow.com/a/47501059/1148249 + alias __MODULE__ + + schema "roles" do + field :desc, :string + field :name, :string + field :person_id, :id + field :app_id, :id + # many_to_many :roles, Auth.Role, join_through: Auth.PeopleRoles + + timestamps() + end + + @doc false + def changeset(role, attrs) do + role + |> cast(attrs, [:name, :desc, :person_id, :app_id]) + |> validate_required([:name, :desc]) + end + + @doc """ + Returns the list of roles. + + ## Examples + + iex> list_roles() + [%Role{}, ...] + + """ + def list_roles do + Repo.all(__MODULE__) + end + + def list_roles_for_app(app_id) do + __MODULE__ + # and a.status != 6) + |> where([r], r.app_id == ^app_id or is_nil(r.app_id)) + |> Repo.all() + end + + @doc """ + get all roles for apps owned + default roles + """ + def list_roles_for_apps(app_ids) do + __MODULE__ + # and r.status != 6 + |> where([r], r.app_id in ^app_ids or is_nil(r.app_id)) + |> Repo.all() + end + + @doc """ + Gets a single role. + + Raises `Ecto.NoResultsError` if the Role does not exist. + + ## Examples + + iex> get_role!(123) + %Role{} + + iex> get_role!(456) + ** (Ecto.NoResultsError) + + """ + def get_role!(id), do: Repo.get!(__MODULE__, id) + + def get_role!(id, person_id) do + # IO.inspect(id, label: "id") + # IO.inspect(person_id, label: "person_id") + __MODULE__ + |> where([r], r.id == ^id and r.person_id == ^person_id) + |> Repo.one() + + # |> IO.inspect() + end + + @doc """ + Creates a role. + + ## Examples + + iex> create_role(%{field: value}) + {:ok, %Role{}} + + iex> create_role(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_role(attrs \\ %{}) do + %Role{} + |> Role.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a role. + + ## Examples + + iex> update_role(role, %{field: new_value}) + {:ok, %Role{}} + + iex> update_role(role, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_role(%Role{} = role, attrs) do + role + |> Role.changeset(attrs) + |> Repo.update() + end + + def upsert_role(role) do + id = Map.get(role, :id) + # if the role Map has no "id" field its not a DB record + if is_nil(id) do + create_role(role) + else + case Repo.get_by(__MODULE__, id: id) do + # record does not exist so create it: + nil -> + create_role(role) + + # record exists, lets update it: + existing_role -> + update_role(existing_role, strip_meta(role)) + end + end + end + + def strip_meta(struct) do + struct + |> Map.delete(:__meta__) + |> Map.delete(:__struct__) + end + + @doc """ + Deletes a role. + + ## Examples + + iex> delete_role(role) + {:ok, %Role{}} + + iex> delete_role(role) + {:error, %Ecto.Changeset{}} + + """ + def delete_role(%Role{} = role) do + Repo.delete(role) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking role changes. + + ## Examples + + iex> change_role(role) + %Ecto.Changeset{data: %Role{}} + + """ + def change_role(%Role{} = role, attrs \\ %{}) do + Role.changeset(role, attrs) + end + + # @doc """ + # grants the default "subscriber" (6) role to the person + # """ + # def set_default_role(person) do + # end +end diff --git a/lib/auth/status.ex b/lib/auth/status.ex index 9228d4dd..c0e11886 100644 --- a/lib/auth/status.ex +++ b/lib/auth/status.ex @@ -4,9 +4,11 @@ defmodule Auth.Status do alias Auth.Repo # https://stackoverflow.com/a/47501059/1148249 alias __MODULE__ + @admin_email System.get_env("ADMIN_EMAIL") schema "status" do field :text, :string + field :desc, :string belongs_to :person, Auth.Person timestamps() @@ -15,27 +17,29 @@ defmodule Auth.Status do @doc false def changeset(status, attrs) do status - |> cast(attrs, [:text]) + |> cast(attrs, [:text, :desc]) |> validate_required([:text]) end - def create_status(text, person) do + def create_status(attrs, person) do %Status{} - |> changeset(%{text: text}) + |> changeset(attrs) |> put_assoc(:person, person) |> Repo.insert!() end - def upsert_status(text) do - case Auth.Repo.get_by(__MODULE__, text: text) do + def upsert_status(attrs) do + case Auth.Repo.get_by(__MODULE__, text: Map.get(attrs, "text")) do # create status nil -> - email = System.get_env("ADMIN_EMAIL") - person = Auth.Person.get_person_by_email(email) - create_status(text, person) + create_status(attrs, Auth.Person.get_person_by_email(@admin_email)) status -> status end end + + def list_statuses do + Repo.all(__MODULE__) + end end diff --git a/lib/auth_web/controllers/apikey_controller.ex b/lib/auth_web/controllers/apikey_controller.ex deleted file mode 100644 index efb9eeb0..00000000 --- a/lib/auth_web/controllers/apikey_controller.ex +++ /dev/null @@ -1,112 +0,0 @@ -defmodule AuthWeb.ApikeyController do - use AuthWeb, :controller - alias Auth.Apikey - - def index(conn, _params) do - person_id = conn.assigns.person.id - apikeys = Apikey.list_apikeys_for_person(person_id) - render(conn, "index.html", apikeys: apikeys) - end - - def new(conn, _params) do - changeset = Apikey.change_apikey(%Apikey{}) - render(conn, "new.html", changeset: changeset) - end - - def encrypt_encode(plaintext) do - Fields.AES.encrypt(plaintext) |> Base58.encode() - end - - def create_api_key(person_id) do - encrypt_encode(person_id) <> "/" <> encrypt_encode(person_id) - end - - @doc """ - `decode_decrypt/1` accepts a `key` and attempts to Base58.decode - followed by AES.decrypt it. If decode or decrypt fails, return 0 (zero). - """ - def decode_decrypt(key) do - try do - key |> Base58.decode() |> Fields.AES.decrypt() |> String.to_integer() - rescue - ArgumentError -> - 0 - end - end - - def decrypt_api_key(key) do - key |> String.split("/") |> List.first() |> decode_decrypt() - end - - def make_apikey(apikey_params, person_id) do - Map.merge(apikey_params, %{ - "client_secret" => encrypt_encode(person_id), - "client_id" => encrypt_encode(person_id), - "person_id" => person_id - }) - end - - def create(conn, %{"apikey" => apikey_params}) do - {:ok, apikey} = - apikey_params - |> make_apikey(conn.assigns.person.id) - |> Apikey.create_apikey() - - conn - |> put_flash(:info, "Apikey created successfully.") - |> redirect(to: Routes.apikey_path(conn, :show, apikey)) - end - - def show(conn, %{"id" => id}) do - apikey = Apikey.get_apikey!(id) - # combined = apikey.client_id <> "/" <> apikey.client_secret - render(conn, "show.html", apikey: apikey) - end - - def edit(conn, %{"id" => id}) do - apikey = Auth.Apikey.get_apikey!(id) - - if apikey.person_id == conn.assigns.person.id do - changeset = Auth.Apikey.change_apikey(apikey) - render(conn, "edit.html", apikey: apikey, changeset: changeset) - else - AuthWeb.AuthController.not_found(conn, "API KEY " <> id <> " not found.") - end - end - - @doc """ - `update/2` updates a given API Key. Checks if the person attempting - to update the key is the "owner" of the key before updating. - """ - def update(conn, %{"id" => id, "apikey" => apikey_params}) do - apikey = Apikey.get_apikey!(id) - # check that the person attempting to update the key owns it! - if apikey.person_id == conn.assigns.person.id do - case Apikey.update_apikey(apikey, apikey_params) do - {:ok, apikey} -> - conn - |> put_flash(:info, "Apikey updated successfully.") - |> redirect(to: Routes.apikey_path(conn, :show, apikey)) - - {:error, %Ecto.Changeset{} = changeset} -> - render(conn, "edit.html", apikey: apikey, changeset: changeset) - end - else - AuthWeb.AuthController.not_found(conn, "API KEY " <> id <> " not found.") - end - end - - def delete(conn, %{"id" => id}) do - apikey = Apikey.get_apikey!(id) - # check that the person attempting to delete the key owns it! - if apikey.person_id == conn.assigns.person.id do - {:ok, _apikey} = Apikey.delete_apikey(apikey) - - conn - |> put_flash(:info, "Apikey deleted successfully.") - |> redirect(to: Routes.apikey_path(conn, :index)) - else - AuthWeb.AuthController.not_found(conn, "API KEY " <> id <> " not found.") - end - end -end diff --git a/lib/auth_web/controllers/app_controller.ex b/lib/auth_web/controllers/app_controller.ex new file mode 100644 index 00000000..af0bbb5a --- /dev/null +++ b/lib/auth_web/controllers/app_controller.ex @@ -0,0 +1,129 @@ +defmodule AuthWeb.AppController do + use AuthWeb, :controller + alias Auth.App + + def index(conn, _params) do + render(conn, "index.html", apps: Auth.App.list_apps(conn)) + end + + def new(conn, _params) do + changeset = App.change_app(%App{}) + render(conn, "new.html", changeset: changeset) + end + + def create(conn, %{"app" => app_params}) do + # IO.inspect(app_params, label: "app_params:16") + attrs = + Map.merge(app_params, %{ + "person_id" => conn.assigns.person.id, + "status" => 3 + }) + + case App.create_app(attrs) do + {:ok, app} -> + # IO.inspect(app, label: "app:23") + conn + |> put_flash(:info, "App created successfully.") + |> redirect(to: Routes.app_path(conn, :show, app)) + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, "new.html", changeset: changeset) + end + end + + def show(conn, %{"id" => id}) do + app = App.get_app!(id) + #  restrict viewership to owner||admin https://github.com/dwyl/auth/issues/99 + if conn.assigns.person.id == app.person_id || conn.assigns.person.id == 1 do + render(conn, "show.html", app: app) + else + AuthWeb.AuthController.not_found(conn, "can't touch this.") + end + end + + def edit(conn, %{"id" => id}) do + # IO.inspect(id, label: "edit id:36") + app = App.get_app!(id) + #  restrict editing to owner||admin https://github.com/dwyl/auth/issues/99 + if conn.assigns.person.id == app.person_id || conn.assigns.person.id == 1 do + changeset = App.change_app(app) + render(conn, "edit.html", app: app, changeset: changeset) + else + AuthWeb.AuthController.not_found(conn, "can't touch this.") + end + end + + def update(conn, %{"id" => id, "app" => app_params}) do + app = App.get_app!(id) + #  restrict updating to owner||admin https://github.com/dwyl/auth/issues/99 + if conn.assigns.person.id == app.person_id || conn.assigns.person.id == 1 do + case App.update_app(app, app_params) do + {:ok, app} -> + conn + |> put_flash(:info, "App updated successfully.") + |> redirect(to: Routes.app_path(conn, :show, app)) + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, "edit.html", app: app, changeset: changeset) + end + else + AuthWeb.AuthController.not_found(conn, "can't touch this.") + end + end + + def delete(conn, %{"id" => id}) do + app = App.get_app!(id) + + if conn.assigns.person.id == app.person_id || conn.assigns.person.id == 1 do + {:ok, _app} = App.delete_app(app) + + conn + |> put_flash(:info, "App deleted successfully.") + |> redirect(to: Routes.app_path(conn, :index)) + else + AuthWeb.AuthController.not_found(conn, "can't touch this.") + end + end + + @doc """ + Reset the API Key in case of suspected compromise. + """ + def resetapikey(conn, %{"id" => id}) do + app = App.get_app!(id) + + if conn.assigns.person.id != app.person_id && conn.assigns.person.id != 1 do + AuthWeb.AuthController.not_found(conn, "can't touch this.") + else + Enum.each(app.apikeys, fn k -> + if k.status == 3 do + # retire the apikey + Auth.Apikey.update_apikey(Map.delete(k, :app), %{status: 6}) + end + end) + + # Create New API Key: + Auth.Apikey.create_apikey(app) + + # get the app again and render it: + conn + |> put_flash(:info, "Your API Key has been successfully reset") + |> render("show.html", app: App.get_app!(id)) + end + end + + @doc """ + approles/2 Return the (JSON) List of Roles for a given App based on apikey.client_id + """ + def approles(conn, %{"client_id" => client_id}) do + app_id = Auth.Apikey.decode_decrypt(client_id) + + # return empty JSON list with 401 status if client_id is invalid + if app_id == 0 or is_nil(app_id) do + AuthWeb.AuthController.unauthorized(conn) + else + roles = Auth.Role.list_roles_for_app(app_id) + roles = Enum.map(roles, fn role -> Auth.Role.strip_meta(role) end) + json(conn, roles) + end + end +end diff --git a/lib/auth_web/controllers/auth_controller.ex b/lib/auth_web/controllers/auth_controller.ex index e0aa83fc..1ad8cd62 100644 --- a/lib/auth_web/controllers/auth_controller.ex +++ b/lib/auth_web/controllers/auth_controller.ex @@ -1,11 +1,14 @@ defmodule AuthWeb.AuthController do + @moduledoc """ + Defines AuthController and all functions for authenticaiton + """ use AuthWeb, :controller + alias Auth.App alias Auth.Person # https://github.com/dwyl/auth/issues/46 def admin(conn, _params) do - conn - |> render(:welcome) + render(conn, :welcome, apps: App.list_apps(conn.assigns.person.id)) end defp get_user_agent_string(conn) do @@ -41,13 +44,6 @@ defmodule AuthWeb.AuthController do nil end - # TODO: add friendly error message when email address is invalid - # errors = if not is_nil(email) and not Fields.Validate.email(email) do - # [email: "email address is invalid"] - # else - # [] - # end - state = if not is_nil(params_person) and not is_nil(Map.get(params_person, "state")) do @@ -115,13 +111,25 @@ defmodule AuthWeb.AuthController do """ def github_handler(conn, %{"code" => code, "state" => state}) do {:ok, profile} = ElixirAuthGithub.github_auth(code) + app_id = get_app_id(state) + # save profile to people: - person = Person.create_github_person(profile) + person = Person.create_github_person(Map.merge(profile, %{app_id: app_id})) # render or redirect: handler(conn, person, state) end + def get_app_id(state) do + client_id = get_client_secret_from_state(state) + app_id = Auth.Apikey.decode_decrypt(client_id) + + case app_id == 0 do + true -> 1 + false -> app_id + end + end + @doc """ `google_handler/2` handles the callback from Google Auth API redirect. """ @@ -129,7 +137,8 @@ defmodule AuthWeb.AuthController do {:ok, token} = ElixirAuthGoogle.get_token(code, conn) {:ok, profile} = ElixirAuthGoogle.get_user_profile(token.access_token) # save profile to people: - person = Person.create_google_person(profile) + app_id = get_app_id(state) + person = Person.create_google_person(Map.merge(profile, %{app_id: app_id})) # render or redirect: handler(conn, person, state) @@ -173,13 +182,15 @@ defmodule AuthWeb.AuthController do # display welcome page on Auth site: false -> + # Grant app_admin role to person who authenticated directly on auth app + # Auth.PeopleRoles.insert(1, person.id, 8) conn |> AuthPlug.create_jwt_session(person) - |> render(:welcome, person: person) + |> render(:welcome, person: person, apps: App.list_apps(person.id)) end end - # TODO: create a human-friendy response + # create a human-friendy response? def unauthorized(conn) do conn |> put_resp_content_type("text/html") @@ -187,7 +198,7 @@ defmodule AuthWeb.AuthController do |> halt() end - # TODO: refactor this to render a template with a nice layout. + # refactor this to render a template with a nice layout? #HelpWanted def not_found(conn, message) do conn |> put_resp_content_type("text/html") @@ -294,7 +305,7 @@ defmodule AuthWeb.AuthController do changeset: Auth.Person.password_new_changeset(%{email: email}), # so we can redirect after creatig a password state: state, - email: AuthWeb.ApikeyController.encrypt_encode(email) + email: Auth.Apikey.encrypt_encode(email) ) end @@ -309,7 +320,7 @@ defmodule AuthWeb.AuthController do def make_verify_link(conn, person, state) do AuthPlug.Helpers.get_baseurl_from_conn(conn) <> "/auth/verify?id=" <> - AuthWeb.ApikeyController.encrypt_encode(person.id) <> + Auth.Apikey.encrypt_encode(person.id) <> "&referer=" <> state end @@ -319,7 +330,7 @@ defmodule AuthWeb.AuthController do # |> render("password_create.html", # changeset: Auth.Person.password_new_changeset(%{email: params["email"]}), # state: params["state"], # so we can redirect after creatig a password - # email: AuthWeb.ApikeyController.encrypt_encode(params["email"]) + # email: AuthWeb.Apikey.encrypt_encode(params["email"]) # ) # end @@ -337,6 +348,8 @@ defmodule AuthWeb.AuthController do if changeset.valid? do person = Auth.Person.upsert_person(%{email: email, password: p["password"]}) + # replace %Auth.Role{} struct with string github.com/dwyl/rbac/issues/4 + person = Map.replace!(person, :roles, RBAC.transform_role_list_to_string(person.roles)) redirect_or_render(conn, person, p["state"]) else conn @@ -370,8 +383,6 @@ defmodule AuthWeb.AuthController do That password is incorrect. """ - # log password incorrect - user_agent = get_user_agent(conn) ip_address = get_ip_address(conn) @@ -389,11 +400,16 @@ defmodule AuthWeb.AuthController do end def verify_email(conn, params) do - id = AuthWeb.ApikeyController.decode_decrypt(params["id"]) + id = Auth.Apikey.decode_decrypt(params["id"]) person = Auth.Person.verify_person_by_id(id) redirect_or_render(conn, person, params["referer"]) end + def get_client_id_from_state(state) do + query = URI.decode_query(List.last(String.split(state, "?"))) + Map.get(query, "auth_client_id") + end + @doc """ `get_client_secret_from_state/1` gets the client_id from state, attempts to decode_decrypt it and then look it up in apikeys @@ -401,8 +417,7 @@ defmodule AuthWeb.AuthController do All other failure conditions return a 0 (zero) which results in a 401. """ def get_client_secret_from_state(state) do - query = URI.decode_query(List.last(String.split(state, "?"))) - client_id = Map.get(query, "auth_client_id") + client_id = get_client_id_from_state(state) case not is_nil(client_id) do # Lookup client_id in apikeys table @@ -416,19 +431,24 @@ defmodule AuthWeb.AuthController do end def get_client_secret(client_id, state) do - person_id = AuthWeb.ApikeyController.decode_decrypt(client_id) - - # decode_decrypt fails with state 0 - if person_id == 0 do + app_id = Auth.Apikey.decode_decrypt(client_id) + # decode_decrypt fails with 0: + if app_id == 0 do 0 else - apikeys = Auth.Apikey.list_apikeys_for_person(person_id) + apikey = Auth.Apikey.get_apikey_by_app_id(app_id) - Enum.filter(apikeys, fn k -> - k.client_id == client_id and state =~ k.url - end) - |> List.first() - |> Map.get(:client_secret) + cond do + apikey.app.person_id == 1 -> + apikey.client_secret + + # all other keys require matching the app url and status to not be "deleted": + apikey.client_id == client_id && state =~ apikey.app.url && apikey.status != 6 -> + apikey.client_secret + + true -> + 0 + end end end @@ -439,7 +459,9 @@ defmodule AuthWeb.AuthController do id: person.id, picture: person.picture, status: person.status, - email: person.email + email: person.email, + roles: RBAC.transform_role_list_to_string(person.roles), + app_id: person.app_id } jwt = AuthPlug.Token.generate_jwt!(data, client_secret) diff --git a/lib/auth_web/controllers/people_controller.ex b/lib/auth_web/controllers/people_controller.ex new file mode 100644 index 00000000..cae13949 --- /dev/null +++ b/lib/auth_web/controllers/people_controller.ex @@ -0,0 +1,45 @@ +defmodule AuthWeb.PeopleController do + @moduledoc """ + Defines People controller functions + """ + use AuthWeb, :controller + + @doc """ + `index/2` lists all the people who have authenticated with the auth app. + """ + def index(conn, _params) do + # should be visible to superadmin and people with "admin" role + if conn.assigns.person.id == 1 do + render(conn, :index, + people: Auth.Person.list_people(), + roles: Auth.Role.list_roles(), + statuses: Auth.Status.list_statuses() + ) + + # Note: this can easily be refactored to save on DB queries. #HelpWanted + else + AuthWeb.AuthController.not_found(conn, "Only admins can see people ... for now!") + end + end + + @doc """ + `show/2` shows the profile of a person with all relevant info. + """ + def show(conn, params) do + # should be visible to superadmin and people with "admin" role + if conn.assigns.person.id == 1 do + person = Auth.Person.get_person_by_id(Map.get(params, "person_id")) + + render(conn, :profile, + person: person, + roles: Auth.PeopleRoles.get_roles_for_person(person.id), + statuses: Auth.Status.list_statuses(), + all_roles: Auth.Role.list_roles() + ) + + # Note: this can easily be refactored to save on DB queries. #HelpWanted + else + AuthWeb.AuthController.unauthorized(conn) + end + end +end diff --git a/lib/auth_web/controllers/permission_controller.ex b/lib/auth_web/controllers/permission_controller.ex new file mode 100644 index 00000000..5b10f276 --- /dev/null +++ b/lib/auth_web/controllers/permission_controller.ex @@ -0,0 +1,61 @@ +defmodule AuthWeb.PermissionController do + use AuthWeb, :controller + + alias Auth.Permission + + def index(conn, _params) do + permissions = Permission.list_permissions() + render(conn, "index.html", permissions: permissions) + end + + def new(conn, _params) do + changeset = Permission.change_permission(%Permission{}) + render(conn, "new.html", changeset: changeset) + end + + def create(conn, %{"permission" => permission_params}) do + case Permission.create_permission(permission_params) do + {:ok, permission} -> + conn + |> put_flash(:info, "Permission created successfully.") + |> redirect(to: Routes.permission_path(conn, :show, permission)) + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, "new.html", changeset: changeset) + end + end + + def show(conn, %{"id" => id}) do + permission = Permission.get_permission!(id) + render(conn, "show.html", permission: permission) + end + + def edit(conn, %{"id" => id}) do + permission = Permission.get_permission!(id) + changeset = Permission.change_permission(permission) + render(conn, "edit.html", permission: permission, changeset: changeset) + end + + def update(conn, %{"id" => id, "permission" => permission_params}) do + permission = Permission.get_permission!(id) + + case Permission.update_permission(permission, permission_params) do + {:ok, permission} -> + conn + |> put_flash(:info, "Permission updated successfully.") + |> redirect(to: Routes.permission_path(conn, :show, permission)) + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, "edit.html", permission: permission, changeset: changeset) + end + end + + def delete(conn, %{"id" => id}) do + permission = Permission.get_permission!(id) + {:ok, _permission} = Permission.delete_permission(permission) + + conn + |> put_flash(:info, "Permission deleted successfully.") + |> redirect(to: Routes.permission_path(conn, :index)) + end +end diff --git a/lib/auth_web/controllers/ping_controller.ex b/lib/auth_web/controllers/ping_controller.ex new file mode 100644 index 00000000..dc6a207f --- /dev/null +++ b/lib/auth_web/controllers/ping_controller.ex @@ -0,0 +1,8 @@ +defmodule AuthWeb.PingController do + use AuthWeb, :controller + + # see: github.com/dwyl/ping + def ping(conn, params) do + Ping.render_pixel(conn, params) + end +end diff --git a/lib/auth_web/controllers/role_controller.ex b/lib/auth_web/controllers/role_controller.ex new file mode 100644 index 00000000..fc5cfe5b --- /dev/null +++ b/lib/auth_web/controllers/role_controller.ex @@ -0,0 +1,177 @@ +defmodule AuthWeb.RoleController do + use AuthWeb, :controller + alias Auth.Role + # import Auth.Plugs.IsOwner + + # plug :is_owner when action in [:index] + + def index(conn, _params) do + # restrict viewing to only roles owned by the person or default roles: + apps = Auth.App.list_apps(conn) + app_ids = Enum.map(apps, fn a -> a.id end) + roles = Role.list_roles_for_apps(app_ids) + render(conn, "index.html", roles: roles) + end + + def new(conn, _params) do + changeset = Role.change_role(%Role{}) + apps = Auth.App.list_apps(conn) + # Roles Ref/Require Apps: https://github.com/dwyl/auth/issues/112 + # Check if the person already has apps: + if length(apps) > 0 do + render(conn, "new.html", changeset: changeset, apps: apps) + else + # No apps, instruct them to create an App before Role(s): + conn + |> put_flash(:info, "Please create an App before attempting to create Roles") + |> redirect(to: Routes.app_path(conn, :new)) + end + end + + def create(conn, %{"role" => role_params}) do + apps = Auth.App.list_apps(conn) + # check that the role_params.app_id is owned by the person: + if person_owns_app?(apps, Map.get(role_params, "app_id")) do + # never allow the request to define the person_id: + create_attrs = Map.merge(role_params, %{"person_id" => conn.assigns.person.id}) + + case Role.create_role(create_attrs) do + {:ok, role} -> + conn + |> put_flash(:info, "Role created successfully.") + |> redirect(to: Routes.role_path(conn, :show, role)) + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, "new.html", changeset: changeset, apps: apps) + end + else + # request is attempting to create a role for an app they don't own ... + changeset = Auth.Role.changeset(%Role{}, role_params) + + conn + |> put_status(:not_found) + |> put_flash(:info, "Please select an app you own.") + |> render("new.html", changeset: changeset, apps: apps) + end + end + + def show(conn, %{"id" => id}) do + role = Role.get_role!(id, conn.assigns.person.id) + + if is_nil(role) do + AuthWeb.AuthController.not_found(conn, "role not found.") + else + render(conn, "show.html", role: role) + end + end + + def edit(conn, %{"id" => id}) do + role = Role.get_role!(id, conn.assigns.person.id) + + if is_nil(role) do + AuthWeb.AuthController.not_found(conn, "role not found.") + else + changeset = Role.change_role(role) + apps = Auth.App.list_apps(conn) + render(conn, "edit.html", role: role, changeset: changeset, apps: apps) + end + end + + def update(conn, %{"id" => id, "role" => role_params}) do + role = Role.get_role!(id, conn.assigns.person.id) + apps = Auth.App.list_apps(conn) + # cannot update a role that doesn't exist (or they don't own): + if is_nil(role) do + AuthWeb.AuthController.not_found(conn, "role not found.") + else + # confirm that the person owns the app they are attempting to attach a role to: + if person_owns_app?(apps, map_get(role_params, "app_id")) do + case Role.update_role(role, role_params) do + {:ok, role} -> + conn + |> put_flash(:info, "Role updated successfully.") + |> redirect(to: Routes.role_path(conn, :show, role)) + + {:error, %Ecto.Changeset{} = changeset} -> + apps = Auth.App.list_apps(conn) + render(conn, "edit.html", role: role, changeset: changeset, apps: apps) + end + else + AuthWeb.AuthController.not_found(conn, "App not found.") + end + end + end + + # https://elixirforum.com/t/map-key-is-a-atom-or-string/13285/2 + #  our use-case for this is specific keys in controller params + # mix gen creates tests with atom keys whereas controller expect string keys! + defp map_get(map, string_key, default \\ 0) do + to_string( + Map.get(map, string_key) || + Map.get(map, String.to_atom(string_key), default) + ) + end + + defp person_owns_app?(apps, app_id) do + app_ids = Enum.map(apps, fn a -> to_string(a.id) end) + Enum.member?(app_ids, app_id) + end + + def delete(conn, %{"id" => id}) do + # can only delete a role you own: + role = Role.get_role!(id, conn.assigns.person.id) + + if is_nil(role) do + AuthWeb.AuthController.not_found(conn, "role not found.") + else + {:ok, _role} = Role.delete_role(role) + + conn + |> put_flash(:info, "Role deleted successfully.") + |> redirect(to: Routes.role_path(conn, :index)) + end + end + + @doc """ + grant_role/3 grants a role to the given person + the conn must have conn.assigns.person to check for admin in order to grant the role. + grantee_id should be a valid person.id (the person you want to grant the role to) and + role_id a valid role.id + """ + def grant(conn, params) do + # confirm that the granter is either superadmin (conn.assigns.person.id == 1) + # or has an "admin" role (1 || 2) + granter_id = conn.assigns.person.id + # we need to expand grant priviledges see: https://github.com/dwyl/auth/issues/119 + + if granter_id == 1 do + role_id = map_get(params, "role_id") + person_id = map_get(params, "person_id") + Auth.PeopleRoles.insert(granter_id, person_id, role_id) + redirect(conn, to: Routes.people_path(conn, :show, person_id)) + else + AuthWeb.AuthController.unauthorized(conn) + end + end + + @doc """ + revoke/2 revokes a role + """ + def revoke(conn, params) do + # confirm that the granter is either superadmin (conn.assigns.person.id == 1) + # or has an "admin" role (1 || 2) + if conn.assigns.person.id == 1 do + people_roles_id = map_get(params, "people_roles_id") + pr = Auth.PeopleRoles.get_by_id(people_roles_id) + + if conn.method == "GET" do + render(conn, "revoke.html", role: pr, people_roles_id: people_roles_id) + else + Auth.PeopleRoles.revoke(conn.assigns.person.id, people_roles_id) + redirect(conn, to: Routes.people_path(conn, :show, pr.person_id)) + end + else + AuthWeb.AuthController.unauthorized(conn) + end + end +end diff --git a/lib/auth_web/plugs/is_owner.ex b/lib/auth_web/plugs/is_owner.ex new file mode 100644 index 00000000..be8197b3 --- /dev/null +++ b/lib/auth_web/plugs/is_owner.ex @@ -0,0 +1,12 @@ +defmodule Auth.Plugs.IsOwner do + @moduledoc """ + Confirm that the authenticated person is the owner of a particular record + """ + import Plug.Conn + + def is_owner(conn, options) when not is_nil(options) do + # IO.inspect(conn, label: "conn") + # IO.inspect(options, label: "options") + assign(conn, :owner, true) + end +end diff --git a/lib/auth_web/router.ex b/lib/auth_web/router.ex index 864c19b8..16b97a79 100644 --- a/lib/auth_web/router.ex +++ b/lib/auth_web/router.ex @@ -1,4 +1,7 @@ defmodule AuthWeb.Router do + @moduledoc """ + Defines Web Application Router pipelines and routes + """ use AuthWeb, :router pipeline :browser do @@ -9,10 +12,6 @@ defmodule AuthWeb.Router do plug :put_secure_browser_headers end - # pipeline :api do - # plug :accepts, ["json"] - # end - scope "/", AuthWeb do pipe_through :browser @@ -24,6 +23,8 @@ defmodule AuthWeb.Router do # get "/auth/password/new", AuthController, :password_input post "/auth/password/create", AuthController, :password_create post "/auth/password/verify", AuthController, :password_prompt + # https://github.com/dwyl/ping + get "/ping", PingController, :ping end pipeline :auth do @@ -34,12 +35,29 @@ defmodule AuthWeb.Router do pipe_through :browser pipe_through :auth + get "/people", PeopleController, :index + get "/people/:person_id", PeopleController, :show get "/profile", AuthController, :admin - resources "/settings/apikeys", ApikeyController + + get "/roles/grant", RoleController, :grant + get "/roles/revoke/:people_roles_id", RoleController, :revoke + post "/roles/revoke/:people_roles_id", RoleController, :revoke + resources "/roles", RoleController + + resources "/permissions", PermissionController + get "/apps/:id/resetapikey", AppController, :resetapikey + resources "/apps", AppController + # resources "/settings/apikeys", ApikeyController + end + + pipeline :api do + plug :accepts, ["json"] end # Other scopes may use custom stacks. - # scope "/api", AuthWeb do - # pipe_through :api - # end + scope "/", AuthWeb do + pipe_through :api + + get "/approles/:client_id", AppController, :approles + end end diff --git a/lib/auth_web/templates/apikey/edit.html.eex b/lib/auth_web/templates/apikey/edit.html.eex deleted file mode 100644 index 2509a843..00000000 --- a/lib/auth_web/templates/apikey/edit.html.eex +++ /dev/null @@ -1,14 +0,0 @@ -
- -

Edit Apikey

- -<%= render "form.html", - Map.put(assigns, :action, Routes.apikey_path(@conn, :update, @apikey)) %> - -
-<%= link "< Back", to: Routes.apikey_path(@conn, :index), -class: "pointer br2 ba b--orange bg-gold white pa3 ml1 mv1 f4 -shadow-hover bg-animate hover-bg-orange border-box no-underline" - %> - -
diff --git a/lib/auth_web/templates/apikey/index.html.eex b/lib/auth_web/templates/apikey/index.html.eex deleted file mode 100644 index 5b9450b8..00000000 --- a/lib/auth_web/templates/apikey/index.html.eex +++ /dev/null @@ -1,66 +0,0 @@ -
-

Auth API Keys

-
- - - - - - - - - - - - <%= for apikey <- @apikeys do %> - - - - - - - - - <% end %> - -
AUTH_API_KEYNameDescriptionUrl
- <%= apikey.client_id %>/<%= apikey.client_secret %> - <%= apikey.name %><%= apikey.description %><%= apikey.url %> - <%= link "View", to: Routes.apikey_path(@conn, :show, apikey), - class: "pointer br2 ba b--dark-blue bg-blue white pa3 ml1 mv1 f4 - bg-animate hover-bg-dark-blue border-box no-underline" - %> - - <%= link "Edit", to: Routes.apikey_path(@conn, :edit, apikey), - class: "pointer br2 ba b--orange bg-gold white pa3 ml1 mv1 f4 - bg-animate hover-bg-orange border-box no-underline" - %> - - <%= link "Delete", to: Routes.apikey_path(@conn, :delete, apikey), - method: :delete, - class: "pointer br2 ba b--dark-red bg-red white pa3 ml1 mv1 f4 - bg-animate hover-bg-dark-red border-box no-underline", - data: - [confirm: "Are you sure you want to delete this API Key? - (This cannot be undone!)"] %> - -
-

- - - <%= link "New Apikey", to: Routes.apikey_path(@conn, :new), - class: "center mr5 pointer br2 ba b--dark-green bg-green white pa3 ml1 mv1 f3 - shadow-hover bg-animate hover-bg-dark-green border-box no-underline" %> - - - -
diff --git a/lib/auth_web/templates/apikey/new.html.eex b/lib/auth_web/templates/apikey/new.html.eex deleted file mode 100644 index 76ed1f6e..00000000 --- a/lib/auth_web/templates/apikey/new.html.eex +++ /dev/null @@ -1,15 +0,0 @@ -
-

New Apikey

- - <%= render "form.html", Map.put(assigns, :action, - Routes.apikey_path(@conn, :create)) %> - -
- <%= link "< Back", to: Routes.apikey_path(@conn, :index), - class: "pointer br2 ba b--orange bg-gold white pa3 ml1 mv1 f4 mt3 - shadow-hover bg-animate hover-bg-orange border-box no-underline", - data: - [confirm: "Are you sure you want to disguard this form? - (The data cannot be recovered!)"] - %> -
diff --git a/lib/auth_web/templates/apikey/show.html.eex b/lib/auth_web/templates/apikey/show.html.eex deleted file mode 100644 index 96740c0d..00000000 --- a/lib/auth_web/templates/apikey/show.html.eex +++ /dev/null @@ -1,63 +0,0 @@ -
-

Your AUTH_API_KEY

- -

- To securely access your data from outside the dwyl app, - you will need to use an API Key. Keep this key safe. - Anyone who has the key can access - all of your data. -

- -

-

- <%= @apikey.client_id %>/<%= @apikey.client_secret %> -
-

- -

Export it as an environment variable: -

- export AUTH_API_KEY=<%= @apikey.client_id %>/<%= @apikey.client_secret %> -
-

- - -

- Key Name: - - <%= @apikey.name %> - -

- -

- Description: - - <%= @apikey.description %> - -

- -

- URL: - - <%= @apikey.url %> - -

- - - - <%= link "< Back", to: Routes.apikey_path(@conn, :index), - class: "pointer br2 ba b--orange bg-gold white pa3 ml1 mv1 f4 - shadow-hover bg-animate hover-bg-orange border-box no-underline" - %> - - - <%= link "Edit", to: Routes.apikey_path(@conn, :edit, @apikey), - class: "pointer br2 ba b--dark-green bg-green white pa3 ml1 mv1 f4 - shadow-hover bg-animate hover-bg-dark-green border-box no-underline" - %> - - -
diff --git a/lib/auth_web/templates/app/edit.html.eex b/lib/auth_web/templates/app/edit.html.eex new file mode 100644 index 00000000..143d468d --- /dev/null +++ b/lib/auth_web/templates/app/edit.html.eex @@ -0,0 +1,12 @@ +
+

Edit App

+ + <%= render "form.html", Map.put(assigns, :action, Routes.app_path(@conn, :update, @app)) %> + + <%= link "< All Apps", to: Routes.app_path(@conn, :index), + class: "fl pointer br2 ba b--orange bg-gold white pa3 f4 mt3 + shadow-hover bg-animate hover-bg-orange border-box no-underline", + data: + [confirm: "Any changes made will not be saved."] + %> +
\ No newline at end of file diff --git a/lib/auth_web/templates/apikey/form.html.eex b/lib/auth_web/templates/app/form.html.eex similarity index 83% rename from lib/auth_web/templates/apikey/form.html.eex rename to lib/auth_web/templates/app/form.html.eex index 4e0ed64c..76b51489 100644 --- a/lib/auth_web/templates/apikey/form.html.eex +++ b/lib/auth_web/templates/app/form.html.eex @@ -10,10 +10,10 @@ placeholder: "A distinctive name"%> <%= error_tag f, :name %>
- <%= label f, :description, class: "mt3" %> - <%= textarea f, :description, class: "db w-100 mt2 pa2 ba b--black", + <%= label f, :desc, class: "mt3" %> + <%= textarea f, :desc, class: "db w-100 mt2 pa2 ba b--black", placeholder: "Be as descriptive as possible." %> - <%= error_tag f, :description %> + <%= error_tag f, :desc %>
<%= label f, :url, class: "pt3" %> <%= text_input f, :url, class: "db w-100 mt2 pa2 ba b--dark-grey", @@ -21,7 +21,7 @@ %> <%= error_tag f, :url %> -
+
<%= submit "Save", class: "pointer br2 ba b--dark-green bg-green white pa3 ml1 mv1 f4 shadow-hover bg-animate hover-bg-dark-green border-box no-underline" diff --git a/lib/auth_web/templates/app/index.html.eex b/lib/auth_web/templates/app/index.html.eex new file mode 100644 index 00000000..8cab2589 --- /dev/null +++ b/lib/auth_web/templates/app/index.html.eex @@ -0,0 +1,62 @@ +

Apps

+ + + + + + + + + + + + +<%= for app <- @apps do %> + + + + + + + + +<% end %> + +
idOwnerNameDescriptionUrl
<%= app.id %><%= app.person_id %><%= app.name %><%= app.desc %><%= app.url %> + <%= link "View", to: Routes.app_path(@conn, :show, app), + class: "pointer br2 ba b--dark-blue bg-blue white pa3 ml1 mv1 f4 + bg-animate hover-bg-dark-blue border-box no-underline" + %> + + <%= link "Edit", to: Routes.app_path(@conn, :edit, app), + class: "pointer br2 ba b--orange bg-gold white pa3 ml1 mv1 f4 + bg-animate hover-bg-orange border-box no-underline" + %> + + <%= link "Delete", to: Routes.app_path(@conn, :delete, app), + method: :delete, + class: "pointer br2 ba b--dark-red bg-red white pa3 ml1 mv1 f4 + bg-animate hover-bg-dark-red border-box no-underline", + data: + [confirm: "Are you sure you want to delete this App? + (This cannot be undone!)"] %> + +
+ + + <%= link "New App", to: Routes.app_path(@conn, :new), + class: "pointer br2 ba b--dark-green bg-green white pa3 ml1 mv1 f3 + shadow-hover bg-animate hover-bg-dark-green border-box no-underline" %> + + + + \ No newline at end of file diff --git a/lib/auth_web/templates/app/new.html.eex b/lib/auth_web/templates/app/new.html.eex new file mode 100644 index 00000000..5e8872b4 --- /dev/null +++ b/lib/auth_web/templates/app/new.html.eex @@ -0,0 +1,13 @@ +
+

New App

+ + <%= render "form.html", Map.put(assigns, :action, Routes.app_path(@conn, :create)) %> + + <%= link "< Back", to: Routes.app_path(@conn, :index), + class: "fl pointer br2 ba b--orange bg-gold white pa3 f4 mt3 + shadow-hover bg-animate hover-bg-orange border-box no-underline", + data: + [confirm: "Are you sure you want to disguard this form? + (The data cannot be recovered!)"] + %> +
\ No newline at end of file diff --git a/lib/auth_web/templates/app/show.html.eex b/lib/auth_web/templates/app/show.html.eex new file mode 100644 index 00000000..ca980777 --- /dev/null +++ b/lib/auth_web/templates/app/show.html.eex @@ -0,0 +1,53 @@ +
+

<%= @app.name %>

+

<%= @app.desc %>

+ +

+ URL: + + <%= @app.url %> + +

+ + <%= for apikey <- @app.apikeys do %> + <%= if apikey.status != 6 do %> +

API KEY: +

+ <%= apikey.client_id %>/<%= apikey.client_secret %> +
+

+ +

Export it as an environment variable: +

+ export AUTH_API_KEY=<%= apikey.client_id %>/<%= apikey.client_secret %> +
+

+ <% end %> + <% end %> + + + <%= link "< All Apps", to: Routes.app_path(@conn, :index), + class: "fl pointer br2 ba b--dark-blue bg-blue white pv2 ph3 ml1 mv1 f4 + shadow-hover bg-animate hover-bg-dark-blue border-box no-underline" + %> + + + + <%= link "Edit", to: Routes.app_path(@conn, :edit, @app), + class: "fr pointer br2 ba b--dark-green bg-green white pv2 ph3 ml1 mv1 f4 + shadow-hover bg-animate hover-bg-dark-green border-box no-underline" + %> + + + + <%= link "Reset API Key", to: Routes.app_path(@conn, :resetapikey, @app), + class: "fl ml5 pointer br2 ba b--orange bg-gold white pv2 ph3 ml1 mv1 f4 + shadow-hover bg-animate hover-bg-orange border-box no-underline" + %> + + +
diff --git a/lib/auth_web/templates/auth/welcome.html.eex b/lib/auth_web/templates/auth/welcome.html.eex index 3b055518..59024008 100644 --- a/lib/auth_web/templates/auth/welcome.html.eex +++ b/lib/auth_web/templates/auth/welcome.html.eex @@ -5,12 +5,21 @@ class="center db br2"/>

You are signed in with your <%= String.capitalize(@conn.assigns.person.auth_provider) %> account. -

+


- - Manage API Keys - + <%= if length(@apps) == 0 do %> + + <%= link "New App", to: Routes.app_path(@conn, :new), + class: "pointer br2 ba b--dark-green bg-green white pa3 ml1 mv1 f3 + shadow-hover bg-animate hover-bg-dark-green border-box no-underline" %> + + + <% else %> + + Manage Apps + + <% end %> diff --git a/lib/auth_web/templates/layout/app.html.eex b/lib/auth_web/templates/layout/app.html.eex index daaae4dd..93c27abe 100644 --- a/lib/auth_web/templates/layout/app.html.eex +++ b/lib/auth_web/templates/layout/app.html.eex @@ -4,7 +4,7 @@ - <%= assigns[:page_title] || "Auth · Phoenix Framework" %> + <%= assigns[:page_title] || "Auth App" %> "/> <%= csrf_meta_tag() %> diff --git a/lib/auth_web/templates/layout/nav.html.eex b/lib/auth_web/templates/layout/nav.html.eex index dce5099e..28c46c49 100644 --- a/lib/auth_web/templates/layout/nav.html.eex +++ b/lib/auth_web/templates/layout/nav.html.eex @@ -12,22 +12,28 @@ dwyl logo - + --> diff --git a/lib/auth_web/templates/people/index.html.eex b/lib/auth_web/templates/people/index.html.eex new file mode 100644 index 00000000..ebf894ba --- /dev/null +++ b/lib/auth_web/templates/people/index.html.eex @@ -0,0 +1,56 @@ +
+

People Authenticated with Auth

+ + + + + + + + + + + + + + + + + <%= for {p, idx} <- Enum.with_index(@people) do %> + + + + + + + + + + + <% end %> + +
IDPicNameStatusTimeEmailAuth ProviderRoles
<%= p.id %><%= p.givenName %><%= status_string(p.status, @statuses) %><%= p.updated_at %><%= p.email %><%= p.auth_provider %><%= role_string(p.roles) %>
+
+ + + + + + + + + \ No newline at end of file diff --git a/lib/auth_web/templates/people/profile.html.eex b/lib/auth_web/templates/people/profile.html.eex new file mode 100644 index 00000000..bdb79d8c --- /dev/null +++ b/lib/auth_web/templates/people/profile.html.eex @@ -0,0 +1,73 @@ +
+ +

<%= @person.givenName %> <%= @person.familyName %>

+
+ <%= @person.email %> + (<%= capitalize(@person.auth_provider) %> Auth) + + <%= capitalize(status_string(@person.status, @statuses)) %> + +
+
Last login: <%= @person.updated_at %>
+
+ +

Roles

+ + + + + + + + + + + + + <%= for r <- @roles do %> + + + + + + + + + <% end %> + +
IDRoleGrantedGranted byRevocation
<%= r.id %><%= r.role.name %><%= r.inserted_at %> + <%= if Map.has_key?(r, :granter_id) and not is_nil(r.granter_id) do %> + <%= r.granter_id %> + <% else %> + System + <% end %> + + <%= if Map.has_key?(r, :revoked) and not is_nil(r.revoked) do %> + <%= r.revoked %> + <% else %> + + Revoke Role + + <% end %> +
+ +

Grant a New Role

+ +<%= form_tag("/roles/grant/", method: "get", class: "w-30 tc center mt2") do %> + + + <%= submit "Grant Role", class: "pointer bn bg-green white f2 pa2 mt1 shadow-hover bg-animate pa3 br1" %> +<% end %> + + \ No newline at end of file diff --git a/lib/auth_web/templates/permission/edit.html.eex b/lib/auth_web/templates/permission/edit.html.eex new file mode 100644 index 00000000..1794189c --- /dev/null +++ b/lib/auth_web/templates/permission/edit.html.eex @@ -0,0 +1,5 @@ +

Edit Permission

+ +<%= render "form.html", Map.put(assigns, :action, Routes.permission_path(@conn, :update, @permission)) %> + +<%= link "Back", to: Routes.permission_path(@conn, :index) %> diff --git a/lib/auth_web/templates/permission/form.html.eex b/lib/auth_web/templates/permission/form.html.eex new file mode 100644 index 00000000..1339ceb1 --- /dev/null +++ b/lib/auth_web/templates/permission/form.html.eex @@ -0,0 +1,19 @@ +<%= form_for @changeset, @action, fn f -> %> + <%= if @changeset.action do %> +
+

Oops, something went wrong! Please check the errors below.

+
+ <% end %> + + <%= label f, :name %> + <%= text_input f, :name %> + <%= error_tag f, :name %> + + <%= label f, :desc %> + <%= text_input f, :desc %> + <%= error_tag f, :desc %> + +
+ <%= submit "Save" %> +
+<% end %> diff --git a/lib/auth_web/templates/permission/index.html.eex b/lib/auth_web/templates/permission/index.html.eex new file mode 100644 index 00000000..b4277070 --- /dev/null +++ b/lib/auth_web/templates/permission/index.html.eex @@ -0,0 +1,28 @@ +

Listing Permissions

+ + + + + + + + + + + +<%= for permission <- @permissions do %> + + + + + + +<% end %> + +
NameDesc
<%= permission.name %><%= permission.desc %> + <%= link "Show", to: Routes.permission_path(@conn, :show, permission) %> + <%= link "Edit", to: Routes.permission_path(@conn, :edit, permission) %> + <%= link "Delete", to: Routes.permission_path(@conn, :delete, permission), method: :delete, data: [confirm: "Are you sure?"] %> +
+ +<%= link "New Permission", to: Routes.permission_path(@conn, :new) %> diff --git a/lib/auth_web/templates/permission/new.html.eex b/lib/auth_web/templates/permission/new.html.eex new file mode 100644 index 00000000..736492b0 --- /dev/null +++ b/lib/auth_web/templates/permission/new.html.eex @@ -0,0 +1,5 @@ +

New Permission

+ +<%= render "form.html", Map.put(assigns, :action, Routes.permission_path(@conn, :create)) %> + +<%= link "Back", to: Routes.permission_path(@conn, :index) %> diff --git a/lib/auth_web/templates/permission/show.html.eex b/lib/auth_web/templates/permission/show.html.eex new file mode 100644 index 00000000..cb78e3a1 --- /dev/null +++ b/lib/auth_web/templates/permission/show.html.eex @@ -0,0 +1,18 @@ +

Show Permission

+ + + +<%= link "Edit", to: Routes.permission_path(@conn, :edit, @permission) %> +<%= link "Back", to: Routes.permission_path(@conn, :index) %> diff --git a/lib/auth_web/templates/role/edit.html.eex b/lib/auth_web/templates/role/edit.html.eex new file mode 100644 index 00000000..08c3e1fd --- /dev/null +++ b/lib/auth_web/templates/role/edit.html.eex @@ -0,0 +1,9 @@ +
+

Edit Role

+ +<%= render "form.html", Map.put(assigns, :action, Routes.role_path(@conn, :update, @role)) %> + +<%= link "< Cancel Edit", to: Routes.role_path(@conn, :index), + class: "fl pointer br2 ba b--orange bg-gold white pv2 ph3 f4 mt1 + shadow-hover bg-animate hover-bg-orange border-box no-underline" %> +
\ No newline at end of file diff --git a/lib/auth_web/templates/role/form.html.eex b/lib/auth_web/templates/role/form.html.eex new file mode 100644 index 00000000..a3972100 --- /dev/null +++ b/lib/auth_web/templates/role/form.html.eex @@ -0,0 +1,26 @@ +<%= form_for @changeset, @action, fn f -> %> + <%= if @changeset.action do %> +
+

Oops, something went wrong! Please check the errors below.

+
+ <% end %> + + <%= label f, :name %> + <%= text_input f, :name, class: "db w-100 mt2 pa2 mb2 ba b--dark-grey" %> + <%= error_tag f, :name %> + + <%= label f, :desc %> + <%= text_input f, :desc, class: "db w-100 mt2 pa2 mb2 ba b--dark-grey" %> + <%= error_tag f, :desc %> + + <%= label f, :app_id %> + <%= select f, :app_id, Enum.map(@apps, &{&1.name, &1.id}), + class: "w-100 f3 pa2 mt2 mb2 bg-white b--black-20"%> + <%= error_tag f, :app_id %> + +
+ <%= submit "Save", + class: "fr pointer br2 ba b--dark-green bg-green white pv2 ph3 ml1 mv1 f4 + shadow-hover bg-animate hover-bg-dark-green border-box no-underline" %> +
+<% end %> diff --git a/lib/auth_web/templates/role/index.html.eex b/lib/auth_web/templates/role/index.html.eex new file mode 100644 index 00000000..ebc2a3c4 --- /dev/null +++ b/lib/auth_web/templates/role/index.html.eex @@ -0,0 +1,42 @@ +

Listing Roles

+ + + + + + + + + + + + + +<%= for role <- @roles do %> + + + + + + + +<% end %> + +
idNameDescriptionapp id
<%= role.id %><%= role.name %><%= role.desc %><%= raw app_link(role.app_id) %> + <%= link "Show", to: Routes.role_path(@conn, :show, role), + class: "pointer br2 ba b--dark-blue bg-blue white pa1 ml1 mv1 f4 + bg-animate hover-bg-dark-blue border-box no-underline" %> + <%= link "Edit", to: Routes.role_path(@conn, :edit, role), + class: "pointer br2 ba b--orange bg-gold white pa1 ml1 mv1 f4 + bg-animate hover-bg-orange border-box no-underline" %> + <%= link "Delete", to: Routes.role_path(@conn, :delete, role), method: :delete, + data: [confirm: "Are you sure you want to delete this role?"], + class: "pointer br2 ba b--dark-red bg-red white pa1 ml1 mv1 f4 + bg-animate hover-bg-dark-red border-box no-underline" %> +
+ + +<%= link "New Role", to: Routes.role_path(@conn, :new), + class: "pointer br2 ba b--dark-green bg-green white pa3 fr mr5 mv1 f3 + shadow-hover bg-animate hover-bg-dark-green border-box no-underline" %> + diff --git a/lib/auth_web/templates/role/new.html.eex b/lib/auth_web/templates/role/new.html.eex new file mode 100644 index 00000000..0375ff30 --- /dev/null +++ b/lib/auth_web/templates/role/new.html.eex @@ -0,0 +1,12 @@ +
+

New Role

+ +<%= render "form.html", Map.put(assigns, :action, Routes.role_path(@conn, :create)) %> + +<%= link "< Back", to: Routes.role_path(@conn, :index), + class: "fl pointer br2 ba b--orange bg-gold white pv2 ph3 f4 mt1 + shadow-hover bg-animate hover-bg-orange border-box no-underline", + data: + [confirm: "Are you sure you want to disguard this form? + (The data cannot be recovered!)"] %> +
\ No newline at end of file diff --git a/lib/auth_web/templates/role/revoke.html.eex b/lib/auth_web/templates/role/revoke.html.eex new file mode 100644 index 00000000..0d2895af --- /dev/null +++ b/lib/auth_web/templates/role/revoke.html.eex @@ -0,0 +1,59 @@ +
+

Are your sure you want + to revoke the + <%= @role.role.name %> role + from <%= @role.person.givenName %> + ?

+ + <%= form_for :revoke, Routes.role_path(AuthWeb.Endpoint, :revoke, @people_roles_id), fn _f -> %> + <%= submit "Revoke", class: "pointer bn bg-red white f2 pa2 mt1 shadow-hover bg-animate" %> + <% end %> + +
+ + \ No newline at end of file diff --git a/lib/auth_web/templates/role/show.html.eex b/lib/auth_web/templates/role/show.html.eex new file mode 100644 index 00000000..48c0cdb5 --- /dev/null +++ b/lib/auth_web/templates/role/show.html.eex @@ -0,0 +1,28 @@ +
+

Role

+ +
    + +

    + Name: + + <%= @role.name %> + +

    + +

    + Description: + + <%= @role.desc %> + +

    + +
+ + <%= link "< All Roles", to: Routes.role_path(@conn, :index), + class: "fl pointer br2 ba b--dark-blue bg-blue white pa3 ml1 mv1 f4 + shadow-hover bg-animate hover-bg-dark-blue border-box no-underline" %> + <%= link "Edit", to: Routes.role_path(@conn, :edit, @role), + class: "fr pointer br2 ba b--dark-green bg-green white pa3 ml1 mv1 f4 + shadow-hover bg-animate hover-bg-dark-green border-box no-underline" %> +
\ No newline at end of file diff --git a/lib/auth_web/views/apikey_view.ex b/lib/auth_web/views/apikey_view.ex deleted file mode 100644 index c135c170..00000000 --- a/lib/auth_web/views/apikey_view.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule AuthWeb.ApikeyView do - use AuthWeb, :view -end diff --git a/lib/auth_web/views/app_view.ex b/lib/auth_web/views/app_view.ex new file mode 100644 index 00000000..b65eb8a7 --- /dev/null +++ b/lib/auth_web/views/app_view.ex @@ -0,0 +1,3 @@ +defmodule AuthWeb.AppView do + use AuthWeb, :view +end diff --git a/lib/auth_web/views/people_view.ex b/lib/auth_web/views/people_view.ex new file mode 100644 index 00000000..d0fdec05 --- /dev/null +++ b/lib/auth_web/views/people_view.ex @@ -0,0 +1,37 @@ +defmodule AuthWeb.PeopleView do + use AuthWeb, :view + + @doc """ + status_string/2 returns a string of status + """ + def status_string(status_id, statuses) do + if status_id != nil do + status = Enum.at(Enum.filter(statuses, fn s -> s.id == status_id end), 0) + status.text + else + "none" + end + end + + @doc """ + role_string/1 returns a string of all the role names + """ + def role_string(person_roles) do + Enum.map_join(person_roles, " ", fn r -> + r.name + end) + end + + @doc """ + capitalize/1 captalises the first character of a string. + checks for nil values to avoid seeing the following error: + (FunctionClauseError) no function clause matching in String.capitalize/2 + """ + def capitalize(str) do + if is_nil(str) do + str + else + :string.titlecase(str) + end + end +end diff --git a/lib/auth_web/views/permission_view.ex b/lib/auth_web/views/permission_view.ex new file mode 100644 index 00000000..52313083 --- /dev/null +++ b/lib/auth_web/views/permission_view.ex @@ -0,0 +1,3 @@ +defmodule AuthWeb.PermissionView do + use AuthWeb, :view +end diff --git a/lib/auth_web/views/role_view.ex b/lib/auth_web/views/role_view.ex new file mode 100644 index 00000000..30860687 --- /dev/null +++ b/lib/auth_web/views/role_view.ex @@ -0,0 +1,10 @@ +defmodule AuthWeb.RoleView do + use AuthWeb, :view + + def app_link(app_id) do + case is_nil(app_id) do + true -> "all" + false -> " #{app_id} " + end + end +end diff --git a/mix.exs b/mix.exs index f7be88e4..7accd1f1 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ defmodule Auth.Mixfile do [ app: :auth, version: "1.2.4", - elixir: "~> 1.10", + elixir: "~> 1.10.4", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), test_coverage: [tool: ExCoveralls], @@ -47,7 +47,7 @@ defmodule Auth.Mixfile do # Phoenix core: {:phoenix, "~> 1.5.3"}, {:phoenix_pubsub, "~> 2.0"}, - {:phoenix_ecto, "~> 4.1.0"}, + {:phoenix_ecto, "~> 4.2.0"}, {:ecto_sql, "~> 3.4.5"}, {:postgrex, ">= 0.0.0"}, {:phoenix_html, "~> 2.14.2"}, @@ -58,19 +58,24 @@ defmodule Auth.Mixfile do # Auth: # https://github.com/dwyl/elixir-auth-github - {:elixir_auth_github, "~> 1.3.0"}, + {:elixir_auth_github, "~> 1.4.0"}, # https://github.com/dwyl/elixir-auth-google {:elixir_auth_google, "~> 1.3.0"}, # https://github.com/dwyl/auth_plug - {:auth_plug, "1.2.0"}, + {:auth_plug, "1.2.1"}, + # https://github.com/dwyl/rbac + {:rbac, "~> 0.5.0"}, # Field Validation and Encryption: github.com/dwyl/fields - {:fields, "~> 2.6.0"}, + {:fields, "~> 2.7.1"}, # Base58 Encodeing: https://github.com/dwyl/base58 {:B58, "~> 1.0", hex: :b58}, + # Ping to Wake Heroku Instance: https://github.com/dwyl/ping + {:ping, "~> 1.0.1"}, + # Check test coverage - {:excoveralls, "~> 0.12.3", only: :test}, + {:excoveralls, "~> 0.12.3", only: :test}, #  Property based tests: github.com/dwyl/learn-property-based-testing {:stream_data, "~> 0.4.3", only: :test}, diff --git a/mix.lock b/mix.lock index 2c92a781..e269e191 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "B58": {:hex, :b58, "1.0.1", "db9443361d2597ea60559ab5ef29a461f0da9698727e68d1ce2d5366644ebc1f", [:mix], [], "hexpm", "f0de289ba02513c8e966b3137b32c50826281e0ac2e11c22e2e7a0e810b4202d"}, "argon2_elixir": {:hex, :argon2_elixir, "2.3.0", "e251bdafd69308e8c1263e111600e6d68bd44f23d2cccbe43fcb1a417a76bc8e", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "28ccb63bff213aecec1f7f3dde9648418b031f822499973281d8f494b9d5a3b3"}, - "auth_plug": {:hex, :auth_plug, "1.2.0", "ae7fe3fb8532ed527baa16979d85be17dcc01d0069d9a3732637c99d7114d193", [:mix], [{:joken, "~> 2.2.0", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3fd700fcd761fb8c128d1ebb480c578d04171bc4a4a49c7131667a97e0bb5e25"}, + "auth_plug": {:hex, :auth_plug, "1.2.1", "7b8af3bc119452b0da01e1f9c848d17ce62e96893c5c3c1b4eb36f3fc986c990", [:mix], [{:joken, "~> 2.2.0", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "6004562a15294f36df3fd844d1c63b776807d5c1509faa9be02d5f8e8bb123d9"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, "comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"}, @@ -10,48 +10,50 @@ "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, "credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"}, "db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"}, - "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"}, + "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, - "earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"}, - "ecto": {:hex, :ecto, "3.4.5", "2bcd262f57b2c888b0bd7f7a28c8a48aa11dc1a2c6a858e45dd8f8426d504265", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8c6d1d4d524559e9b7a062f0498e2c206122552d63eacff0a6567ffe7a8e8691"}, + "earmark": {:hex, :earmark, "1.4.10", "bddce5e8ea37712a5bfb01541be8ba57d3b171d3fa4f80a0be9bcf1db417bcaf", [:mix], [{:earmark_parser, ">= 1.4.10", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "12dbfa80810478e521d3ffb941ad9fbfcbbd7debe94e1341b4c4a1b2411c1c27"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, + "ecto": {:hex, :ecto, "3.4.6", "08f7afad3257d6eb8613309af31037e16c36808dfda5a3cd0cb4e9738db030e4", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6f13a9e2a62e75c2dcfc7207bfc65645ab387af8360db4c89fee8b5a4bf3f70b"}, "ecto_sql": {:hex, :ecto_sql, "3.4.5", "30161f81b167d561a9a2df4329c10ae05ff36eca7ccc84628f2c8b9fa1e43323", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31990c6a3579b36a3c0841d34a94c275e727de8b84f58509da5f1b2032c98ac2"}, - "elixir_auth_github": {:hex, :elixir_auth_github, "1.3.0", "1885de6fb7c3994e4a614864c76032f59761d5b3d2dce6c839f93d72f46e375a", [:mix], [{:httpoison, "~> 1.7.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 4.0.1", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "4ae8bc9c9cd1a7a087ffe1e6dfda55137cf3ca9b82cd30e18e9dc7b79ba3f253"}, + "elixir_auth_github": {:hex, :elixir_auth_github, "1.4.0", "fa6b4f52812201380213d49bbe09d8706dc97eacf9962c9e8d34d95aec3ad33c", [:mix], [{:httpoison, "~> 1.7.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 4.0.1", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "7c6ec4e8c73cef5c810a70e308505f67b56805a72b6fe9841ea81e8d24b91c7e"}, "elixir_auth_google": {:hex, :elixir_auth_google, "1.3.0", "b3a7843ccc004888f2dd1478b3ca5a102751138657ee2fd68fdbbf43e44fb138", [:mix], [{:httpoison, "~> 1.7.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 4.0.1", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "d2fb766ca4f6fcbf8f1f4128dcf2a35c7cd0c702ab62e3110458acd8038d0267"}, - "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"}, + "elixir_make": {:hex, :elixir_make, "0.6.1", "8faa29a5597faba999aeeb72bbb9c91694ef8068f0131192fb199f98d32994ef", [:mix], [], "hexpm", "35d33270680f8d839a4003c3e9f43afb595310a592405a00afc12de4c7f55a18"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"}, - "exbase58": {:hex, :exbase58, "1.0.2", "2caa5df4d769b5c555cde11b85e93199037ed8b41f1da23e812619c10e3a3424", [:mix], [], "hexpm", "fe6b6b465750bdc1bd01c7b33b265902dabd63061f7db24e663509b45b4bba3c"}, "excoveralls": {:hex, :excoveralls, "0.12.3", "2142be7cb978a3ae78385487edda6d1aff0e482ffc6123877bb7270a8ffbcfe0", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "568a3e616c264283f5dea5b020783ae40eef3f7ee2163f7a67cbd7b35bcadada"}, - "fields": {:hex, :fields, "2.6.0", "bbbc32f3a49895b447996b993ebc380d5ea488ab76604ff1bb7390c49fab17ae", [:mix], [{:argon2_elixir, "~> 2.3.0", [hex: :argon2_elixir, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:html_sanitize_ex, "~> 1.4", [hex: :html_sanitize_ex, repo: "hexpm", optional: false]}], "hexpm", "a72a49cdf26cca05f56eb3de708af6e5744829e844aff6e89b71617c3a3309f9"}, + "fields": {:hex, :fields, "2.7.1", "8e388bce46877c25607100caae686d6ea3cd88182d8c100169484a7744b1b502", [:mix], [{:argon2_elixir, "~> 2.3.0", [hex: :argon2_elixir, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.6", [hex: :ecto, repo: "hexpm", optional: false]}, {:html_sanitize_ex, "~> 1.4.1", [hex: :html_sanitize_ex, repo: "hexpm", optional: false]}], "hexpm", "8ec6545e50128a1a8e77009b51de46d121011bb04449d99ab3e7592ea21ce636"}, "file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"}, - "gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"}, + "gettext": {:hex, :gettext, "0.18.1", "89e8499b051c7671fa60782faf24409b5d2306aa71feb43d79648a8bc63d0522", [:mix], [], "hexpm", "e70750c10a5f88cb8dc026fc28fa101529835026dec4a06dba3b614f2a99c7a9"}, "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, - "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.0", "0310d27d7bafb662f30bff22ec732a72414799c83eaf44239781fd23b96216c0", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "c5d79626be0b6e50c19ecdfb783ee26e85bd3a77436b488379ce6dc104ec4593"}, + "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.1", "e8a67da405fe9f0d1be121a40a60f70811192033a5b8d00a95dddd807f5e053e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "68d92656f47cd73598c45ad2394561f025c8c65d146001b955fd7b517858962a"}, "httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"}, "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, - "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, + "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, "joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"}, "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"}, - "makeup": {:hex, :makeup, "1.0.1", "82f332e461dc6c79dbd82fbe2a9c10d48ed07146f0a478286e590c83c52010b5", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49736fe5b66a08d8575bf5321d716bac5da20c8e6b97714fec2bcd6febcfa1f8"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, + "makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, + "mime": {:hex, :mime, "1.4.0", "5066f14944b470286146047d2f73518cf5cca82f8e4815cf35d196b58cf07c47", [:mix], [], "hexpm", "75fa42c4228ea9a23f70f123c74ba7cece6a03b1fd474fe13f6a7a85c6ea4ff6"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mochiweb": {:hex, :mochiweb, "2.20.1", "e4dbd0ed716f076366ecf62ada5755a844e1d95c781e8c77df1d4114be868cdf", [:rebar3], [], "hexpm", "d1aeee7870470d2fa9eae0b3d5ab6c33801aa2d82b10e9dade885c5c921b36aa"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, - "phoenix": {:hex, :phoenix, "1.5.3", "bfe0404e48ea03dfe17f141eff34e1e058a23f15f109885bbdcf62be303b49ff", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e16febeb9640d8b33895a691a56481464b82836d338bb3a23125cd7b6157c25"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"}, + "phoenix": {:hex, :phoenix, "1.5.4", "0fca9ce7e960f9498d6315e41fcd0c80bfa6fbeb5fa3255b830c67fdfb7e703f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4e516d131fde87b568abd62e1b14aa07ba7d5edfd230bab4e25cc9dedbb39135"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.2.0", "4ac3300a22240a37ed54dfe6c0be1b5623304385d1a2c210a70f011d9e7af7ac", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "59e7e2a550d7ea082a665c0fc29485f06f55d1a51dd02f513aafdb9d16fc72c4"}, "phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"}, - "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.2", "38d94c30df5e2ef11000697a4fbe2b38d0fbf79239d492ff1be87bbc33bc3a84", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "a3dec3d28ddb5476c96a7c8a38ea8437923408bc88da43e5c45d97037b396280"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.4", "940c0344b1d66a2e46eef02af3a70e0c5bb45a4db0bf47917add271b76cd3914", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "38f9308357dea4cc77f247e216da99fcb0224e05ada1469167520bed4cb8cccd"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, - "plug": {:hex, :plug, "1.10.3", "c9cebe917637d8db0e759039cc106adca069874e1a9034fd6e3fdd427fd3c283", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "01f9037a2a1de1d633b5a881101e6a444bcabb1d386ca1e00bb273a1f1d9d939"}, + "ping": {:hex, :ping, "1.0.1", "1f44c7b7151af18627edbd7946a376935871f285fc9c11e9f16ddb1555ea65b5", [:mix], [{:plug, "~> 1.10.1", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c1421bebcb6601e7fb1158700ca99eb2dc0976b45fe2a26023db5a9318aadfa6"}, + "plug": {:hex, :plug, "1.10.4", "41eba7d1a2d671faaf531fa867645bd5a3dce0957d8e2a3f398ccff7d2ef017f", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad1e233fe73d2eec56616568d260777b67f53148a999dc2d048f4eb9778fe4a0"}, "plug_cowboy": {:hex, :plug_cowboy, "2.3.0", "149a50e05cb73c12aad6506a371cd75750c0b19a32f81866e1a323dda9e0e99d", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bc595a1870cef13f9c1e03df56d96804db7f702175e4ccacdb8fc75c02a7b97e"}, "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"}, "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, "postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, - "sobelow": {:hex, :sobelow, "0.10.2", "00e91208046d3b434f9f08779fe0ca7c6d6595b7fa33b289e792dffa6dde8081", [:mix], [], "hexpm", "e30fc994330cf6f485c1c4f2fb7c4b2d403557d0e101c6e5329fd17a58e55a7e"}, + "rbac": {:hex, :rbac, "0.5.0", "49b34b40f4845e2521f1e4f6e0b187d71d3bb98aafd51b2db340fdb9b60e59cd", [:mix], [{:auth_plug, "~> 1.2", [hex: :auth_plug, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.7.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.2.2", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "985e6a6c7c50b12e9b1876669432741b690b971463e68f09320e5ef7b3248869"}, + "sobelow": {:hex, :sobelow, "0.10.4", "44ba642da120d84fedb9e85473375084034330c8f15a992351dd164a82963103", [:mix], [], "hexpm", "fea62a94a4112de45ee9c9d076fd636fbbc10b7c7c2ea99a928e7c289b8498d1"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "stream_data": {:hex, :stream_data, "0.4.3", "62aafd870caff0849a5057a7ec270fad0eb86889f4d433b937d996de99e3db25", [:mix], [], "hexpm", "7dafd5a801f0bc897f74fcd414651632b77ca367a7ae4568778191fc3bf3a19a"}, "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, diff --git a/priv/repo/default_roles.json b/priv/repo/default_roles.json new file mode 100644 index 00000000..ba2e29c9 --- /dev/null +++ b/priv/repo/default_roles.json @@ -0,0 +1,58 @@ +[ + { + "name": "superadmin", + "desc": "With great power comes great responsibility", + "person_id": "1", + "id": "1", + "permissions": "grant_admin_role" + }, + { + "name": "admin", + "desc": "Can perform all system administration tasks", + "person_id": "1", + "id": "2", + "permissions": "manage_people, grant_non_admin_role" + }, + { + "name": "moderator", + "desc": "Can view and neutrally moderate any content. Can ban rule-breakers. Cannot delete.", + "person_id": "1", + "id": "3", + "permissions": "edit_any_content, lock_content, unpublish_content, ban_rule_breaking_people, view_deleted" + }, + { + "name": "creator", + "desc": "Can create any content. Can edit and delete their own content.", + "person_id": "1", + "id": "4", + "permissions": "create_content, upload_images, edit_own_content, delete_own_content, invite_people" + }, + { + "name": "commenter", + "desc": "Can comment on content where commenting is available.", + "person_id": "1", + "id": "5", + "permissions": "comment, flag_comments, flag_content" + }, + { + "name": "subscriber", + "desc": "Subscribes for updates e.g. newsletter or content from a specific person. Cannot comment until verified.", + "person_id": "1", + "id": "6", + "permissions": "subscribe, give_feedback" + }, + { + "name": "banned", + "desc": "Can still login to see their content but cannot perform any other action.", + "person_id": "1", + "id": "7", + "permissions": "view_content, view_profile, delete_own_content, delete_own_profile" + }, + { + "name": "app_admin", + "desc": "Can manage their own App(s).", + "person_id": "1", + "id": "8", + "permissions": "manage_own_apps, create_content, upload_images, edit_own_content, delete_own_content, invite_people" + } +] \ No newline at end of file diff --git a/priv/repo/migrations/20191113100912_create_status.exs b/priv/repo/migrations/20191113100912_create_status.exs index 6d19f2c7..df640170 100644 --- a/priv/repo/migrations/20191113100912_create_status.exs +++ b/priv/repo/migrations/20191113100912_create_status.exs @@ -4,6 +4,7 @@ defmodule Auth.Repo.Migrations.CreateStatus do def change do create table(:status) do add :text, :string + add :desc, :string timestamps() end diff --git a/priv/repo/migrations/20200424141937_create_apps.exs b/priv/repo/migrations/20200424141937_create_apps.exs new file mode 100644 index 00000000..9e907bf8 --- /dev/null +++ b/priv/repo/migrations/20200424141937_create_apps.exs @@ -0,0 +1,21 @@ +defmodule Auth.Repo.Migrations.CreateApps do + use Ecto.Migration + + def change do + create table(:apps) do + add :name, :binary + add :desc, :binary + add :url, :binary + add :end, :naive_datetime + add :person_id, references(:people, on_delete: :nothing) + add :status, references(:status, on_delete: :nothing) + # add :apikey_id, references(:apikeys, on_delete: :nothing) + + timestamps() + end + + create index(:apps, [:person_id]) + create index(:apps, [:status]) + # create index(:apps, [:apikey_id]) + end +end diff --git a/priv/repo/migrations/20200424141938_create_apikeys.exs b/priv/repo/migrations/20200424141938_create_apikeys.exs index d96c53f1..dededfa5 100644 --- a/priv/repo/migrations/20200424141938_create_apikeys.exs +++ b/priv/repo/migrations/20200424141938_create_apikeys.exs @@ -5,9 +5,10 @@ defmodule Auth.Repo.Migrations.CreateApikeys do create table(:apikeys) do add :client_id, :binary add :client_secret, :binary - add :name, :string - add :description, :text - add :url, :binary + # add :name, :string + # add :description, :text + # add :url, :binary + add :app_id, references(:apps, on_delete: :nothing) add :person_id, references(:people, on_delete: :nothing) add :status, references(:status, on_delete: :nothing) diff --git a/priv/repo/migrations/20200722175850_create_roles.exs b/priv/repo/migrations/20200722175850_create_roles.exs new file mode 100644 index 00000000..1586d0be --- /dev/null +++ b/priv/repo/migrations/20200722175850_create_roles.exs @@ -0,0 +1,17 @@ +defmodule Auth.Repo.Migrations.CreateRoles do + use Ecto.Migration + + def change do + create table(:roles) do + add :name, :string + add :desc, :string + add :person_id, references(:people, on_delete: :nothing) + add :app_id, references(:apps, on_delete: :nothing) + + timestamps() + end + + create index(:roles, [:person_id]) + create index(:roles, [:app_id]) + end +end diff --git a/priv/repo/migrations/20200722180019_create_permissions.exs b/priv/repo/migrations/20200722180019_create_permissions.exs new file mode 100644 index 00000000..7c47cbad --- /dev/null +++ b/priv/repo/migrations/20200722180019_create_permissions.exs @@ -0,0 +1,15 @@ +defmodule Auth.Repo.Migrations.CreatePermissions do + use Ecto.Migration + + def change do + create table(:permissions) do + add :name, :string + add :desc, :string + add :person_id, references(:people, on_delete: :nothing) + + timestamps() + end + + create index(:permissions, [:person_id]) + end +end diff --git a/priv/repo/migrations/20200723143204_create_role_permissions.exs b/priv/repo/migrations/20200723143204_create_role_permissions.exs new file mode 100644 index 00000000..55bde3af --- /dev/null +++ b/priv/repo/migrations/20200723143204_create_role_permissions.exs @@ -0,0 +1,15 @@ +defmodule Auth.Repo.Migrations.CreateRolePermissions do + use Ecto.Migration + + def change do + create table(:role_permissions) do + add :role_id, references(:roles, on_delete: :nothing) + add :permission_id, references(:permissions, on_delete: :nothing) + add :granter_id, references(:people, on_delete: :nothing) + + timestamps() + end + + create unique_index(:role_permissions, [:role_id, :permission_id]) + end +end diff --git a/priv/repo/migrations/20200723154847_create_people_roles.exs b/priv/repo/migrations/20200723154847_create_people_roles.exs new file mode 100644 index 00000000..f71062b8 --- /dev/null +++ b/priv/repo/migrations/20200723154847_create_people_roles.exs @@ -0,0 +1,18 @@ +defmodule Auth.Repo.Migrations.CreatePeopleRoles do + use Ecto.Migration + + def change do + create table(:people_roles) do + add :person_id, references(:people, on_delete: :nothing) + add :role_id, references(:roles, on_delete: :nothing) + add :granter_id, references(:people, on_delete: :nothing) + # elixirforum.com/t/difference-between-utc-datetime-and-naive-datetime/12551 + add :revoked, :utc_datetime + add :revoker_id, references(:people, on_delete: :nothing) + + timestamps() + end + + create unique_index(:people_roles, [:person_id, :role_id]) + end +end diff --git a/priv/repo/migrations/20200912235027_add_app_id_people.exs b/priv/repo/migrations/20200912235027_add_app_id_people.exs new file mode 100644 index 00000000..c517717f --- /dev/null +++ b/priv/repo/migrations/20200912235027_add_app_id_people.exs @@ -0,0 +1,9 @@ +defmodule Auth.Repo.Migrations.AddAppIdPeople do + use Ecto.Migration + + def change do + alter table(:people) do + add :app_id, references(:apps, on_delete: :nothing) + end + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index be260fc2..100b8240 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -1,3 +1,4 @@ +require Logger # Script for populating the database. You can run it as: # # mix run priv/repo/seeds.exs @@ -9,10 +10,13 @@ # mix ecto.setup defmodule Auth.Seeds do alias Auth.{Person, Repo, Status} - # put_assoc import Ecto.Changeset def create_admin do + if is_nil(System.get_env("TRAVIS")) do + load_env() + end + email = System.get_env("ADMIN_EMAIL") person = @@ -20,38 +24,116 @@ defmodule Auth.Seeds do nil -> %Person{} |> Person.changeset(%{email: email}) - |> put_assoc(:statuses, [%Status{text: "verified"}]) + # |> put_assoc(:statuses, [%Status{text: "verified"}]) |> Repo.insert!() person -> person end - IO.inspect(person.id, label: "seeds.exs person.id") - IO.puts("- - - - - - - - - - - - - - - - - - - - - - ") + if(Mix.env() == :test) do + # don't print noise during tests + else + IO.inspect(person.id, label: "seeds.exs person.id") + IO.puts("- - - - - - - - - - - - - - - - - - - - - - ") + end person end def create_apikey_for_admin(person) do - {:ok, key} = + {:ok, app} = %{ - "name" => "system admin key", - "description" => "Created by /priv/repo/seeds.exs during setup.", - # the default host in %Plug.Conn - "url" => "www.example.com" + "name" => "default system app", + "desc" => "Created by /priv/repo/seeds.exs during setup.", + "url" => "localhost:4000", + "person_id" => person.id, + "status" => 3 } - |> AuthWeb.ApikeyController.make_apikey(person.id) - |> Auth.Apikey.create_apikey() + |> Auth.App.create_app() + + # set the api key to AUTH_API_KEY in env: + update_attrs = %{ + "client_id" => AuthPlug.Token.client_id(), + "client_secret" => AuthPlug.Token.client_secret() + } + + {:ok, key} = + Auth.Apikey.get_apikey_by_app_id(app.id) + |> cast(update_attrs, [:client_id, :client_secret]) + |> Repo.update() api_key = key.client_id <> "/" <> key.client_secret - # Set the AUTH_API_KEY to a valid value that is in the DB: - System.put_env("AUTH_API_KEY", api_key) - IO.inspect(System.get_env("AUTH_API_KEY"), label: "AUTH_API_KEY") - IO.puts("- - - - - - - - - - - - - - - - - - - - - - ") - key + # set the AUTH_API_KEY environment variable during test run: + IO.inspect(Mix.env(), label: "Mix.env()") + # IO.inspect(person) + case Mix.env() do + :test -> + System.put_env("AUTH_API_KEY", api_key) + + :prod -> + Logger.info("export AUTH_API_KEY=#{api_key}") + + _ -> + nil + end + end + + # load the .env file + def load_env() do + path = File.cwd!() <> "/.env" + IO.inspect(path, label: ".env file path") + {:ok, data} = File.read(path) + + Enum.map(String.split(data, "\n"), fn line -> + line = + line + |> String.replace("export ", "") + |> String.replace("'", "") + + # this part is convoluted because some .env values can contain "=" chacter: + {index, _} = :binary.match(line, "=") + len = String.length(line) + value = String.slice(line, index + 1, len) + [key | _rest] = String.split(line, "=") + # IO.inspect(value, label: key) + System.put_env(key, value) + end) end end -Auth.Seeds.create_admin() -|> Auth.Seeds.create_apikey_for_admin() +# scripts for creating default roles and permissions +defmodule SeedData do + alias Auth.{Role, Status} + + def get_json(filepath) do + path = File.cwd!() <> filepath + {:ok, data} = File.read(path) + json = Jason.decode!(data) + json + end + + def create_default_roles do + Enum.each(get_json("/priv/repo/default_roles.json"), fn role -> + Role.upsert_role(role) + end) + end + + def insert_statuses do + Enum.each(get_json("/priv/repo/statuses.json"), fn status -> + Status.upsert_status(status) + end) + end +end + +person = Auth.Seeds.create_admin() +SeedData.insert_statuses() + +Auth.Seeds.create_apikey_for_admin(person) + +# Update status of Admin to "Verified" +Auth.Person.verify_person_by_id(1) + +SeedData.create_default_roles() +# grant superadmin role to app owner: +Auth.PeopleRoles.insert(1, 1, 1) diff --git a/priv/repo/statuses.json b/priv/repo/statuses.json new file mode 100644 index 00000000..0427ab2c --- /dev/null +++ b/priv/repo/statuses.json @@ -0,0 +1,57 @@ +[ + { + "text": "verified", + "id": "1", + "desc": "People are verified once they confirm their email address" + }, + { + "text": "uncategorized", + "id": "2", + "desc": "All items are uncategorized when they are first created. (Yes, US spelling)" + }, + { + "text": "active", + "id": "3", + "desc": "An App, Item or Person can be active; this is the default state for an App" + }, + { + "text": "done", + "id": "4", + "desc": "Items marked as done are complete" + }, + { + "text": "flagged", + "id": "5", + "desc": "A flagged App, Item or Person requires admin attention" + }, + { + "text": "deleted", + "id": "6", + "desc": "Soft-deleted items that no longer appear in UI but are kept for audit trail purposes" + }, + { + "text": "pending", + "id": "7", + "desc": "When an email or item is ready to be started/sent is still pending" + }, + { + "text": "sent", + "id": "8", + "desc": "An email that has been sent but not yet opened" + }, + { + "text": "opened", + "id": "9", + "desc": "When an email is opened by the recipient" + }, + { + "text": "bounce_transient", + "id": "10", + "desc": "Temporary email bounce e.g. because inbox is full" + }, + { + "text": "bounce_permanent", + "id": "11", + "desc": "Permanent email bounce e.g. when inbox doesn't exist" + } +] \ No newline at end of file diff --git a/role-based-access-control.md b/role-based-access-control.md new file mode 100644 index 00000000..9772c77a --- /dev/null +++ b/role-based-access-control.md @@ -0,0 +1,227 @@ +# Role Based Access Control (RBAC) + +_Understand_ the fundamentals of Role Based Access Control (RBAC) +so that you can easily control who has access to what in your App. + +## Why? + +RBAC lets you easily manage roles and permissions in any application +and see at a glance exactly what permissions a person has. +It reduces complexity over traditional +Access Control List (ACL) based permissions systems +and helps everyone building and maintaining the app +to focus on security. + +## _Who_? + +This document is relevant to anyone +that is interested in developing and _maintaining_ +secure multi-person applications +should learn about RBAC. + + +## What? + +The purpose of RBAC is to provide a framework +for application administrators and developers +to manage the permissions assigned to the people using the App(s). + +Each role granted just enough flexibility and permissions +to perform the tasks required for their job, +this helps enforce the +[principal of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege). + +The RBAC methodology is based on a set of three principal rules +that govern access to systems: + +1. **Role Assignment**: +Each transaction or operation can only be carried out +if the person has assumed the appropriate role. +An operation is defined as any action taken +with respect to a system or network object that is protected by RBAC. +Roles may be assigned by a separate party +or selected by the person attempting to perform the action. + +2. **Role Authorization**: +The purpose of role authorization +is to ensure that people can only assume a role +for which they have been given the appropriate authorization. +When a person assumes a role, +they must do so with authorization from an admin. + +3. **Transaction Authorization**: +An operation can only be completed +if the person attempting to complete the transaction +possesses the appropriate role. + + +### Default Roles + +We have defined the following 7 `default` roles based on our experience/research +into RBAC systems of several of the most popular applications +including both "enterprise" (closed source) and popular open source CRM/CMS apps. + +| **`id`** | **`name`** | **`desc`** | `person_id` | +| -------- | ---------- | ---------- | ----------- | +| `1` | superadmin | Can **`CREATE`** new roles. Can **`CREATE`**, **`UPDATE`** and **`DELETE`** Any content. Can **`PURGE`** deleted items. Can "ban" any user including people with "Admin" Role. | 1 | +| `2` | admin | Can **create** new roles and **assign** existing roles. Can **`CREATE`**, **`UPDATE`** and **`DELETE`** any content. Can "ban" any user except people with "admin" Role. Can see deleted content and un-delete it. Cannot _purge_ deleted. This guarantees audit-trail. | 1 | +| `3` | moderator | Can neutrally moderate _any_ content. Can _ban_ rule-breaking `people`. Cannot **"`DELETE`"** content. | 1 | +| `4` | creator | Can **`CREATE`** content. Can **`UPDATE`** their _own_ content. Can **`DELETE`** their _own_ content. | 1 | +| `5` | commenter | Can **`COMMENT`** on content that has commenting enabled. | 1 | +| `6` | subscriber | Can **`SUBSCRIBE`** to receive updates (e.g: newsletter), but has either not verified their account or has made negative comments and is therefore _not_ allowed to comment. | 1 | +| `7` | banned | Can login and see their past content. Cannot create any new content. Can see the _reason_ for their banning (_which the Admin has to write when performing the "ban user" action. usually linked to a specific action the person performed like a particularly unacceptable comment._) | 1 | + +These roles are loosely inspired by WordPress: +https://wordpress.org/support/article/roles-and-capabilities
+The **`superadmin`** and **`admin`** roles make sense: +The person who can perform "system administration" tasks like updating the schemas/code, +this is the **`superadmin`** or "**owner**" of the application. +Typically there is only ***one*** **`superadmin`**, +this person is ultimately responsible for _everything_. +The people who are responsible for _maintaining_ the site/app +including the `content` and `people` +are given the **`admin`** role; +they can see everything that is going on. + +The **`admin`** role should +We have renamed "editor" to "moderator" +because we feel this role is more _relevant_ in a multi-content setting +see: https://en.wikipedia.org/wiki/Moderator +We have also renamed "author" to "creator" +to emphasize that creating content +is more than just "authoring" text. +There will be various types of content not just "posts". +We have added a "**commenter** role as an "upgrade" to **subscriber**, +to indicate that the person has the ability to _comment_ on content. +Finally, we have added the concept of a "**banned**" role +that still allows the person to login and view their _own_ content, +but they have no other privileges. + + +## _How_? + +_Before_ creating any roles, +you will need to have a baseline schema including **`people`** +as **`person.id`** will be referenced by roles. + +If you don't already have these schemas/tables, +see: https://github.com/dwyl/app-mvp-phoenix#create-schemas + + + +### Create `Roles` and `Permissions` Schemas + +Let's create the Database Schemas (Tables) +to store our RBAC data, +starting with **`Roles`**: + +``` +mix phx.gen.html Ctx Role roles name:string desc:string person_id:references:people +``` + +Next create the permissions schema: +``` +mix phx.gen.html Ctx Permission permissions name:string desc:string person_id:references:people +``` + +We placed the roles and permissions resources in an **`:auth`** pipeline +because we only want people with **`superadmin`** role to access them. +See: +[`/lib/auth_web/router.ex#L41-L43`](https://github.com/dwyl/auth/blob/2a3c361e87cbe4fadbd6beda2eef989299c48a53/lib/auth_web/router.ex#L41-L42) + + + +### Create Roles<->Permissions Associations + +Next create the **`many-to-many`** relationship +between roles and permissions. + +``` +mix ecto.gen.migration create_role_permissions +``` + +Open the file that was just created, e.g: +[`priv/repo/migrations/20200723143204_create_role_permissions.exs`](https://github.com/dwyl/auth/blob/ef4261d09a702c4003cd84f30dabe630b47922d2/priv/repo/migrations/20200723143204_create_role_permissions.exs) + +And replace the contents with: +```elixir +defmodule Auth.Repo.Migrations.CreateRolePermissions do + use Ecto.Migration + + def change do + create table(:role_permissions) do + add :role_id, references(:roles, on_delete: :nothing) + add :permission_id, references(:permissions, on_delete: :nothing) + add :granter, references(:people, on_delete: :nothing) + + timestamps() + end + + create unique_index(:role_permissions, [:role_id, :permission_id]) + end +end +``` + +### Create People<->Roles Associations + +Now create the **`many-to-many`** relationship +between **`people`** and **`roles`**: + +``` +mix ecto.gen.migration create_people_roles +``` + +Open the migration file that was just created, e.g: +[`/Users/n/code/auth/priv/repo/migrations/20200723154847_create_people_roles.exs`](https://github.com/dwyl/auth/blob/ef4261d09a702c4003cd84f30dabe630b47922d2/priv/repo/migrations/20200723154847_create_people_roles.exs) + + +Replace the contents of the file with the following code: + +```elixir +defmodule Auth.Repo.Migrations.CreatePeopleRoles do + use Ecto.Migration + + def change do + create table(:people_roles) do + add :person_id, references(:people, on_delete: :nothing) + add :role_id, references(:roles, on_delete: :nothing) + add :granter, references(:people, on_delete: :nothing) + + timestamps() + end + + create unique_index(:people_roles, [:person_id, :role_id]) + end +end +``` + +This is all we need in terms of database tables for now. +Run: +``` +mix ecto.migrate +``` +To create the tables. + +The Entity Relationship Diagram (ERD) should now look like this: + +[![auth-erd-with-roles-permissions](https://user-images.githubusercontent.com/194400/88439166-5c2e0e00-ce02-11ea-93ce-11c3a721cb18.png "Schema Diagram - Click to Enlarge")](https://user-images.githubusercontent.com/194400/88439166-5c2e0e00-ce02-11ea-93ce-11c3a721cb18.png) + +Next we need to create a script +that inserts the default roles and permissions +during the setup of the Auth App. + +### Setup Default Roles & Permissions + + + + + + + + +## Recommended Reading + ++ https://en.wikipedia.org/wiki/Role-based_access_control ++ https://www.sumologic.com/glossary/role-based-access-control ++ https://medium.com/@adriennedomingus/role-based-access-control-rbac-permissions-vs-roles-55f1f0051468 ++ https://digitalguardian.com/blog/what-role-based-access-control-rbac-examples-benefits-and-more \ No newline at end of file diff --git a/test/auth/apikey_test.exs b/test/auth/apikey_test.exs index 3e262bf8..c100228a 100644 --- a/test/auth/apikey_test.exs +++ b/test/auth/apikey_test.exs @@ -1,32 +1,57 @@ defmodule Auth.ApikeyTest do - # use Auth.DataCase - use AuthWeb.ConnCase - # alias Auth.Apikey - @email System.get_env("ADMIN_EMAIL") - - test "list_apikeys_for_person/1 returns all apikeys person" do - person = Auth.Person.get_person_by_email(@email) - - keys = Auth.Apikey.list_apikeys_for_person(person.id) - assert length(keys) == 1 - - # Insert Two API keys: - params = %{ - # "description" => "test key", - "name" => "My Amazing Key", - "url" => "http://localhost:400", - "person_id" => person.id, - "client_secret" => AuthWeb.ApikeyController.encrypt_encode(person.id) - } - - Auth.Apikey.create_apikey(params) - - Map.merge(params, %{ - "client_secret" => AuthWeb.ApikeyController.encrypt_encode(person.id) - }) - |> Auth.Apikey.create_apikey() - - keys = Auth.Apikey.list_apikeys_for_person(person.id) - assert length(keys) == 3 + use Auth.DataCase + use ExUnitProperties + + describe "Create an AUTH_API_KEY for a given person_id" do + test "encrypt_encode/1 returns a base58 we can decrypt" do + person_id = 1 + key = Auth.Apikey.encrypt_encode(person_id) + + decrypted = + key + |> Base58.decode() + |> Fields.AES.decrypt() + |> String.to_integer() + + assert decrypted == person_id + end + + test "decode_decrypt/1 reverses the operation of encrypt_encode/1" do + app_id = 4_869_234_521 + key = Auth.Apikey.encrypt_encode(app_id) + id = Auth.Apikey.decode_decrypt(key) + assert app_id == id + end + + test "create_api_key/1 creates an AUTH_API_KEY" do + app_id = 123_456_789 + key = Auth.Apikey.create_api_key(app_id) + assert key =~ "/" + parts = String.split(key, "/") + assert Auth.Apikey.decode_decrypt(List.first(parts)) == app_id + end + + test "decrypt_api_key/1 decrypts an AUTH_API_KEY" do + app_id = 1234 + key = Auth.Apikey.create_api_key(app_id) + decrypted = Auth.Apikey.decrypt_api_key(key) + assert decrypted == app_id + end + + test "decode_decrypt/1 with invalid client_id" do + valid_key = Auth.Apikey.encrypt_encode(1) + app_id = Auth.Apikey.decode_decrypt(valid_key) + assert app_id == 1 + + invalid_key = String.slice(valid_key, 0..-2) + error = Auth.Apikey.decode_decrypt(invalid_key) + assert error == 0 + end + + property "Check a batch of int values can be decoded decode_decrypt/1" do + check all(int <- integer()) do + assert Auth.Apikey.decode_decrypt(Auth.Apikey.encrypt_encode(int)) == int + end + end end end diff --git a/test/auth/app_test.exs b/test/auth/app_test.exs new file mode 100644 index 00000000..b5ff61e1 --- /dev/null +++ b/test/auth/app_test.exs @@ -0,0 +1,83 @@ +defmodule Auth.AppTest do + use Auth.DataCase + + describe "apps" do + alias Auth.App + + @valid_attrs %{ + desc: "some description", + end: ~N[2010-04-17 14:00:00], + name: "some name", + url: "some url", + status: 3 + } + @update_attrs %{ + desc: "some updated description", + end: ~N[2011-05-18 15:01:01], + name: "some updated name", + url: "some updated url", + status: 3 + } + @invalid_attrs %{description: nil, end: nil, name: nil, url: nil} + + def app_fixture(attrs \\ %{}) do + {:ok, app} = + attrs + |> Enum.into(@valid_attrs) + |> App.create_app() + + app + end + + test "list_apps/0 returns all apps" do + app = app_fixture() + a = List.first(Enum.filter(App.list_apps(), fn a -> a.id == app.id end)) + assert a.id == app.id + end + + test "get_app!/1 returns the app with given id" do + app = app_fixture(%{person_id: 1}) + a = App.get_app!(app.id) + assert a.id == app.id + end + + test "create_app/1 with valid data creates a app" do + assert {:ok, %App{} = app} = App.create_app(@valid_attrs) + assert app.desc == "some description" + assert app.end == ~N[2010-04-17 14:00:00] + assert app.name == "some name" + assert app.url == "some url" + end + + test "create_app/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = App.create_app(@invalid_attrs) + end + + test "update_app/2 with valid data updates the app" do + app = app_fixture() + assert {:ok, %App{} = app} = App.update_app(app, @update_attrs) + assert app.desc == "some updated description" + assert app.end == ~N[2011-05-18 15:01:01] + assert app.name == "some updated name" + assert app.url == "some updated url" + end + + test "update_app/2 with invalid data returns error changeset" do + app = app_fixture() + assert {:error, %Ecto.Changeset{}} = App.update_app(app, @invalid_attrs) + assert app == App.get_app!(app.id) + end + + test "delete_app/1 deletes the app" do + app = app_fixture() + assert {:ok, %App{}} = App.delete_app(app) + app = App.get_app!(app.id) + assert is_nil(app) + end + + test "change_app/1 returns a app changeset" do + app = app_fixture() + assert %Ecto.Changeset{} = App.change_app(app) + end + end +end diff --git a/test/auth/people_roles_test.exs b/test/auth/people_roles_test.exs new file mode 100644 index 00000000..48a6526c --- /dev/null +++ b/test/auth/people_roles_test.exs @@ -0,0 +1,52 @@ +defmodule AuthWeb.PeopleRolesTest do + use Auth.DataCase + + test "Auth.PeopleRoles.insert/3 happy path" do + # create a new person: + alex = %{email: "alex_grant_role@gmail.com", auth_provider: "email"} + grantee = Auth.Person.create_person(alex) + role_id = 4 + # grant the "creator" role (id: 4) to the new person: + Auth.PeopleRoles.insert(1, grantee.id, role_id) + + # confirm people_roles record exists: + record = Auth.PeopleRoles.get_record(grantee.id, role_id) + assert record.person_id == grantee.id + assert record.role_id == role_id + assert record.granter_id == 1 + + # confirm person record has roles preloaded: + person_with_role = Auth.Person.get_person_by_id(grantee.id) + roles = RBAC.transform_role_list_to_string(person_with_role.roles) + assert roles =~ Integer.to_string(role_id) + + # check the latest people_roles record: + list = Auth.PeopleRoles.list_people_roles() + pr = List.last(list) + assert pr.granter_id == 1 + assert pr.person_id == grantee.id + end + + test "Auth.PeopleRoles.revoke/2 revokes a role" do + # create a new person: + alex = %{email: "alex_revoke@gmail.com", auth_provider: "email"} + grantee = Auth.Person.create_person(alex) + role_id = 3 + # grant the role to the new person: + Auth.PeopleRoles.insert(1, grantee.id, role_id) + + # confirm people_roles record exists: + record = Auth.PeopleRoles.get_record(grantee.id, role_id) + assert record.person_id == grantee.id + assert record.role_id == role_id + assert record.granter_id == 1 + + # revoke the role! + Auth.PeopleRoles.revoke(1, record.id) + + # confirm people_roles record was updated: + record = Auth.PeopleRoles.get_record(grantee.id, role_id) + assert record.revoker_id == 1 + assert not is_nil(record.revoked) + end +end diff --git a/test/auth/role_test.exs b/test/auth/role_test.exs new file mode 100644 index 00000000..ae2e4bb4 --- /dev/null +++ b/test/auth/role_test.exs @@ -0,0 +1,164 @@ +defmodule Auth.RoleTest do + use Auth.DataCase + # use AuthWeb.ConnCase + + describe "roles" do + alias Auth.Role + + @valid_attrs %{desc: "some desc", name: "some name"} + @update_attrs %{desc: "some updated desc", name: "some updated name"} + @invalid_attrs %{desc: nil, name: nil} + + def role_fixture(attrs \\ %{}) do + {:ok, role} = + attrs + |> Enum.into(@valid_attrs) + |> Role.create_role() + + role + end + + test "list_roles/0 returns all roles" do + role = role_fixture() + assert List.last(Role.list_roles()) == role + end + + test "get_role!/1 returns the role with given id" do + role = role_fixture() + assert Role.get_role!(role.id) == role + end + + test "create_role/1 with valid data creates a role" do + assert {:ok, %Role{} = role} = Role.create_role(@valid_attrs) + assert role.desc == "some desc" + assert role.name == "some name" + end + + test "create_role/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Role.create_role(@invalid_attrs) + end + + test "update_role/2 with valid data updates the role" do + role = role_fixture() + assert {:ok, %Role{} = role} = Role.update_role(role, @update_attrs) + assert role.desc == "some updated desc" + assert role.name == "some updated name" + end + + test "update_role/2 with invalid data returns error changeset" do + role = role_fixture() + assert {:error, %Ecto.Changeset{}} = Role.update_role(role, @invalid_attrs) + assert role == Role.get_role!(role.id) + end + + test "delete_role/1 deletes the role" do + role = role_fixture() + assert {:ok, %Role{}} = Role.delete_role(role) + assert_raise Ecto.NoResultsError, fn -> Role.get_role!(role.id) end + end + + test "change_role/1 returns a role changeset" do + role = role_fixture() + assert %Ecto.Changeset{} = Role.change_role(role) + end + + test "upsert_role/1 inserts or updates a role" do + # role without :id -> create_role + assert {:ok, %Role{} = role} = Role.upsert_role(@valid_attrs) + assert role.desc == "some desc" + assert role.name == "some name" + + # existing role with id -> update_role + updated_role = Map.merge(role, @update_attrs) + assert {:ok, %Role{} = role} = Role.upsert_role(updated_role) + assert role.desc == "some updated desc" + assert role.name == "some updated name" + + # attempt to "upsert" a role that does not exist: + non_existent_role = %{name: "hello", desc: "world", id: 1492} + assert {:ok, %Role{} = role} = Role.upsert_role(non_existent_role) + assert role.name == "hello" + assert role.id != non_existent_role.id + end + end + + describe "permissions" do + alias Auth.Permission + + @valid_attrs %{desc: "some desc", name: "some name"} + @update_attrs %{desc: "some updated desc", name: "some updated name"} + @invalid_attrs %{desc: nil, name: nil} + + def permission_fixture(attrs \\ %{}) do + {:ok, permission} = + attrs + |> Enum.into(@valid_attrs) + |> Permission.create_permission() + + permission + end + + test "list_permissions/0 returns all permissions" do + permission = permission_fixture() + assert Permission.list_permissions() == [permission] + end + + test "get_permission!/1 returns the permission with given id" do + permission = permission_fixture() + assert Permission.get_permission!(permission.id) == permission + end + + test "create_permission/1 with valid data creates a permission" do + assert {:ok, %Permission{} = permission} = Permission.create_permission(@valid_attrs) + assert permission.desc == "some desc" + assert permission.name == "some name" + end + + test "create_permission/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Permission.create_permission(@invalid_attrs) + end + + test "update_permission/2 with valid data updates the permission" do + permission = permission_fixture() + + assert {:ok, %Permission{} = permission} = + Permission.update_permission(permission, @update_attrs) + + assert permission.desc == "some updated desc" + assert permission.name == "some updated name" + end + + test "update_permission/2 with invalid data returns error changeset" do + permission = permission_fixture() + + assert {:error, %Ecto.Changeset{}} = + Permission.update_permission(permission, @invalid_attrs) + + assert permission == Permission.get_permission!(permission.id) + end + + test "delete_permission/1 deletes the permission" do + permission = permission_fixture() + assert {:ok, %Permission{}} = Permission.delete_permission(permission) + + assert_raise Ecto.NoResultsError, fn -> + Permission.get_permission!(permission.id) + end + end + + test "change_permission/1 returns a permission changeset" do + permission = permission_fixture() + assert %Ecto.Changeset{} = Permission.change_permission(permission) + end + end + + # create a new person and confirm they were asigned a default role of "subscriber" + + # describe "grant role" do + + # # test "change_permission/1 returns a permission changeset" do + # # permission = permission_fixture() + # # assert %Ecto.Changeset{} = Permission.change_permission(permission) + # # end + # end +end diff --git a/test/auth/status_test.exs b/test/auth/status_test.exs index 76addb66..a71018d7 100644 --- a/test/auth/status_test.exs +++ b/test/auth/status_test.exs @@ -3,10 +3,10 @@ defmodule Auth.StatusTest do alias Auth.{Status} test "upsert_status/1 inserts or updates a status record" do - status = Status.upsert_status("verified") + status = Status.upsert_status(%{"text" => "verified"}) assert status.id == 1 - new_status = Status.upsert_status("amaze") - assert new_status.id == 2 + new_status = Status.upsert_status(%{"text" => "amaze"}) + assert new_status.id == 12 end end diff --git a/test/auth_web/controllers/apikey_controller_test.exs b/test/auth_web/controllers/apikey_controller_test.exs deleted file mode 100644 index 2f4d26d6..00000000 --- a/test/auth_web/controllers/apikey_controller_test.exs +++ /dev/null @@ -1,241 +0,0 @@ -defmodule AuthWeb.ApikeyControllerTest do - use AuthWeb.ConnCase - use ExUnitProperties - - # alias Auth.Apikey - # alias AuthWeb.ApikeyController, as: Ctrl - @email System.get_env("ADMIN_EMAIL") - @create_attrs %{description: "some description", name: "some name", url: "some url"} - @update_attrs %{ - client_secret: "updated client sec", - description: "some updated desc", - name: "updated name", - url: "surl" - } - @invalid_attrs %{client_secret: nil, description: nil, key_id: nil, name: nil, url: nil} - - describe "Create an AUTH_API_KEY for a given person_id" do - test "encrypt_encode/1 returns a base58 we can decrypt" do - person_id = 1 - key = AuthWeb.ApikeyController.encrypt_encode(person_id) - - decrypted = - key - |> Base58.decode() - |> Fields.AES.decrypt() - |> String.to_integer() - - assert decrypted == person_id - end - - test "decode_decrypt/1 reverses the operation of encrypt_encode/1" do - person_id = 4_869_234_521 - key = AuthWeb.ApikeyController.encrypt_encode(person_id) - id = AuthWeb.ApikeyController.decode_decrypt(key) - assert person_id == id - end - - test "create_api_key/1 creates an AUTH_API_KEY" do - person_id = 123_456_789 - key = AuthWeb.ApikeyController.create_api_key(person_id) - assert key =~ "/" - end - - test "decrypt_api_key/1 decrypts an AUTH_API_KEY" do - person_id = 1234 - key = AuthWeb.ApikeyController.create_api_key(person_id) - decrypted = AuthWeb.ApikeyController.decrypt_api_key(key) - assert decrypted == person_id - end - - test "decode_decrypt/1 with invalid client_id" do - valid_key = AuthWeb.ApikeyController.encrypt_encode(1) - person_id = AuthWeb.ApikeyController.decode_decrypt(valid_key) - assert person_id == 1 - - invalid_key = String.slice(valid_key, 0..-2) - error = AuthWeb.ApikeyController.decode_decrypt(invalid_key) - assert error == 0 - end - - property "Check a batch of int values can be decoded decode_decrypt/1" do - check all(int <- integer()) do - assert AuthWeb.ApikeyController.decode_decrypt( - AuthWeb.ApikeyController.encrypt_encode(int) - ) == int - end - end - end - - # def fixture(:apikey) do - # {:ok, apikey} = Ctx.create_apikey(@create_attrs) - # apikey - # end - # - describe "index" do - test "lists all apikeys", %{conn: conn} do - person = Auth.Person.get_person_by_email(@email) - conn = AuthPlug.create_jwt_session(conn, %{email: @email, id: person.id}) - |> get(Routes.apikey_path(conn, :index)) - - assert html_response(conn, 200) =~ "Auth API Keys" - end - end - - describe "new apikey" do - test "renders form", %{conn: conn} do - person = Auth.Person.get_person_by_email(@email) - - conn = AuthPlug.create_jwt_session(conn, %{email: @email, id: person.id}) - |> get(Routes.apikey_path(conn, :new)) - - assert html_response(conn, 200) =~ "New Apikey" - end - end - - describe "create apikey" do - test "redirects to show when data is valid", %{conn: conn} do - person = Auth.Person.get_person_by_email(@email) - conn = AuthPlug.create_jwt_session(conn, person) - |> post(Routes.apikey_path(conn, :create), apikey: @create_attrs) - - assert %{id: id} = redirected_params(conn) - assert redirected_to(conn) == Routes.apikey_path(conn, :show, id) - - conn = get(conn, Routes.apikey_path(conn, :show, id)) - assert html_response(conn, 200) =~ "Your AUTH_API_KEY" - end - - # test "renders errors when data is invalid", %{conn: conn} do - # person = Auth.Person.get_person_by_email(@email) - # conn = AuthPlug.create_jwt_session(conn, %{email: @email, id: person.id}) - # - # conn = post(conn, Routes.apikey_path(conn, :create), apikey: @invalid_attrs) - # assert html_response(conn, 200) =~ "New Apikey" - # end - end - - describe "edit apikey" do - test "renders form for editing chosen apikey", %{conn: conn} do - person = Auth.Person.get_person_by_email(@email) - conn = AuthPlug.create_jwt_session(conn, person) - - {:ok, key} = - %{"name" => "test key", "url" => "http://localhost:4000"} - |> AuthWeb.ApikeyController.make_apikey(person.id) - |> Auth.Apikey.create_apikey() - - conn = get(conn, Routes.apikey_path(conn, :edit, key.id)) - assert html_response(conn, 200) =~ "Edit Apikey" - end - - test "attempt to edit a key I don't own > should 404", %{conn: conn} do - person = Auth.Person.get_person_by_email(@email) - - wrong_person = - Auth.Person.create_person(%{ - email: "wrong@gmail.com", - auth_provider: "email" - }) - - conn = AuthPlug.create_jwt_session(conn, wrong_person) - - {:ok, key} = - %{"name" => "test key", "url" => "http://localhost:4000"} - |> AuthWeb.ApikeyController.make_apikey(person.id) - |> Auth.Apikey.create_apikey() - - conn = get(conn, Routes.apikey_path(conn, :edit, key.id)) - assert html_response(conn, 404) =~ "not found" - end - end - - describe "update apikey" do - test "redirects when data is valid", %{conn: conn} do - person = Auth.Person.get_person_by_email(@email) - conn = AuthPlug.create_jwt_session(conn, person) - - {:ok, key} = - %{"name" => "test key", "url" => "http://localhost:4000"} - |> AuthWeb.ApikeyController.make_apikey(person.id) - |> Auth.Apikey.create_apikey() - - conn = put(conn, Routes.apikey_path(conn, :update, key.id), apikey: @update_attrs) - assert redirected_to(conn) == Routes.apikey_path(conn, :show, key) - - conn = get(conn, Routes.apikey_path(conn, :show, key)) - assert html_response(conn, 200) =~ "some updated desc" - end - - test "renders errors when data is invalid", %{conn: conn} do - person = Auth.Person.get_person_by_email(@email) - conn = AuthPlug.create_jwt_session(conn, person) - - {:ok, key} = - %{"name" => "test key", "url" => "http://localhost:4000"} - |> AuthWeb.ApikeyController.make_apikey(person.id) - |> Auth.Apikey.create_apikey() - - conn = put(conn, Routes.apikey_path(conn, :update, key), apikey: @invalid_attrs) - assert html_response(conn, 200) =~ "Edit Apikey" - end - - test "attempt to UPDATE a key I don't own > should 404", %{conn: conn} do - person = Auth.Person.get_person_by_email(@email) - # create session with wrong person: - wrong_person = - Auth.Person.create_person(%{ - email: "wronger@gmail.com", - auth_provider: "email" - }) - - conn = AuthPlug.create_jwt_session(conn, wrong_person) - - {:ok, key} = - %{"name" => "test key", "url" => "http://localhost:4000", "person_id" => person.id} - |> AuthWeb.ApikeyController.make_apikey(person.id) - |> Auth.Apikey.create_apikey() - - conn = put(conn, Routes.apikey_path(conn, :update, key.id), apikey: @update_attrs) - assert html_response(conn, 404) =~ "not found" - end - end - - describe "delete apikey" do - test "deletes chosen apikey", %{conn: conn} do - person = Auth.Person.get_person_by_email(@email) - conn = AuthPlug.create_jwt_session(conn, person) - - {:ok, key} = - %{"name" => "test key", "url" => "http://localhost:4000"} - |> AuthWeb.ApikeyController.make_apikey(person.id) - |> Auth.Apikey.create_apikey() - - conn = delete(conn, Routes.apikey_path(conn, :delete, key)) - assert redirected_to(conn) == Routes.apikey_path(conn, :index) - - assert_error_sent 404, fn -> - get(conn, Routes.apikey_path(conn, :show, key)) - end - end - - test "cannot delete a key belonging to someone else! 404", %{conn: conn} do - wrong_person = - Auth.Person.create_person(%{ - email: "wrongin@gmail.com", - auth_provider: "email" - }) - - conn = AuthPlug.create_jwt_session(conn, wrong_person) - person = Auth.Person.get_person_by_email(@email) - - {:ok, key} = - %{"name" => "test key", "url" => "http://localhost:4000"} - |> AuthWeb.ApikeyController.make_apikey(person.id) - |> Auth.Apikey.create_apikey() - - conn = delete(conn, Routes.apikey_path(conn, :delete, key)) - assert html_response(conn, 404) =~ "not found" - end - end -end diff --git a/test/auth_web/controllers/app_controller_test.exs b/test/auth_web/controllers/app_controller_test.exs new file mode 100644 index 00000000..8db828d5 --- /dev/null +++ b/test/auth_web/controllers/app_controller_test.exs @@ -0,0 +1,248 @@ +defmodule AuthWeb.AppControllerTest do + use AuthWeb.ConnCase + alias Auth.App + alias Auth.Role + + @create_attrs %{ + desc: "some description", + end: ~N[2010-04-17 14:00:00], + name: "some name", + url: "some url", + status: 3, + person_id: 1 + } + @update_attrs %{ + desc: "some updated description", + end: ~N[2011-05-18 15:01:01], + name: "some updated name", + url: "some updated url" + } + @invalid_attrs %{description: nil, end: nil, name: nil, url: nil, person_id: nil} + + def fixture(:app) do + {:ok, app} = App.create_app(@create_attrs) + app + end + + describe "index" do + setup [:create_app] + + test "lists all apps", %{conn: conn} do + conn = admin_login(conn) + conn = get(conn, Routes.app_path(conn, :index)) + assert html_response(conn, 200) =~ "Apps" + end + + test "non-admin cannot see admin apps", %{conn: conn, app: app} do + conn = non_admin_login(conn) + conn = get(conn, Routes.app_path(conn, :index)) + assert html_response(conn, 200) =~ "Apps" + # the non-admin cannot see the app created in setup: + assert not String.contains?(conn.resp_body, app.name) + end + end + + describe "new app" do + test "renders form", %{conn: conn} do + conn = admin_login(conn) + conn = get(conn, Routes.app_path(conn, :new)) + assert html_response(conn, 200) =~ "New App" + end + end + + describe "create app" do + test "redirects to show when data is valid", %{conn: conn} do + conn = admin_login(conn) + conn = post(conn, Routes.app_path(conn, :create), app: @create_attrs) + + assert %{id: id} = redirected_params(conn) + assert redirected_to(conn) == Routes.app_path(conn, :show, id) + + conn = get(conn, Routes.app_path(conn, :show, id)) + assert html_response(conn, 200) =~ "App" + end + + test "renders errors when data is invalid", %{conn: conn} do + conn = admin_login(conn) + conn = post(conn, Routes.app_path(conn, :create), app: @invalid_attrs) + assert html_response(conn, 200) =~ "New App" + end + end + + describe "show app" do + setup [:create_app] + + test "attempt to VIEW app you don't own > 404", %{conn: conn, app: app} do + conn = non_admin_login(conn) + conn = get(conn, Routes.app_path(conn, :show, app)) + assert html_response(conn, 404) =~ "can't touch this." + end + end + + describe "edit app" do + setup [:create_app] + + test "renders form for editing chosen app", %{conn: conn, app: app} do + conn = admin_login(conn) + conn = get(conn, Routes.app_path(conn, :edit, app)) + assert html_response(conn, 200) =~ "Edit App" + end + + test "attempt to EDIT app you don't own > 404", %{conn: conn, app: app} do + conn = non_admin_login(conn) + conn = get(conn, Routes.app_path(conn, :edit, app)) + assert html_response(conn, 404) =~ "can't touch this." + end + end + + describe "update app" do + setup [:create_app] + + test "redirects when data is valid", %{conn: conn, app: app} do + conn = admin_login(conn) + conn = put(conn, Routes.app_path(conn, :update, app), app: @update_attrs) + assert redirected_to(conn) == Routes.app_path(conn, :show, app) + + conn = get(conn, Routes.app_path(conn, :show, app)) + assert html_response(conn, 200) + end + + test "renders errors when data is invalid", %{conn: conn, app: app} do + conn = admin_login(conn) + conn = put(conn, Routes.app_path(conn, :update, app), app: @invalid_attrs) + assert html_response(conn, 200) =~ "Edit App" + end + + test "attempt UPDATE app you don't own > 404", %{conn: conn, app: app} do + conn = non_admin_login(conn) + conn = put(conn, Routes.app_path(conn, :update, app), app: @update_attrs) + assert html_response(conn, 404) =~ "can't touch this." + end + end + + describe "delete app" do + setup [:create_app] + + test "deletes chosen app", %{conn: conn, app: app} do + conn = admin_login(conn) + conn = delete(conn, Routes.app_path(conn, :delete, app)) + assert redirected_to(conn) == Routes.app_path(conn, :index) + + assert_error_sent 500, fn -> + get(conn, Routes.app_path(conn, :show, app)) + end + end + + test "attempt DELETE app you don't own > 404", %{conn: conn, app: app} do + conn = non_admin_login(conn) + conn = delete(conn, Routes.app_path(conn, :delete, app)) + assert html_response(conn, 404) =~ "can't touch this." + end + end + + defp create_app(_) do + app = fixture(:app) + %{app: app} + end + + describe "reset apikey" do + setup [:create_app] + + test "reset apikey for an app", %{conn: conn, app: app} do + conn = admin_login(conn) + conn = get(conn, Routes.app_path(conn, :resetapikey, app)) + assert html_response(conn, 200) =~ "successfully reset" + end + + test "attempt reset apikey you don't own > 404", %{conn: conn, app: app} do + conn = non_admin_login(conn) + conn = get(conn, Routes.app_path(conn, :resetapikey, app)) + assert html_response(conn, 404) =~ "can't touch this." + end + end + + describe "GET /approles/:client_id" do + setup [:create_app] + + test "returns 401 if client_id is invalid", %{conn: conn} do + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/approles/invalid") + + assert html_response(conn, 401) =~ "invalid" + end + + test "returns (JSON) list of roles", %{conn: conn, app: app} do + roles = Auth.Role.list_roles_for_app(app.id) + key = List.first(app.apikeys) + # IO.inspect(app, label: "app") + conn = + conn + |> admin_login() + |> put_req_header("accept", "application/json") + |> get("/approles/#{key.client_id}") + + assert conn.status == 200 + {:ok, json} = Jason.decode(conn.resp_body) + # IO.inspect(json) + assert length(roles) == length(json) + # assert html_response(conn, 200) =~ "successfully reset" + end + + test "returns only relevant roles", %{conn: conn, app: app} do + roles = Role.list_roles_for_app(app.id) + # admin create role: + admin_role = %{desc: "admin role", name: "new admin role", app_id: app.id} + {:ok, %Role{} = admin_role} = Role.create_role(admin_role) + # check that the new role was added to the admin app role list: + roles2 = Role.list_roles_for_app(app.id) + assert length(roles) < length(roles2) + last = List.last(roles2) + assert last.name == admin_role.name + + # login as non-admin person + conn2 = non_admin_login(conn) + + # create non-admin app (to get API Key) + {:ok, non_admin_app} = + Auth.App.create_app(%{ + "name" => "default system app", + "desc" => "Demo App", + "url" => "localhost:4000", + "person_id" => conn2.assigns.person.id, + "status" => 3 + }) + + # create non-admin role: + role_data = %{ + desc: "non-admin role", + name: "non-admin role", + app_id: non_admin_app.id + } + + {:ok, %Role{} = role2} = Role.create_role(role_data) + key = List.first(non_admin_app.apikeys) + + conn3 = + conn2 + |> admin_login() + |> put_req_header("accept", "application/json") + |> get("/approles/#{key.client_id}") + + assert conn3.status == 200 + {:ok, json} = Jason.decode(conn3.resp_body) + last_role = List.last(json) + # confirm the last role in the list is the new non-admin role: + assert Map.get(last_role, "name") == role2.name + + # confirm the admin_role is NOT in the JSON reponse: + should_be_empty = + Enum.filter(json, fn r -> + Map.get(r, "name") == admin_role.name + end) + + assert should_be_empty == [] + end + end +end diff --git a/test/auth_web/controllers/auth_controller_test.exs b/test/auth_web/controllers/auth_controller_test.exs index 1925d4d5..9e84c486 100644 --- a/test/auth_web/controllers/auth_controller_test.exs +++ b/test/auth_web/controllers/auth_controller_test.exs @@ -2,6 +2,13 @@ defmodule AuthWeb.AuthControllerTest do use AuthWeb.ConnCase # @email System.get_env("ADMIN_EMAIL") + @app_data %{ + "name" => "example key", + "url" => "https://www.example.com", + "person_id" => 1, + "status" => 3 + } + test "GET /", %{conn: conn} do conn = get(conn, "/") assert html_response(conn, 200) =~ "Sign in" @@ -22,8 +29,10 @@ defmodule AuthWeb.AuthControllerTest do } person = Auth.Person.create_person(data) - conn = AuthPlug.create_jwt_session(conn, Map.merge(data, %{id: person.id})) - |> get("/profile", %{}) + + conn = + AuthPlug.create_jwt_session(conn, Map.merge(data, %{id: person.id})) + |> get("/profile", %{}) assert html_response(conn, 200) =~ "Google account" end @@ -49,6 +58,49 @@ defmodule AuthWeb.AuthControllerTest do assert conn.resp_body =~ "state=http://localhost/admin" end + test "get_client_secret(client_id, state) gets the secret for the given client_id" do + {:ok, app} = Auth.App.create_app(@app_data) + key = List.first(app.apikeys) + state = "https://www.example.com/profile?auth_client_id=#{key.client_id}" + secret = AuthWeb.AuthController.get_client_secret(key.client_id, state) + assert secret == key.client_secret + end + + test "get_client_secret/2 for non_admin key" do + person = non_admin_person() + app_data = Map.merge(@app_data, %{"person_id" => person.id}) + {:ok, app} = Auth.App.create_app(app_data) + key = List.first(app.apikeys) + state = "#{app.url}/profile?auth_client_id=#{key.client_id}" + secret = AuthWeb.AuthController.get_client_secret(key.client_id, state) + assert secret == key.client_secret + end + + test "get_client_secret(client_id, state) for 'deleted' apikey (non-admin)" do + person = non_admin_person() + {:ok, app} = Auth.App.create_app(Map.merge(@app_data, %{"person_id" => person.id})) + key = List.first(app.apikeys) + Auth.Apikey.update_apikey(Map.delete(key, :app), %{status: 6}) + state = "#{app.url}/profile?auth_client_id=#{key.client_id}" + # Note: not sure what to assert here ... ¯\_(ツ)_/¯ + # The API Key is "deleted" so it won't be found in the lookup + try do + AuthWeb.AuthController.get_client_secret(key.client_id, state) + rescue + e in BadMapError -> assert e == %BadMapError{term: nil} + end + end + + # test "redirect_or_render assigns app_admin role if direct auth", %{conn: conn} do + # conn = non_admin_login(conn) + # IO.inspect(conn, label: "conn:96") + # person = conn.assigns.person + # IO.inspect(person, label: "person:98") + + # AuthWeb.AuthController.redirect_or_render(conn, person, "") + # IO.inspect(conn, label: "conn:100") + # end + test "github_handler/2 github auth callback", %{conn: conn} do baseurl = AuthPlug.Helpers.get_baseurl_from_conn(conn) @@ -82,10 +134,8 @@ defmodule AuthWeb.AuthControllerTest do test "google_handler/2 show welcome page", %{conn: conn} do # Google Auth Mock makes the state https://www.example.com # so we need to create a new API_KEY with that url: - {:ok, key} = - %{"name" => "example key", "url" => "https://www.example.com"} - |> AuthWeb.ApikeyController.make_apikey(1) - |> Auth.Apikey.create_apikey() + {:ok, app} = Auth.App.create_app(@app_data) + key = List.first(app.apikeys) conn = get(conn, "/auth/google/callback", %{ @@ -107,10 +157,11 @@ defmodule AuthWeb.AuthControllerTest do auth_provider: "google" } - person = Auth.Person.upsert_person(data) + Auth.Person.upsert_person(data) - conn = AuthPlug.create_jwt_session(conn, person) - |> get("/auth/google/callback", %{"code" => "234", "state" => nil}) + conn = + AuthPlug.create_jwt_session(conn, data) + |> get("/auth/google/callback", %{"code" => "234", "state" => nil}) assert html_response(conn, 200) =~ "Google account" end @@ -262,12 +313,12 @@ defmodule AuthWeb.AuthControllerTest do end test "password_create/2 create a new password", %{conn: conn} do - %{email: "anabela@mail.com", auth_provider: "email"} + %{email: "anabela@mail.com", auth_provider: "email", givenName: "timmy"} |> Auth.Person.upsert_person() params = %{ "person" => %{ - "email" => AuthWeb.ApikeyController.encrypt_encode("anabela@mail.com"), + "email" => Auth.Apikey.encrypt_encode("anabela@mail.com"), "password" => "thiswillbehashed" } } @@ -279,7 +330,7 @@ defmodule AuthWeb.AuthControllerTest do test "password_create/2 display form when password not valid", %{conn: conn} do params = %{ "person" => %{ - "email" => AuthWeb.ApikeyController.encrypt_encode("anabela@mail.com"), + "email" => Auth.Apikey.encrypt_encode("anabela@mail.com"), "password" => "short" } } @@ -299,6 +350,7 @@ defmodule AuthWeb.AuthControllerTest do link = AuthWeb.AuthController.make_verify_link(conn, person, state) link = "/auth/verify" <> List.last(String.split(link, "/auth/verify")) + conn = get(conn, link, %{}) assert html_response(conn, 302) =~ "redirected" end @@ -319,7 +371,7 @@ defmodule AuthWeb.AuthControllerTest do params = %{ "person" => %{ - "email" => AuthWeb.ApikeyController.encrypt_encode(data.email), + "email" => Auth.Apikey.encrypt_encode(data.email), "password" => "thiswillbehashed", "state" => state } @@ -345,7 +397,7 @@ defmodule AuthWeb.AuthControllerTest do params = %{ "person" => %{ - "email" => AuthWeb.ApikeyController.encrypt_encode(data.email), + "email" => Auth.Apikey.encrypt_encode(data.email), "password" => "fail", "state" => state } diff --git a/test/auth_web/controllers/people_controller_test.exs b/test/auth_web/controllers/people_controller_test.exs new file mode 100644 index 00000000..cf720348 --- /dev/null +++ b/test/auth_web/controllers/people_controller_test.exs @@ -0,0 +1,56 @@ +defmodule AuthWeb.PeopleControllerTest do + use AuthWeb.ConnCase + # @email System.get_env("ADMIN_EMAIL") + + test "GET /people displays list of people", %{conn: conn} do + conn = get(admin_login(conn), "/people") + assert html_response(conn, 200) =~ "People Authenticated" + end + + test "Attempt to GET /people not unathenticated", %{conn: conn} do + conn = get(conn, "/people") + assert conn.status == 302 + end + + test "Attempt to GET /people without admin role should 404", %{conn: conn} do + wrong_person_data = %{ + email: "not_admin@gmail.com", + auth_provider: "email", + id: 42 + } + + Auth.Person.create_person(wrong_person_data) + conn = AuthPlug.create_jwt_session(conn, wrong_person_data) + + conn = get(conn, "/people") + assert conn.status == 404 + end + + test "GET /people/:person_id displays person", %{conn: conn} do + conn = get(admin_login(conn), "/people/1") + assert html_response(conn, 200) =~ "Roles" + end + + test "AuthWeb.PeopleView.status_string/2" do + statuses = [%{text: "verified", id: 1}] + assert AuthWeb.PeopleView.status_string(1, statuses) == "verified" + end + + test "AuthWeb.PeopleView.status_string/2 if status_id is nil" do + assert AuthWeb.PeopleView.status_string(nil, []) == "none" + end + + test "AuthWeb.PeopleController.show/2 unauthorized if not admin", %{conn: conn} do + wrong_person_data = %{ + email: "unauthorized@gmail.com", + auth_provider: "email", + id: 42 + } + + Auth.Person.create_person(wrong_person_data) + conn = AuthPlug.create_jwt_session(conn, wrong_person_data) + + conn = AuthWeb.PeopleController.show(conn, %{"people_roles_id" => 1}) + assert conn.status == 401 + end +end diff --git a/test/auth_web/controllers/permission_controller_test.exs b/test/auth_web/controllers/permission_controller_test.exs new file mode 100644 index 00000000..3181c7e7 --- /dev/null +++ b/test/auth_web/controllers/permission_controller_test.exs @@ -0,0 +1,106 @@ +defmodule AuthWeb.PermissionControllerTest do + use AuthWeb.ConnCase + + alias Auth.Permission + + @create_attrs %{desc: "some desc", name: "some name"} + @update_attrs %{desc: "some updated desc", name: "some updated name"} + @invalid_attrs %{desc: nil, name: nil} + + def fixture(:permission) do + {:ok, permission} = Permission.create_permission(@create_attrs) + permission + end + + describe "index" do + test "lists all permissions", %{conn: conn} do + conn = admin_login(conn) + conn = get(conn, Routes.permission_path(conn, :index)) + assert html_response(conn, 200) =~ "Listing Permissions" + end + end + + describe "new permission" do + test "renders form", %{conn: conn} do + conn = admin_login(conn) + conn = get(conn, Routes.permission_path(conn, :new)) + assert html_response(conn, 200) =~ "New Permission" + end + end + + describe "create permission" do + test "redirects to show when data is valid", %{conn: conn} do + conn = admin_login(conn) + conn = post(conn, Routes.permission_path(conn, :create), permission: @create_attrs) + + assert %{id: id} = redirected_params(conn) + assert redirected_to(conn) == Routes.permission_path(conn, :show, id) + + conn = get(conn, Routes.permission_path(conn, :show, id)) + assert html_response(conn, 200) =~ "Show Permission" + end + + test "renders errors when data is invalid", %{conn: conn} do + conn = admin_login(conn) + conn = post(conn, Routes.permission_path(conn, :create), permission: @invalid_attrs) + + assert html_response(conn, 200) =~ "New Permission" + end + end + + describe "edit permission" do + setup [:create_permission] + + test "renders form for editing chosen permission", %{conn: conn, permission: permission} do + conn = admin_login(conn) + conn = get(conn, Routes.permission_path(conn, :edit, permission)) + + assert html_response(conn, 200) =~ "Edit Permission" + end + end + + describe "update permission" do + setup [:create_permission] + + test "redirects when data is valid", %{conn: conn, permission: permission} do + conn = admin_login(conn) + + conn = + put(conn, Routes.permission_path(conn, :update, permission), permission: @update_attrs) + + assert redirected_to(conn) == Routes.permission_path(conn, :show, permission) + + conn = get(conn, Routes.permission_path(conn, :show, permission)) + assert html_response(conn, 200) =~ "some updated desc" + end + + test "renders errors when data is invalid", %{conn: conn, permission: permission} do + conn = admin_login(conn) + + conn = + put(conn, Routes.permission_path(conn, :update, permission), permission: @invalid_attrs) + + assert html_response(conn, 200) =~ "Edit Permission" + end + end + + describe "delete permission" do + setup [:create_permission] + + test "deletes chosen permission", %{conn: conn, permission: permission} do + conn = admin_login(conn) + conn = delete(conn, Routes.permission_path(conn, :delete, permission)) + + assert redirected_to(conn) == Routes.permission_path(conn, :index) + + assert_error_sent 404, fn -> + get(conn, Routes.permission_path(conn, :show, permission)) + end + end + end + + defp create_permission(_) do + permission = fixture(:permission) + %{permission: permission} + end +end diff --git a/test/auth_web/controllers/ping_controller_test.exs b/test/auth_web/controllers/ping_controller_test.exs new file mode 100644 index 00000000..cc536c58 --- /dev/null +++ b/test/auth_web/controllers/ping_controller_test.exs @@ -0,0 +1,10 @@ +defmodule AuthWeb.PingControllerTest do + use AuthWeb.ConnCase + + test "GET /ping (GIF) renders 1x1 pixel", %{conn: conn} do + conn = get(conn, Routes.ping_path(conn, :ping)) + assert conn.status == 200 + assert conn.state == :sent + assert conn.resp_body =~ <<71, 73, 70, 56, 57>> + end +end diff --git a/test/auth_web/controllers/role_controller_test.exs b/test/auth_web/controllers/role_controller_test.exs new file mode 100644 index 00000000..5c801263 --- /dev/null +++ b/test/auth_web/controllers/role_controller_test.exs @@ -0,0 +1,242 @@ +defmodule AuthWeb.RoleControllerTest do + use AuthWeb.ConnCase + + alias Auth.Role + + @create_attrs %{desc: "some desc", name: "some name", app_id: "1", person_id: 1} + @update_attrs %{desc: "some updated desc", name: "some updated name"} + @invalid_attrs %{desc: nil, name: nil} + + def fixture(:role) do + {:ok, role} = Role.create_role(@create_attrs) + role + end + + describe "index" do + test "lists all roles", %{conn: conn} do + conn = admin_login(conn) + # create a new role to exercise the RoleView.app_link/1 fn: + attrs = Map.merge(@create_attrs, %{person_id: conn.assigns.person.id}) + {:ok, _role} = Auth.Role.create_role(attrs) + + conn = get(conn, Routes.role_path(conn, :index)) + assert html_response(conn, 200) =~ "Listing Roles" + end + + test "non-admin can see all system and owned roles", %{conn: conn} do + conn = non_admin_login(conn) + conn = post(conn, Routes.role_path(conn, :create), role: @create_attrs) + conn = get(conn, Routes.role_path(conn, :index)) + assert html_response(conn, 200) =~ "Listing Roles" + end + end + + describe "new role" do + test "renders form", %{conn: conn} do + conn = admin_login(conn) + + conn = get(conn, Routes.role_path(conn, :new)) + assert html_response(conn, 200) =~ "New Role" + end + + test "attempt to /roles/new without App redirects to /apps/new", %{conn: conn} do + conn = non_admin_login(conn) + conn = get(conn, Routes.role_path(conn, :new)) + assert html_response(conn, 302) =~ "redirected" + end + + test "non-admin person create role", %{conn: conn} do + conn = non_admin_login(conn) + + {:ok, _app} = + Auth.App.create_app(%{ + "name" => "default system app", + "desc" => "Demo App", + "url" => "localhost:4000", + "person_id" => conn.assigns.person.id, + "status" => 3 + }) + + conn = get(conn, Routes.role_path(conn, :new)) + assert html_response(conn, 200) =~ "New Role" + end + end + + describe "create role" do + test "redirects to show when data is valid", %{conn: conn} do + conn = admin_login(conn) + conn = post(conn, Routes.role_path(conn, :create), role: @create_attrs) + + assert %{id: id} = redirected_params(conn) + assert redirected_to(conn) == Routes.role_path(conn, :show, id) + + conn = get(conn, Routes.role_path(conn, :show, id)) + assert html_response(conn, 200) =~ @create_attrs.name + end + + test "renders errors when data is invalid", %{conn: conn} do + conn = admin_login(conn) + conn = post(conn, Routes.role_path(conn, :create), role: @invalid_attrs) + + assert html_response(conn, 404) =~ "New Role" + end + + test "renders errors when data is invalid (with app_id)", %{conn: conn} do + conn = admin_login(conn) + # invalid but with app_id: + invalid = Map.merge(@invalid_attrs, %{"app_id" => "1"}) + conn = post(conn, Routes.role_path(conn, :create), role: invalid) + + assert html_response(conn, 200) =~ "New Role" + end + + test "attempt to create role for app they don't own.", %{conn: conn} do + conn = non_admin_login(conn) + conn = post(conn, Routes.role_path(conn, :create), role: @create_attrs) + + assert html_response(conn, 404) =~ "Please select an app you own" + end + end + + describe "edit role" do + setup [:create_role] + + test "renders form for editing chosen role", %{conn: conn, role: role} do + conn = admin_login(conn) + conn = get(conn, Routes.role_path(conn, :edit, role)) + + assert html_response(conn, 200) =~ "Edit Role" + end + + test "attempt to edit a role I don't own", %{conn: conn, role: role} do + conn = non_admin_login(conn) + conn = get(conn, Routes.role_path(conn, :edit, role)) + + assert html_response(conn, 404) =~ "role not found" + end + end + + describe "update role" do + setup [:create_role] + + test "redirects when data is valid", %{conn: conn} do + conn = admin_login(conn) + attrs = Map.merge(@create_attrs, %{person_id: conn.assigns.person.id}) + {:ok, role} = Auth.Role.create_role(attrs) + update_attrs = Map.merge(@update_attrs, %{app_id: 1, role_id: role.id}) + conn = put(conn, Routes.role_path(conn, :update, role), role: update_attrs) + + assert redirected_to(conn) == Routes.role_path(conn, :show, role) + # IO.inspect(role, label: "role:106") + # IO.inspect(conn.assigns.person) + + conn = get(conn, Routes.role_path(conn, :show, role)) + assert html_response(conn, 200) =~ "some updated desc" + end + + test "renders errors when data is invalid", %{conn: conn, role: role} do + conn = admin_login(conn) + invalid_app_id = Map.merge(@invalid_attrs, %{"app_id" => "1"}) + conn = put(conn, Routes.role_path(conn, :update, role), role: invalid_app_id) + + assert html_response(conn, 200) =~ "Edit Role" + end + + test "cannot update role I don't own", %{conn: conn, role: role} do + conn = non_admin_login(conn) + conn = put(conn, Routes.role_path(conn, :update, role), role: @update_attrs) + + assert html_response(conn, 404) =~ "role not found" + end + + test "cannot update role I own to App I don't own!", %{conn: conn} do + conn = non_admin_login(conn) + attrs = Map.merge(@create_attrs, %{person_id: conn.assigns.person.id}) + {:ok, role} = Auth.Role.create_role(attrs) + # attempt to update app_id to app owned by admin: + update_attrs = Map.merge(role, %{app_id: 1}) + conn = put(conn, Routes.role_path(conn, :update, role), role: update_attrs) + + assert html_response(conn, 404) =~ "App not found" + end + end + + describe "delete role" do + setup [:create_role] + + test "deletes chosen role", %{conn: conn, role: role} do + conn = admin_login(conn) + conn = delete(conn, Routes.role_path(conn, :delete, role)) + + assert redirected_to(conn) == Routes.role_path(conn, :index) + + conn = get(conn, Routes.role_path(conn, :show, role)) + assert conn.status == 404 + end + + test "attempt to deletes role I don't own", %{conn: conn, role: role} do + conn = non_admin_login(conn) + conn = delete(conn, Routes.role_path(conn, :delete, role)) + assert conn.status == 404 + end + end + + defp create_role(_) do + role = fixture(:role) + %{role: role} + end + + test "POST /roles/grant without admin should 401", %{conn: conn} do + alex = %{email: "alex_grant_role_fail@gmail.com", auth_provider: "email"} + grantee = Auth.Person.create_person(alex) + conn = assign(conn, :person, grantee) + conn = AuthWeb.RoleController.grant(conn, %{"role_id" => 5, "person_id" => grantee.id}) + assert conn.status == 401 + end + + test "POST /roles/grant should create people_roles entry", %{conn: conn} do + alex = %{email: "alex_grant_success@gmail.com", auth_provider: "email"} + grantee = Auth.Person.create_person(alex) + + conn = admin_login(conn) + conn = get(conn, Routes.role_path(conn, :grant, %{"role_id" => 5, "person_id" => grantee.id})) + + # the grant/2 controller handler redirects back to /person/:id + assert html_response(conn, 302) =~ "redirected" + + # check that the record was created: + pr = Auth.PeopleRoles.get_record(grantee.id, 5) + assert pr.person_id == grantee.id + assert pr.role_id == 5 + assert pr.granter_id == 1 + end + + test "GET /roles/revoke/:people_roles_id displays confirm prompt", %{conn: conn} do + conn = admin_login(conn) + conn = get(conn, Routes.role_path(conn, :revoke, 1)) + assert html_response(conn, 200) =~ "superadmin" + end + + test "POST /roles/revoke/:people_roles_id revokes the role", %{conn: conn} do + conn = admin_login(conn) + conn = post(conn, Routes.role_path(conn, :revoke, 1)) + assert html_response(conn, 302) =~ "redirected" + + pr = Auth.PeopleRoles.get_by_id(1) + assert pr.revoker_id == 1 + end + + test "AuthWeb.RoleController.revoke/2 unauthorized if not admin", %{conn: conn} do + wrong_person_data = %{ + email: "unauthorized@gmail.com", + auth_provider: "email", + id: 42 + } + + Auth.Person.create_person(wrong_person_data) + conn = AuthPlug.create_jwt_session(conn, wrong_person_data) + + conn = AuthWeb.RoleController.revoke(conn, %{"people_roles_id" => 1}) + assert conn.status == 401 + end +end diff --git a/test/auth_web/plugs/is_owner_test.exs b/test/auth_web/plugs/is_owner_test.exs new file mode 100644 index 00000000..b457fe46 --- /dev/null +++ b/test/auth_web/plugs/is_owner_test.exs @@ -0,0 +1,21 @@ +defmodule Auth.Plugs.IsOwnerTest do + use ExUnit.Case, async: true + # use Plug.Test + use AuthWeb.ConnCase + + # @opts MyRouter.init([]) + + test "returns hello world", %{conn: conn} do + # Create a test connection + # conn = conn(:get, "/hello") + Auth.Plugs.IsOwner.is_owner(conn, %{this: "that"}) + + # Invoke the plug + # conn = MyRouter.call(conn, @opts) + + # Assert the response and status + # assert conn.state == :sent + # assert conn.status == 200 + # assert conn.resp_body == "world" + end +end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index f453f883..63bf95df 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -22,6 +22,9 @@ defmodule AuthWeb.ConnCase do # Import conveniences for testing with connections import Plug.Conn import Phoenix.ConnTest + # AuthTest is defined in test_helpers.exs + # as per https://stackoverflow.com/a/58902158/1148249 + import AuthTest alias AuthWeb.Router.Helpers, as: Routes # The default endpoint for testing @@ -36,8 +39,8 @@ defmodule AuthWeb.ConnCase do Ecto.Adapters.SQL.Sandbox.mode(Auth.Repo, {:shared, self()}) end - conn = Phoenix.ConnTest.build_conn() - |> Phoenix.ConnTest.init_test_session(%{}) + conn = Phoenix.ConnTest.init_test_session(Phoenix.ConnTest.build_conn(), %{}) + # invoke Plug.Test.init_test_session/2 to setup the test session # before attempting to set a JWT. see: # https://github.com/dwyl/auth/issues/83#issuecomment-660052222 diff --git a/test/test_helper.exs b/test/test_helper.exs index 26689cc8..b4942153 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,2 +1,47 @@ ExUnit.start() Ecto.Adapters.SQL.Sandbox.mode(Auth.Repo, :manual) + +defmodule AuthTest do + @moduledoc """ + Test helper functions :-) + """ + @admin_email System.get_env("ADMIN_EMAIL") + @doc """ + add a valid JWT/session to the conn for routes that require auth as "SuperAdmin" + """ + def admin_login(conn) do + person = Auth.Person.get_person_by_email(@admin_email) + + data = %{ + id: person.id, + email: person.email, + auth_provider: person.auth_provider + } + + # IO.inspect(person, label: "person") + AuthPlug.create_jwt_session(conn, data) + end + + def non_admin_person() do + rand = :rand.uniform(1_000_000) + + Auth.Person.upsert_person(%{ + email: "alex+#{rand}@gmail.com", + givenName: "Alexander McAwesome", + auth_provider: "email", + password: "thiswillbehashed" + }) + end + + def non_admin_login(conn) do + person = non_admin_person() + + data = %{ + id: person.id, + email: person.email, + auth_provider: person.auth_provider + } + + AuthPlug.create_jwt_session(conn, data) + end +end