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

Use DocApiKey as valid ApiKeys #1115

Closed
wants to merge 31 commits into from

Conversation

hexaltation
Copy link
Collaborator

Resolves #579

DocApiKey are service Api Keys binded to a document rather than to a user.
Under the hood they are using Shares implementation.

To differentiate them for "classical" Shares they have a flag ApiKey set to true in sharesOptions.

Their key can be used to make Api Calls a given document, as well as be used in url like "classical" Shares.

They have to be used like this:

Authorization: Bearer DOC-{share.key}

@hexaltation hexaltation marked this pull request as draft July 22, 2024 08:51
@hexaltation hexaltation self-assigned this Jul 22, 2024
@hexaltation
Copy link
Collaborator Author

I still have some tests to write before it can be a full PR.

@paulfitz I'm a little worried about my implementation in the Authorizer.
I'm not sure that I didn't reinvented some already existing function that didn't manage to find.
And I hope that my way to do that do not introduce security issues.

Copy link
Collaborator

@fflorent fflorent left a comment

Choose a reason for hiding this comment

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

I made a quick review, here are my first comments.

Also I think you'd better rebase with the main repo? I moved the HomeDBManager to a subdir (homedb/)

app/gen-server/lib/HomeDBManager.ts Outdated Show resolved Hide resolved
app/gen-server/lib/HomeDBManager.ts Outdated Show resolved Hide resolved
return query ? key : query;
}

// in parameters linkId is the linkId in db in case of update of this id in the share
Copy link
Collaborator

Choose a reason for hiding this comment

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

I took some time to understand this comment, what do you think of this instead:

Suggested change
// in parameters linkId is the linkId in db in case of update of this id in the share
// the linkId parameter corresponds to the one currently stored in the db in case we want to change it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In fact any parameter can be updated, not just linkId.
If we want to update linkId it have to be set in share object passed as a parameter
share.linkId = newLinkId

So I go with

  // nb. The linkId parameter corresponds to the one currently stored in the db.
  // If we want to update it the new value must be passed through the share parameter

Let me know

app/gen-server/lib/HomeDBManager.ts Outdated Show resolved Hide resolved
Copy link
Collaborator

@fflorent fflorent left a comment

Choose a reason for hiding this comment

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

Some other comments for now

dom.boolAttr('disabled', (use) => use(this._loading)) // or is not owner
),
),
description('This doc API key can be used to access this document via the API. \
Copy link
Collaborator

Choose a reason for hiding this comment

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

Translation missing if I am right (t() should be called)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hello,
This are pieces of code written by @CamilleLegeron that I copied from her branch.
I'm not familiar with it.
May be It's a better idea to remove it from this PR as it's not integrated in admin page yet.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If this piece of code is not used yet, we should keep it in a separate branch and reuse it when it will be.

),
),
description('This doc API key can be used to access this document via the API. \
Don’t share this API key.', testId('description')),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe rather explain the consequences rather than discouraging its use (I tend to think there could be legitimate reasons to share it). Wdyt?

