Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add (REST) API for managing (GraphQL) API credentials(/tokens) #436

Merged
merged 7 commits into from
Dec 14, 2020

Conversation

johnshaughnessy
Copy link
Contributor

No description provided.

@johnshaughnessy johnshaughnessy changed the title Add REST API for managing graphql API tokens/credentials Add (REST) API for managing (GraphQL) API credentials(/tokens) Dec 10, 2020
@johnshaughnessy johnshaughnessy marked this pull request as ready for review December 10, 2020 03:53
defp ensure_atom(x) when is_binary(x), do: String.to_atom(x)

# TODO Should we permit the creation of tokens
# on behalf of other accounts like this?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels odd to allow this, but it seemed like an easy way for us to do a closed alpha/beta test of the API on HMC. Alternatively, we could add a column (e.g. an integer bitmask) to the database and use it to assign roles, then check these roles to see if the given account has permission to create credentials.

lib/ret/hub.ex Outdated
scopes: scopes,
account_id: account_id
}) do
# TODO: Is it correct to disallow the create_accounts scope for account type tokens?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got it into my head that only :app credentials would be allowed to have the create_accounts scope, but then I don't have a good reason for this. Maybe only admin accounts should be allowed to create :account credentials that have the create_accounts scope. Then the only important distinction between :app credentials and :account credentials is the fact that :app credentials are accessible / managed / shared by all admin accounts.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a bit confused by this since I thought we removed create_accounts from #399 since it was not implemented. But I see create_accounts still exists in Ret.Api.Scaopes. I would suggest we remove create_accounts from these PRs altogether and also, importantly, ensure that input validation rejects claims that don't exist before it even gets to this point.

end

def can?(%Ret.Account{}, :list_credentials, :account) do
# TODO: Allow admins to disable this in config
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how/where to add config values. I'll have to learn

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to help if needed. See examples of where we use "features|public_rooms" and get_config_bool for reference. We might want to introduce a cached version, get_cached_config_bool if we're concerned about database hits for config values, and if a stale cache delay is acceptable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only: [to_claims: 2, authed_create_credentials: 2, authed_list_credentials: 2, authed_revoke_credentials: 2]

# Limit to 1 TPS
# TODO : I copied this from other controllers. Is this an appropriate limit? -John
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps the same limit should be added to the graphql endpoint

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely seems appropriate for this API. Not sure about the entire graphql endpoint.

Copy link
Contributor

@brianpeiris brianpeiris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, though we should definitely resolve our approach to this in general, as we discussed.

@@ -87,11 +87,11 @@ defmodule Ret.Api.Credentials do
end
end

defp validate_subject_type(:subject_type, subject_type) do
def validate_subject_type(:subject_type, subject_type) do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Might be better to abstract these a bit, instead of exposing the callback used by validate_change. It's a bit awkward to have the callers repeat :scopes and :subject_type. In fact, it may be better to create a private validate_field function that overloads for each field type instead, and use that for validate_change. Something like:

defp validate_field(:scopes, scopes) do
  ...
end

defp validate_field(:subject_type, scopes) do
  ...
end

def validate_scopes(scopes) do
  validate_field(:scopes, scopes)
end

def validate_subject_type(subject_type) do
  validate_field(:subject_type, subject_type)
end

end

defp validate_account_id(%Account{}, account_id) do
case Account.query()
|> Account.where_account_id_is(account_id)
|> Repo.one() do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest using Repo.exists? instead of actually getting the account object.

Comment on lines 68 to 71
with [] <-
validate_account_id(account, account_id) ++
Credentials.validate_scopes_type(:scopes, scopes) ++
Credentials.validate_subject_type(:subject_type, subject_type) do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Not sure if we've been using this pattern already, but it does feel a bit too clever to use with in this way, since we're not actually doing anything with the matched value and implicitly allowing with to return the error list. Seems like this is one of those times where it's better to be clear than concise.

lib/ret/hub.ex Outdated
scopes: scopes,
account_id: account_id
}) do
# TODO: Is it correct to disallow the create_accounts scope for account type tokens?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a bit confused by this since I thought we removed create_accounts from #399 since it was not implemented. But I see create_accounts still exists in Ret.Api.Scaopes. I would suggest we remove create_accounts from these PRs altogether and also, importantly, ensure that input validation rejects claims that don't exist before it even gets to this point.

end

def can?(%Ret.Account{}, :list_credentials, :account) do
# TODO: Allow admins to disable this in config
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to help if needed. See examples of where we use "features|public_rooms" and get_config_bool for reference. We might want to introduce a cached version, get_cached_config_bool if we're concerned about database hits for config values, and if a stale cache delay is acceptable.

Comment on lines 24 to 26
case Credentials.query()
|> Credentials.where_sid_is(credentials_sid)
|> Repo.one() do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be inclined to use Repo.get_by() instead of constructing a query.

end

def render("show.json", %{credentials: credentials}) do
render("show.json", %{credentials: credentials, token: nil})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bit strange that token is on the outer result here, but inside credentials: in the other views.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be misunderstanding your comment - but token always ends up as a key inside the credentials json, not outside it. This line is just redirecting to the other render("show.json", ...) (which does the call to render_credentials and merges in the token).

Open to suggestions to make this clearer -- My intention is to return a token in the credentials object when you first create one, but then have token: null every time after that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, my mistake, I misread the code paths in this case.

lib/ret/hub.ex Outdated
# TODO: Is it correct to disallow the create_accounts scope for account type tokens?
# If so, why? Do we need to enforce this everywhere?
Scopes.create_accounts() not in scopes
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should only allow admins to create credentials for the first release, at least until we decide on doing roles?

|> get(list)
|> json_response(200)
|> Map.get("credentials")
|> Enum.count() === 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do this enough in the tests that it should be a get_credentials_count helper.

|> hd()
|> Map.get("is_revoked")
end
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 nice tests!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks :)

I have not wrapped my head around or studied up on best practices for testing elixir + phoenix apps. Here I test an interface (CredentialsController) and the model (Credentials) together, which I could have done this separately. I am happy enough with it as it is, but want to be better about this in the future. This seems like it'll be increasingly important as we have alternative interfaces, transports, and views (REST over HTTP, graphql, websockets passing json).

@johnshaughnessy johnshaughnessy merged commit 2de8003 into feature/graphql-2 Dec 14, 2020
@johnshaughnessy johnshaughnessy deleted the feature/credentials-api branch December 14, 2020 19:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants