-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Design doc: secure access to APIs from builders #10289
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
Changes from 4 commits
d222243
f3f381e
cbe1adf
5a0207e
7ceb4bd
b2de560
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,164 @@ | ||||||||||||||||||||||||||||
Secure API access from builders | ||||||||||||||||||||||||||||
=============================== | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Goals | ||||||||||||||||||||||||||||
----- | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
- Provide a secure way for builders to access the API. | ||||||||||||||||||||||||||||
- Limit the access of the tokens to the minimum required. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Non-goals | ||||||||||||||||||||||||||||
--------- | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
- Migrate builds to use API V3 | ||||||||||||||||||||||||||||
- Implement this mechanism in API V3 | ||||||||||||||||||||||||||||
- Expose it to users | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
All these changes can be made in the future, if needed. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Current state | ||||||||||||||||||||||||||||
------------- | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Currently, we access the API V2 from the builders using the credentials of the "builder" user. | ||||||||||||||||||||||||||||
This user is a superuser, it has access to all projects, | ||||||||||||||||||||||||||||
write access to the API, access to restricted endpoints, and restricted fields. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
The credentials are hardcoded in our settings file, | ||||||||||||||||||||||||||||
so if there is a vulnerability that allows users to have access to the settings file, | ||||||||||||||||||||||||||||
the attacker will have access to the credentials of the "builder" user, | ||||||||||||||||||||||||||||
giving them full access to the API and all projects. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Proposed solution | ||||||||||||||||||||||||||||
----------------- | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Instead of using the credential of a super user to access the API, | ||||||||||||||||||||||||||||
we will create a temporal token attached to a project, and one of the owners of the project. | ||||||||||||||||||||||||||||
This way this token will have access to the given project only for a limited period of time. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
This token will be generated from the webs, | ||||||||||||||||||||||||||||
benjaoming marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||
and passed to the builders via the celery task, | ||||||||||||||||||||||||||||
where it can be used to access the API. | ||||||||||||||||||||||||||||
Once the build has finished, this token will be revoked. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Technical implementation | ||||||||||||||||||||||||||||
------------------------ | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
We will use the rest-knox_ package, | ||||||||||||||||||||||||||||
this package is recommended by the DRF documentation, | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Easy enough! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you considered the package that David mentioned in the end? |
||||||||||||||||||||||||||||
since the default token implementation of DRF is very basic, | ||||||||||||||||||||||||||||
some relevant features of knox are: | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
- Support for several tokens per user. | ||||||||||||||||||||||||||||
- Tokens are stored in a hashed format in the database. | ||||||||||||||||||||||||||||
We don't have access the tokens after they are created. | ||||||||||||||||||||||||||||
- Tokens can have an expiration date. | ||||||||||||||||||||||||||||
- Tokens can be created with a prefix (rtd_xxx) (unreleased) | ||||||||||||||||||||||||||||
- Support for custom token model (unreleased) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
We won't expose the token creation view directly, | ||||||||||||||||||||||||||||
since we can create the tokens from the webs, | ||||||||||||||||||||||||||||
and this isn't exposed to users. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
The view to revoke the token will be exposed, | ||||||||||||||||||||||||||||
since we need it to revoke the token once the build has finished. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
From the API, we just need to add the proper permission and authentication classes | ||||||||||||||||||||||||||||
to the views we want to support. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
To differentiate from a normal user and a token authed user, | ||||||||||||||||||||||||||||
we will have access to the token via the ``request.auth`` attribute in the API views, | ||||||||||||||||||||||||||||
this will also be used to get the attached projects to filter the querysets. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
The knox package allows us to provide our own token model, | ||||||||||||||||||||||||||||
this will be useful to add our own fields to the token model. | ||||||||||||||||||||||||||||
Fields like the projects attached to the token, | ||||||||||||||||||||||||||||
or access to all projects the user has access to, etc. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
.. _rest-knox: https://james1345.github.io/django-rest-knox/ | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Flow | ||||||||||||||||||||||||||||
---- | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
The flow of creation and usage of the token will be: | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
- Create a token from the webs when a build is triggered. | ||||||||||||||||||||||||||||
The triggered project will be attached to the token, | ||||||||||||||||||||||||||||
if the build was triggered by a user, that user will be attached to the token, | ||||||||||||||||||||||||||||
otherwise the token will be attached to one of the owners of the project. | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't love this requirement, but seems fine... Does attaching it to a project user make more sense than just using a dummy user of some kind? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Django rest knox requires you to attach the token to a user, so we need to set a user here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aside from needing some way to attach a token scope to a project rather than a user, do we have other cases where we would need to customize the functionality of Token expiry is covered, what about scopes? Do we need them only per-user? Or also per-team or per-organization? Do we need to limit rest-knox tokens beyond what they are originally able to do? Can they do more than what a build needs? Just asking these questions to understand how well the package fits, and if it fits, that's great 👍 If it needs some level of customization or we have to accept significant gaps, then there can be a motivation to implement our own token authentication. I'm only knowledgeable of "rolling your own token authentication" from django-ninja, but in this case, it was easy because django-ninja was already designed for it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can attach an organization to the token if needed, but knox requires you to always have a user attached to it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand the package that David mentioned solves this problem of the requirement of attaching the token to a User, right? We could explore that option a little more. I have the same concerns about having to attach the token to a User and I don't like that too much. It would be great if we can avoid it. |
||||||||||||||||||||||||||||
- The token will be created with an expiration date | ||||||||||||||||||||||||||||
of 3 hours, this should be enough for the build to finish. | ||||||||||||||||||||||||||||
We could also make this dynamic depending of the project. | ||||||||||||||||||||||||||||
Comment on lines
+88
to
+90
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is great 👍 I don't think we need to overengineer this, 3 hours seems like a great default to start. |
||||||||||||||||||||||||||||
- Pass the token to the builder via the celery task. | ||||||||||||||||||||||||||||
- Pass the token to all places where the API is used. | ||||||||||||||||||||||||||||
- Revoke the token when the build has finished. | ||||||||||||||||||||||||||||
This is done by hitting the revoke endpoint. | ||||||||||||||||||||||||||||
- In case the revoke endpoint fails, the token will expire in 3 hours. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Why attach tokens to users? | ||||||||||||||||||||||||||||
--------------------------- | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Attaching tokens to users will ease the implementation, | ||||||||||||||||||||||||||||
since we can re-use the code from knox package. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Attaching tokens to projects only is possible, | ||||||||||||||||||||||||||||
but it will require to manage the authentication manually. | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If a build is triggered as an "external version", i.e. a PR build, isn't it nontrivial to attach it to a specific user? I don't think it seems entirely obvious to attach these tokens to users. Attaching them to projects seems more "natural" to sequences where no users are really involved. Incoming webhooks as well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For actions that don't come from a user, we will attach it to one of the owners of that project. But I'm taking a look at the other alternative to see if it can be done without users.
Comment on lines
+103
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't really explain why it's hard. I had this same question around why we're attaching it to a user. Is it so we can do something like:
or something? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. knox requires you to attach the token to a user, so it can perform authentication (this validates the token and expose it to us), if we only use it for authorization we will need to manually do this (extract the token). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The other lib DRF API key doesn't have this constraint, but if we want to implement per-user tokens for our normal API, then we will need to manually implement that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This probably doesn't make too much sense, but I'm asking anyways 😄: what about using both packages?
Would that be too complex? Are they compatible to work together? Taking into account, in particular, that there are features from knox that we need but they are not released yet. So, maybe it makes sense to use both packages together since they solve different problems. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Haven't tested it, but we should be able to use both packages, since they work per-view. But I'd prefer if we just choose one, otherwise we will be maintaining both integrations, it could get confusing. Also, I think we should take into consideration that we want to expose this similar feature to users (for API v3 at least from what I understand). To summarize:
Both packages allow you to use a custom model to attach any models to it (projects, organizations, etc). API key can be extended to attach users to it, and use it for authentication, but that will need to be implemented manually. rest-knox requires a user, but it doesn't require you to make use of it, so we can just ignore the user when we don't need it. There are more differences/features of each summarized in: readthedocs.org/docs/dev/design/secure-api-access-from-builders.rst Lines 51 to 56 in 5a0207e
readthedocs.org/docs/dev/design/secure-api-access-from-builders.rst Lines 135 to 141 in 5a0207e
|
||||||||||||||||||||||||||||
This is since Knox requires a user to be attached to the token, | ||||||||||||||||||||||||||||
and this user is used in their ``TokenAuthentication`` class. | ||||||||||||||||||||||||||||
An alternative is to use the DRF API key package, which doesn't require a user, | ||||||||||||||||||||||||||||
but then if we wanted to extend this functionality to our normal APIs, we will have | ||||||||||||||||||||||||||||
to implement the authentication manually. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Kepping backwards compatibility | ||||||||||||||||||||||||||||
------------------------------- | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Access to write API V2 is restricted to superusers, | ||||||||||||||||||||||||||||
and was used only from the builders. | ||||||||||||||||||||||||||||
So we don't need to keep backwards compatibility for authed requests, | ||||||||||||||||||||||||||||
but we need to keep the old implementation working while we deploy the new one. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Possible issues | ||||||||||||||||||||||||||||
--------------- | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Some of the features that we may need are not released yet, | ||||||||||||||||||||||||||||
we need the custom token model feature, specially. | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we need the set if projects for the token, if we're already hacking it onto a user. Seems like a middle step would just be generating it for the user who has access to the project, as a hack. |
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
There is a race condition when using the token, | ||||||||||||||||||||||||||||
and the user that is attached to that token is removed from the project. | ||||||||||||||||||||||||||||
This is, if the user is removed while the build is running, | ||||||||||||||||||||||||||||
the builders won't be able to access the API. | ||||||||||||||||||||||||||||
Comment on lines
+125
to
+128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea, I was wondering about this limitation around the user linkage. I think it could be useful to remove the user entirely if possible.. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could also just use the user for knox to make authentication work, but internatlly just check for the projects attached to the token only, this is ignoring the user, for the build APIs at least. |
||||||||||||||||||||||||||||
We could avoid this by not relying on the user attached to the token, | ||||||||||||||||||||||||||||
only on the projects attached to it (this would be for our build APIs only). | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Alternative implementation with Django REST Framework API Key | ||||||||||||||||||||||||||||
------------------------------------------------------------- | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Instead of using knox, we can use `DRF API key`_, | ||||||||||||||||||||||||||||
it has the same features as knox, with the exception of: | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
- It is only used for authorization, | ||||||||||||||||||||||||||||
it can't be used for authentication (or it can't be out of the box). | ||||||||||||||||||||||||||||
- It doesn't expose views to revoke the tokens (but this should be easy to manually implement) | ||||||||||||||||||||||||||||
- Changing the behaviour of some things require sub-classing instead of defining settings. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
The implementation will be very similar to the one described for knox, | ||||||||||||||||||||||||||||
with the exception that tokens won't be attached to users, | ||||||||||||||||||||||||||||
but just a project. And we won't be needing to handle authentication, | ||||||||||||||||||||||||||||
since the token itself will grant access to the projects. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
To avoid breaking builders, | ||||||||||||||||||||||||||||
we need to be able to make the old and the new implementation work together, | ||||||||||||||||||||||||||||
this is, allow authentication and handle tokens at the same time. | ||||||||||||||||||||||||||||
This means passing valid user credentials together with the token, | ||||||||||||||||||||||||||||
this "feature" can be removed in the next deploy | ||||||||||||||||||||||||||||
(with knox we also need to handle both implementations, | ||||||||||||||||||||||||||||
but it doesn't require passing credentials with the token, | ||||||||||||||||||||||||||||
since it also handles authentication). | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
.. _DRF API key: https://florimondmanca.github.io/djangorestframework-api-key/ | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Future work | ||||||||||||||||||||||||||||
----------- | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
This work can be extended to API V3, and be exposed to users in the future. | ||||||||||||||||||||||||||||
We only need to take into consideration that the token model will be shared by both, | ||||||||||||||||||||||||||||
API V2 and API V3. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why it has to be attached to one of the owners?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(from @ericholscher comment in #10289 (review))
I think it's important to attach the token to a Project itself so we don't have to do it to an owner. Attaching the token only to a Project (and not a User) will be useful for audit purposes as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For audit, we can just add another field like
source
that indicates if the token was generated from a manual action or a wehbook, so we know when to take the user into consideration.