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 @@ -
AUTH_API_KEY | -Name | -Description | -Url | -|
---|---|---|---|---|
- <%= 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!)"] %> - - | -
- 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. -
- --
Export it as an environment variable: -
- 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" - %> - - -id | +Owner | +Name | +Description | +Url | +|
---|---|---|---|---|---|
<%= 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!)"] %> + + | +
<%= @app.desc %>
+ ++ URL: + + <%= @app.url %> + +
+ + <%= for apikey <- @app.apikeys do %> + <%= if apikey.status != 6 do %> +API KEY: +
Export it as an environment variable: +
You are signed in with your <%= String.capitalize(@conn.assigns.person.auth_provider) %> account. -
+ID | +Pic | +Name | +Status | +Time | + +Auth Provider | +Roles | +|
---|---|---|---|---|---|---|---|
<%= p.id %> | ++ | <%= p.givenName %> | +<%= status_string(p.status, @statuses) %> | +<%= p.updated_at %> | +<%= p.email %> | +<%= p.auth_provider %> | +<%= role_string(p.roles) %> | +
ID | +Role | +Granted | +Granted by | +Revocation | +
---|---|---|---|---|
<%= 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 %> + | + +
Oops, something went wrong! Please check the errors below.
+Name | +Desc | + ++ |
---|---|---|
<%= 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?"] %> + | +
Oops, something went wrong! Please check the errors below.
+id | +Name | +Description | +app 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" %> + | +
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 %> + ++ Name: + + <%= @role.name %> + +
+ ++ Description: + + <%= @role.desc %> + +
+ +