dom.maybe((use) => !use(this._docApiKey), () => [
basicButton(t("Create"), dom.on('click', () => this._onCreate()), testId('create'),
dom.boolAttr('disabled', this._loading)),
description(t("By generating a doc API key, you will be able to \
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nitpicking: The \ is not standard for multiline strings (unless it has changed). Probably better to either use template strings or string concatenation.

() => this._onDelete(),
{
explanation: t(
"You're about to delete a doc API key. This will cause all future requests \
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: Idem for \

Copy link
Collaborator

@fflorent fflorent left a comment

Choose a reason for hiding this comment

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

Made some progress in my review.
I still have to review the tests.

this._app.post('/api/docs/:did/apiKey', expressWrap(async (req, res) => {
const did = req.params.did;
const doc = await this._dbManager.getDoc(req);
if (!doc){
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nitpicking:

Suggested change
if (!doc){
if (!doc) {


const linkId = req.body.linkId;
const query = await this._dbManager.getDocApiKeyByLinkId(did, linkId);
if (query){
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nitpicking:

Suggested change
if (query){
if (query) {

Same for below.


In vim: :%s/if (.*)\zs{/ {/g should fix the problem (not tested).

For \zs, it starts the match after this piece of pattern, it see :help \zs:

\zs	Matches at any position, but not inside [], and sets the start of the
	match there: The next char is the first char of the whole match.
	|/zero-width|
	Example: >
		/^\s*\zsif
<	matches an "if" at the start of a line, ignoring white space.
	Can be used multiple times, the last one encountered in a matching
	branch is used.  Example: >
		/\(.\{-}\zsFab\)\{3}
<	Finds the third occurrence of "Fab".
	This cannot be followed by a multi. *E888*

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

prettier or riot. :p

Copy link
Collaborator

Choose a reason for hiding this comment

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

Eslint has some rules I think for that stuff, but still, I agree with a tool that would prevent us from making style-related comments.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe configuring eslint this way:

...
"space-after-keywords": "error",
"space-before-blocks": "error",

Comment on lines +341 to +344
const options = sanitizeDocApiKeyOptions(req.body.options);
req.body.options = JSON.stringify(options);

const key = await this._dbManager.createDocApiKey(did, req.body);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I prefer avoiding altering the parameters, req.body is meant to store the information passed by the user.

Suggested change
const options = sanitizeDocApiKeyOptions(req.body.options);
req.body.options = JSON.stringify(options);
const key = await this._dbManager.createDocApiKey(did, req.body);
const options = sanitizeDocApiKeyOptions(req.body.options);
const key = await this._dbManager.createDocApiKey(did, {
...req.body,
options: JSON.stringify(options)
});

Comment on lines +385 to +391
// In order to catch {options: ""} case
if (Object.keys(req.body).includes("options")){
const options = sanitizeDocApiKeyOptions(req.body.options);
req.body.options = JSON.stringify(options);
}

const query = await this._dbManager.updateDocApiKeyByLinkId(did, linkId, req.body);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// In order to catch {options: ""} case
if (Object.keys(req.body).includes("options")){
const options = sanitizeDocApiKeyOptions(req.body.options);
req.body.options = JSON.stringify(options);
}
const query = await this._dbManager.updateDocApiKeyByLinkId(did, linkId, req.body);
// In order to catch {options: ""} case
const options = Object.hasOwnProperty(req.body, 'options') ? // Or you may use req.body.options simply
sanitizeDocApiKeyOptions(req.body.options) : undefined; // Or null depending on what the function expects
const query = await this._dbManager.updateDocApiKeyByLinkId(did, linkId, {
...req.body,
options: JSON.stringify(options)
});

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also I wonder if we could not adapt or use inheritance for ShareInfo to allow options to be a typed object. This way:

  • we would keep this the parameters strongly typed;
  • less JSON operations, which would improve the readability and a bit the performance.

}

if (req.body.docId){
throw new ApiError("Can't update DocId", 400);
Copy link
Collaborator

Choose a reason for hiding this comment

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

You may control and throw what the user passes using the convenient type-interface-checker util:
https://github.com/gristlabs/ts-interface-checker

I can explain you how to do that after lunch.

if (!legalAccessValues.includes(options.access)){
throw new ApiError(`Invalid access value: ${options.access}`, 400);
}
return {...options, "apikey": true};
Copy link
Collaborator

Choose a reason for hiding this comment

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

You may use type-interface-checker, maybe with StringUnion and StringUnion.checkAll(array).

const key = makeId();
const query = await this._connection.createQueryBuilder()
.insert()
.setParameter('options', share.options)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it still used?

// anonymous user's profile via the api (e.g. how should the api key be managed).
return res.status(401).send('Credentials cannot be presented for the anonymous user account via API key');
// Bearer needs to be form "DOC-YYYYYYYYY" to apply as Doc Api key
if (parts[1].match(/^DOC-.*/)) {
Copy link
Collaborator

@fflorent fflorent Jul 23, 2024

Choose a reason for hiding this comment

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

Nit (improves a bit the performance, especially interesting for requests middlewares):

Suggested change
if (parts[1].match(/^DOC-.*/)) {
const docApiPrefix = 'DOC-';
if (parts[1]?.startsWith('DOC-')) {

And later:

        // Doc Api Key
        const docApiKey = parts[1].substring(docApiPrefix.length)

This way, if by chance the docApi accepts hyphens, there would be no regressions here.

Comment on lines +184 to +189
const didRegex = /(?<=^\/docs\/).+?(?=\/|$)/g;
const did = url.match(didRegex);
// Only API scope matching regex MUST be accessed with Doc Api keys
if (!did || !did[0]){
return res.status(401).send(`Access Denied: Scope limited to Documents`);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: A bit simpler and probably more optimized, wdyt?

NB: if you agree on that proposal, don't forget to replace all occurrences of did[0] to did below.

Suggested change
const didRegex = /(?<=^\/docs\/).+?(?=\/|$)/g;
const did = url.match(didRegex);
// Only API scope matching regex MUST be accessed with Doc Api keys
if (!did || !did[0]){
return res.status(401).send(`Access Denied: Scope limited to Documents`);
}
const didRegex = /\/docs\/(?<did>[^\/]*)/;
const matchRes = url.match(didRegex);
// Only API scope matching regex MUST be accessed with Doc Api keys
const did = matches?.groups.did;
if (!did){
return res.status(401).send(`Access Denied: Scope limited to Documents`);
}

const access = share.options.access ? share.options.access : null;
mreq.docAuth = {...docAuth, access};
mreq.userIsAuthorized = true;
hasApiKey = true;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I share here a proposal about the express routes which would be maybe too ambitious for this PR (let's do that in a next one).

I feel like we can split the DocApi.ts modules in different ones, each one with its specific route (docs/:did, docs/:did/tables, docs/:did/tables/:tid/columns, docs/:did/tables/:tid/records, ...).

The immediate benefit would be to have probably better separation of concerns and lighter modules.

Also, and for our problem here, I feel like this piece of middleware can be delayed and put as a middleware in the /docs/:did route.

Some adaptations has to be found, especially hasApiKey is used for telemetry (I guess it can be moved in a separate function?), and I don't know if there are some caveats with having the user set to anon first before changing again.

@hexaltation
Copy link
Collaborator Author

@paulfitz after a first review with @fflorent It appears that the main doubt about the actual implementation is the following.

In app/server/lib/Authorizer.ts:211

const org = await dbManager.getOrgOwnerFromDocApiKey(share);
const userId = org?.ownerId;
mreq.user = anon;
mreq.userId = userId;

const docAuth = await getOrSetDocAuth(mreq, dbManager, options.gristServer, did[0]);
const access = share.options.access ? share.options.access : null;
mreq.docAuth = {...docAuth, access};
mreq.userIsAuthorized = true;
hasApiKey = true;

I've done this to have a user whom we can apply share.access rights.

Getting a user by the org owner I'm expecting that the user still exists and have rights to the doc. But that's maybe false

dbManager.getOrgOwnerFromDocApiKey(share);

The two other options that have come in our mind after the review are :

  • const userId = doc.createdBy;
  • Or store the Id of the DocApiKey creator in the share (Adding a userId column ?), then apply the lowest right beetween user access and DocApiKey access over the document.

Thanks for your feedbacks.

@paulfitz paulfitz added the preview Launch preview deployment of this PR label Jul 23, 2024
Copy link
Contributor

Deployed commit 3ab5f95fb57ffe4a246dd80f567ca2912528d2ef as https://grist-hexaltation-grist-core-doc-api-key.fly.dev (until 2024-08-22T15:36:14.132Z)

@paulfitz
Copy link
Member

@paulfitz after a first review with @fflorent It appears that the main doubt about the actual implementation is the following.

Before getting into review of this implementation, or reading the implementation more, I'd like a clear description of what it is supposed to do. I sat down recently to understand it, only to realize some time later that the UI code I was scratching my head over is not in fact used. I have read the linked issue. I can imagine many things this feature might be. In slack, I've been pointed to parts of the code, and given curl commands to try it out. What I haven't had yet is a clear description of what it does (not how).

The description of what the feature does will help in the following ways:

  • It will be a good quality commit message.
  • It may be the basis of documentation, including API documentation.
  • It will help decide whether any product conversations need to happen prior to further implementation.
  • It will help review, to know what should be happening.

Plenty of Grist work has skimpy descriptions, so I'm sorry to single this PR out. The issue it links just doesn't have any clear decisions in it or product conversation, which sometimes compensates for weak descriptions.

Copy link
Contributor

github-actions bot commented Aug 7, 2024

Deployed commit ea05508ba70626dfa096a5752feafeb5ead0ff5c as https://grist-hexaltation-grist-core-doc-api-key.fly.dev (until 2024-09-06T15:37:42.240Z)

@hexaltation
Copy link
Collaborator Author

Before getting into review of this implementation, or reading the implementation more, I'd like a clear description of what it is supposed to do. I sat down recently to understand it, only to realize some time later that the UI code I was scratching my head over is not in fact used. I have read the linked issue. I can imagine many things this feature might be. In slack, I've been pointed to parts of the code, and given curl commands to try it out. What I haven't had yet is a clear description of what it does (not how).

Hello @paulfitz ,
I'm sorry for the waste of time I generated for you.
And for the noise, mixing github and slack discussions.

We work with @fflorent on a specification to help to have an overview on the feature proposal.
You'll find it in the next message in this thread.

@hexaltation
Copy link
Collaborator Author

DocApiKey

Feature Abstract

As a workflow builder, I want to interact through API calls with a single document, with limited rights, in a third party software (like zappier, n8n, ...), without exposing other documents/ressources I have rights on, so I reduce the potential damages in case the API key is leaked or is used for dangerous calls by mistake, and so I can revoke them quickly and easily.

Motivations

We witness that some users are tempted to share their user api Key with others ones in order to implement some automation.
If/when so they give full priviledges on all their ressources/account, not only the one document that need an automation.
To reduce this risk, we suggest to add a service key acting at document level. Let's call it DocApiKey for the rest of this specification.

User stories

As a user, I would like to update my document using n8n. To do so, I open the API console of the document, I generate a new DocApiKey with n8n-key as name and choose beetween editors or viewers for rights. I got a key as a response. Then I can use this key in n8n to perform tasks on the document.

As a document owner, I list Doc Api keys availaible for a document, so I know how these keys are used.

As a document owner I edit Doc Api Keys names/Descriptions.

As a document owner I revoke Doc Api Keys, so it cannot be used any longer.

As an administrator, I look for REST Api calls using Doc Api Key in the logs, so I know how they are used and can revoke them if necessary.

Proposed solution

  • Create a service api Key / Document Api Key with a scope limited to /api/docs
  • The DocApiKey must be limited to single document
  • The DocApiKey can have either editors or viewers rights, no more complicated ACLs are applied.

The functionality delivery is scheduled in two steps:

  1. An API implementation and accessibility though the console API,
  2. Then an UX/UI for managing the DocApiKeys.

Proposed implementation

DB side (current option in PR draft)

  • The docApiKey will be a Share under the hood

Example of share adapted to docApiKey needs

id key docId linkId options
31 uKXGYSc5pZ2muaQP9WKgpY sdDhS4Ez9dpMxSMyzW1Qau DEV-KEY-ID-1720013890 {"access": "editors","apikey": true}

The only difference is an apikey flag set to true.

DB side (alternative option)

Add a new table for this DocApiKeys.
It may look a lot like Shares but with a link to userId in the table.

id key docId UserId desc rightsLevel
31 uKXGYSc5pZ2muaQP9WKgpY sdDhS4Ez9dpMxSMyzW1Qau 12 n8n-key editors

Endpoints

New endpoints will be added to CRUD this docApiKeys

CREATE a DocApiKey

POST /api/docs/{docId}/apikey

  • Authorizations: ApiKey
path Parameters
parameter type
DocId required string A string id (UUID)
Request Body schema: application/json
  • docId: <String>
  • linkId: <String>
  • options: <Object ShareOptions>
Response

200

  • key: <String>

READ a DocApiKey

GET /api/docs/{docId}/apikey/{linkId}

  • Authorizations: ApiKey
path Parameters
parameter type
DocId required string A string id (UUID)
LinkId required string A string describing the key
Response

200

  • <Object Share>

UPDATE a DocApiKey

PATCH /api/docs/{docId}/apikey/{linkId}

  • Authorizations: ApiKey

path Parameters

parameter type
DocId required string A string id (UUID)
LinkId required string A string describing the key
Request Body schema: application/json
Response

200

  • <Object Share>

DELETE a DocApiKey

DELETE /api/docs/{docId}/apikey/{linkId}

  • Authorizations: ApiKey
Response

200

path Parameters
parameter type
DocId required string A string id (UUID)
LinkId required string A string describing the key

List all keys of a document

GET /api/docs/{docId}/apikeys

  • Authorizations: ApiKey
path Parameters
parameter type
DocId required string A string id (UUID)
Response

200

  • <Array of Objects Share>

DELETE all keys of a document

DELETE /api/docs/{docId}/apikeys

  • Authorizations: ApiKey
path Parameters
parameter type
DocId required string A string id (UUID)
Response

200

Nota bene

In order to avoid to guarantee unicity beetween shares.key and user.apikey DocAPIKey and to be sure to discriminate these two keys in API calls, which would be complex to do, Authorization bearer will be formed as :

Authorization: Bearer DOC-xxxxxxxxxxx

The logs need to be customized so we can clearly see which key have been used whose (and whose if necessary), and what docApiKey. Helpful when concerns are raised about possible intrusions or api key thief.

To be debated

  • Do we want to be able to have a link beetween a key and its creator?
  • Do we want to apply fine ACL to docApiKeys?
  • Are DocApiKey still valid shares, or should we inhinbit the ability to display the document in a web browser using the docApiKey?

Implementation questions

The Authorizer for now allow calls only for non anonymous users.
By definition docApiKeys will be anonymous as they are not bound to a user.

Many options come to my mind to address this point :

  • Create a new special user, set UserId to its ID
    • Pros:
      • simple rights inheritance
      • possible ACL fine tuning
      • lifespan is bound to a document and not to a user, so a user may leave and so the automations using the doc api key may continue to work.
    • Cons:
      • Hard to track who did the actions in history.
      • Requires to update the Share table model to retrieve the doc api key creator.
  • Set UserId as the user which created the DocApiKey
    • Pros:
      • Have a distinct user for each docApiKey ready for Authorizer
      • Key creator is already in the table, and can be easily displayed in the UI.
    • Cons:
      • Problem of right inheritance ?
      • lifespan is bound to a user and key is destructed when user is.
  • Set UserId to the org owner Id (seems impossible because only Personal orgs have ownerId set in database)

And then, of course, limit rights to editors or viewers depending on values in the Share.

Can you please tell me which is the best option in your opinion?

@paulfitz
Copy link
Member

paulfitz commented Aug 7, 2024

@hexaltation thanks for the proposal write-up. Some questions:

  • What is the proposed access control situation for the document api key. In other words, for all operations on the key, who has the right to perform that operation. I see references to an individual user making a key, presumably for a document to which they have access, and perhaps limited by their current level of access? The user story may suggest the user is an owner, but that isn't clear in proposal. I see reference to an ability of an administrator to revoke? If users other than owners can make api keys, do document owners have any special rights on operations on api keys that are scoped to their document?
  • Are there any nuances to a users' access to a document changes over time. For example, is it fine for a user to have their access to a document removed, but for a document api key they created in the past to still exist.
  • I see a statement about "no more complicated ACLs are applied". Do granular access rules apply to document api keys? I see a nota bene that this to be debated. Ideally this would be settled before implementation.
  • All document requests are currently made on behalf of a specific user, with their identity available for granular access rules and trigger formulas. What identity will be provided when using document api keys? I see an answer in the implementation, but this is not an implementation detail. I see this identified as an Implementation question. It is an important question for the proposal.
  • For the key shape (the DOC- prefix). Is it known that this prefix is impossible for all current keys? I am optimistic, but don't see an argument made yet. The argument may be simple if one of the characters is impossible with current keys. A length argument may be possible also. One thing to watch out for is that they algorithm for generating the keys has changed once I believe, so there might be two family of keys to argue for.
  • If a user or a document is deleted, what should happen to associated keys. Delete them?

Some possible answers off the top of my head (but I have no special authority on design, once we've got a clear proposal we can circulate it):

  • Perhaps any user can create/read/update/delete a doc-api-key for a doc to which they currently have some access. Administrators or document owners has no special rights. (this could be undesirable! my focus is for consistency and simplicity)
  • Access is limited by the combination of the access level of the user at the time of any request made using the doc-api-key, plus an optional viewers/editors/owners bound. Meaning granular access rules apply, and if the user later has access restricted or dropped that applies to the doc-api-key. (this could be undesirable! my focus is for consistency and simplicity)
  • For any request made with a doc-api-key, the nominal user is the one that created the doc-api-key, with their nominal access level respecting the option bounds.
  • Doc-api-keys are deleted if either the document they refer to or the user they are linked to are deleted.
  • (The key shape, the DOC- prefix, seems straightforward to check if it is fine, I don't know offhand)

@hexaltation
Copy link
Collaborator Author

Hello @paulfitz,

Here are some answers to your previous message.
I will modify my original proposal message accordingly if its ok for you.

* What is the proposed access control situation for the document api key. In other words, for all operations on the key, who has the right to perform that operation. I see references to an individual user making a key, presumably for a document to which they have access, and perhaps limited by their current level of access? The user story may suggest the user is an owner, but that isn't clear in proposal. I see reference to an ability of an administrator to revoke? If users other than owners can make api keys, do document owners have any special rights on operations on api keys that are scoped to their document?
  1. To clarify our point of view, our prefered option is this first one for this first implementation is to create a new special user.
    So the key is only atached to the document.
    Only document owners can create read update delete DocApiKeys.
    The instance administrator can only delete the key in database if needed.
* Are there any nuances to a users' access to a document changes over time. For example, is it fine for a user to have their access to a document removed, but for a document api key they created in the past to still exist.
  1. If we follow our preffered option, the keys are attached to the document. If a previous owner can no longer access a document, it the duty to the current owners to update the secrets. If an owner leave an org he/she must transfer ownership over its document to other persons.
* I see a statement about "no more complicated ACLs are applied". Do granular access rules apply to document api keys? I see a nota bene that this to be debated. Ideally this would be settled before implementation.
  1. After more reflexion, it seems a good idea to
    After more reflexion, we think we may answer to this need by adding a user.share property which would give access to the Share non-secret information like the linkId, and maybe other information we would like to introduce (like a label to describe what this key is used for). This could be used to reduce the rights for docApiKeys, and define custom attributes through a lookup table based on the value of the linkId.
* All document requests are currently made on behalf of a specific user, with their identity available for granular access rules and trigger formulas. What identity will be provided when using document api keys? I see an answer in the implementation, but this is not an implementation detail. I see this identified as an Implementation question. It is an important question for the proposal.
  1. The document will take the identity of a new special user. The rights applied are the one in the share composed with the one set in ACLs.
* For the key shape (the `DOC-` prefix). Is it known that this prefix is impossible for all current keys? I am optimistic, but don't see an argument made yet. The argument may be simple if one of the characters is impossible with current keys. A length argument may be possible also. One thing to watch out for is that they algorithm for generating the keys has changed once I believe, so there might be two family of keys to argue for.
  1. DOC- prefixed keys cannot colide with current user api keys or shares keys.
* If a user or a document is deleted, what should happen to associated keys. Delete them?
  1. If a document is deleted, associated keys are deleted in cascade in the database.

Ideally the implementation must be modular enough to be compatible in the future with WYSIWYS (What You See Is What You Share) plans.

@paulfitz
Copy link
Member

The document will take the identity of a new special user

Can you tell me more about this new special user? For example, how are the properties of this user determined (see https://support.getgrist.com/access-rules/#access-rule-conditions for a list).

So the key is only attached to the document.
Only document owners can create read update delete DocApiKeys.

Ok, this simplifies things technically, pending details of the special user associated with the key.

adding a user.share property which would give access to the Share non-secret information like the linkId, and maybe other information

Exposing linkId to granular access rules could be useful. So, to be clear, granular access rules would apply?

I will modify my #1115 (comment) accordingly if its ok for you.

Yes, I'd like that, so that there is a proposal I can show to Anais and Dmitry once it is sufficiently clear. It is getting closer, thanks for your clarifications!

@hexaltation
Copy link
Collaborator Author

The document will take the identity of a new special user

Can you tell me more about this new special user? For example, how are the properties of this user determined (see https://support.getgrist.com/access-rules/#access-rule-conditions for a list).

I our mind the default access of this user are editors one.
The applied access level to a document will be the lowest one between the user.access and docapiKey.options.access.

Special User Access Level DocApiKey options access level Applied access Level
editor editor editor (CRUDS)
editor viewer viewer (R)
editor not set access denied

Granular access rules can be set to a linkId so it will be possible to limit the scope of action of the docApiKey.

adding a user.share property which would give access to the Share non-secret information like the linkId, and maybe other information

Exposing linkId to granular access rules could be useful. So, to be clear, granular access rules would apply?

Yes, It's our goal.

@paulfitz
Copy link
Member

paulfitz commented Aug 21, 2024

@hexaltation can you tell me more about the following properties of the special user:

  • What is their Email
  • What is their UserID, how is that decided, is it a constant
  • What is their Name

I just want to be sure I know what a special user is exactly.

@hexaltation
Copy link
Collaborator Author

hexaltation commented Aug 21, 2024

We plan to use the already existing method initializeSpecialIds in app/gen-server/lib/homedb/UsersManager.ts

* What is their `Email`

DOCAPIKEY_MAIL default value set to [email protected]

* What is their `UserID`, how is that decided, is it a constant

The user id is set dynamically by _maybeCreateSpecialUserId

* What is their `Name`

Docapikey

If you have better suggestions for name and email, I'll be glad to take it :).

@paulfitz
Copy link
Member

paulfitz commented Aug 22, 2024

We plan to use the already existing method initializeSpecialIds in app/gen-server/lib/homedb/UsersManager.ts

* What is their `Email`

DOCAPIKEY_MAIL default value set to [email protected]

* What is their `UserID`, how is that decided, is it a constant

The user id is set dynamically by _maybeCreateSpecialUserId

* What is their `Name`

Docapikey

If you have better suggestions for name and email, I'll be glad to take it :).

Ah, so these are concrete, real, user accounts (rows of the users table and logins table) with one created per docapikey? Are they members of any site or enumeration?

(I think the exact email address you propose wouldn't be great for gristlabs.com but an alternative with a different domain that was clearly a black hole would be easy) Edit: oh if it is literally the single string [email protected], that's not a problem - I interpreted it that as docapikey expanded to the actual key, but now I think I read it wrong? But then there could be a problem since distinct users are expected to have distinct email addresses by some parts of Grist.

Do the users get cleaned up when the associated key gets removed?

Edit: I'm now wondering if I misinterpreted everything and this is one single user account for the full installation?

@hexaltation
Copy link
Collaborator Author

Ah, so these are concrete, real, user accounts (rows of the users table and logins table) with one created per docapikey? Are they members of any site or enumeration?

When we discussed about the feature with @fflorent we were more on a unique special user for all DocApiKeys.
What I propose here is to create a DOCAPIKEY_USER whom rights will be determined in the authorizer when evaluating the rights attached to the docApiKey, masked with the proper ACLs.

In my first try of implementation it was through setting some editors or viewers rights to ANONYMOUS_USER in the Authorizer.

But I wasn't sure it was the good way to implement it. And what is the proper/best way to do it was as a feature and technically was (may be still is 😓 ) my initial question at the top of this PR.

May be stay with the ANONYMOUS_USER rather than inventing a new special user DOCAPIKEY_USER is a better idea.

@dsagal
Copy link
Member

dsagal commented Aug 26, 2024

Doc API keys would be a really valuable feature. It’s worth doing it well. I am glad it’s going through a real proposal + review process. I decided to chime in here with some wishes.

Wishes:

  1. I'd like to be able to get a listing of all doc-api-keys via User Manager (perhaps indirectly, via a link like “Document API keys: 3 [Manage]”). If a doc-api-key is limited to a particular user’s access, then just having that user listed may be sufficient. Mainly, I’d like to avoid the situation of “Someone is making changes to this document, and it’s no one listed in User Manager”. That would be worrisome.
  2. When examining who made a change to a document (e.g. via Action History, or logs), I’d like to know if it was a doc-api-key. I’d also like to be able to get to the information about this key: identifier, description, who and when created it.
    • I feel there is an important difference from shares as used for forms: those identify all requests as the "anonymous user". That’s OK for a published form, since the special link is a kind of “secret” that’s normally intentionally made public. For a doc-api-key, the main use case is for the key to be actually secret, only known to a single tool (e.g. a script), which was given this secret key at the time the key was created. It does not seem great to identify such requests as the anonymous user.
  3. I’d like Access Rules to be usable for doc-api-keys. For example, one might create a doc-api-key to allow a script to sync a list of employees between a Grist doc and a public listing of staff names and roles, but leaking this key shouldn’t give access to the employees’ sensitive information.
    • In particular, I’d like for it to be possible to give less access to a key that I create, than I have myself, even if the implementation translates my key to my email address. I.e. requests using this key should have some properties to distinguish them from requests I make via the browser or using my personal API key.
  4. A security-oriented wish: when a doc-api-key is created, it shouldn’t ever be shown in plain-text again. This seems a best practice that I’ve seen in most other products that support granular-access keys. This would involve NOT storing the key in plain-text, but as a hashed value (e.g. using SHA-256). In particular, implementation should minimize places where the key is sent or stored in plain-text.
    • The proposal suggests owners could read doc-api-keys. I think it’s better to only allow reading descriptions, identifier, and other info about the key, not the key itself. If an owner needs a key for some purpose, they should generate a new one.
  5. I’d like to know the last time a key was used. This helps in revoking stale keys.

My thoughts on some questions listed earlier:

  • Do we want to be able to have a link beetween a key and its creator?
    • Yes, for wish 2 above.
  • Do we want to apply fine ACL to docApiKeys?
    • Yes, wish 3 above.
  • Are DocApiKey still valid shares, or should we inhinbit the ability to display the document in a web browser using the docApiKey?
    • I don’t see a clear need for this, and I expect it would be additional work to implement safely.

I am also interested in questions that have already been asked:

  • If a creator of a key loses their Owner-level access: does the key continue working or gets revoked? (I can see both options as valuable, in different cases.)
  • Who can revoke keys: can any owner revoke any key, even those they didn’t create?

@hexaltation
Copy link
Collaborator Author

@dsagal Thank you for your answers.

We agree on everything you propose.
The UserManager adjustment surely needs UX/UI design by @lusebille if we take this way.

It seems that your answers leads us to implement OAuth2 for these service keys.

At least :

  • Scope (For now limited to documents, Read and Write)
  • Revocation
  • Validity Period
  • Access Token
  • Refresh Token

So we must drop the idea of using the Shares mechanism under the hood.

If a creator of a key loses their Owner-level access: does the key continue working or gets revoked? (I can see both options as valuable, in different cases.)

In our mind a key is attached to a document, Other owners can decide what is the future of the key when a previous owner loose priviledges.

Who can revoke keys: can any owner revoke any key, even those they didn’t create?

So any owner can revoke the key.

@dsagal
Copy link
Member

dsagal commented Aug 27, 2024

* Validity Period 
* Access Token
* Refresh Token

I don't think these points are required by my wishes, and seem both more difficult to implement and to use. But I may be missing something.

So we must drop the idea of using the Shares mechanism under the hood.

For under the hood, it seems to me that if a key is limited to its creator's privileges, as Paul suggested, then it could be a mechanism very similar to existing API keys; and if a key can exist separately from its owner, then the Shares mechanism might work for it, with some adjustments.

In our mind a key is attached to a document, Other owners can decide what is the future of the key when a previous owner loose priviledges.

To give owners a chance to decide this, we should check for keys and ask what to do with them whenever any user's access is removed or reduced, not only on the document level, but also for a workspace or org (since access can be inherited). Would it be a good idea, then, to require the user making the access changes to take responsibility for these keys? So that each key remains associated with a current owner of the document?

@hexaltation
Copy link
Collaborator Author

As a new implementation have been designed in #1217 I close this MR

@hexaltation hexaltation closed this Oct 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
anct preview Launch preview deployment of this PR
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Service API key
5 participants