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

CSS-5674 Replace database-stored access with OpenFGA #1051

Merged

Conversation

babakks
Copy link
Member

@babakks babakks commented Oct 2, 2023

Description

This PR replaces remainings of JIMM.v1 access storage with OpenFGA; namely the following has been updated:

  • Grant/revoke cloud access.

I've added a lot of test cases. To make the review easier here is the list tests:

GrantCloudAccess

  • CloudNotFound
  • Admin grants admin access
  • Admin grants add-model access
  • UserNotAuthorized
  • DialError
  • APIError

RevokeCloudAccess

  • CloudNotFound
  • Admin revokes 'admin' from another admin
  • Admin revokes 'add-model' from another admin
  • Admin revokes 'add-model' from a user with 'add-model' access
  • Admin revokes 'add-model' from a user with no access
  • Admin revokes 'admin' from a user with no access
  • Admin revokes 'add-model' access from a user who has separate tuples for all accesses (add-model/admin)
  • Admin revokes 'admin' access from a user who has separate tuples for all accesses (add-model/admin)
  • UserNotAuthorized
  • DialError
  • APIError

GrantModelAccess

  • ModelNotFound
  • Admin grants 'admin' access to a user with no access
  • Admin grants 'write' access to a user with no access
  • Admin grants 'read' access to a user with no access
  • Admin grants 'write' access to a user who already has 'write' access
  • Admin grants 'read' access to a user who already has 'write' access
  • Admin grants 'admin' access to themselves
  • Admin grants 'write' access to themselves
  • Admin grants 'read' access to themselves
  • UserNotAuthorized
  • DialError
  • APIError

RevokeModelAccess

  • Admin revokes 'admin' access from another admin
  • Admin revokes 'write' access from another admin
  • Admin revokes 'read' access from another admin
  • Admin revokes 'admin' access from a user who has 'write' access
  • Admin revokes 'write' access from a user who has 'write' access
  • Admin revokes 'read' access from a user who has 'write' access
  • Admin revokes 'admin' access from a user who has 'read' access
  • Admin revokes 'write' access from a user who has 'read' access
  • Admin revokes 'read' access from a user who has 'read' access
  • Admin revokes 'admin' access from themselves
  • Admin revokes 'write' access from themselves
  • Admin revokes 'read' access from themselves
  • Writer revokes 'admin' access from themselves
  • Writer revokes 'write' access from themselves
  • Writer revokes 'read' access from themselves
  • Reader revokes 'admin' access from themselves
  • Reader revokes 'write' access from themselves
  • Reader revokes 'read' access from themselves
  • Admin revokes 'admin' access from a user who has separate tuples for all accesses (read/write/admin)
  • Admin revokes 'write' access from a user who has separate tuples for all accesses (read/write/admin)
  • Admin revokes 'read' access from a user who has separate tuples for all accesses (read/write/admin)
  • UserNotAuthorized
  • DialError
  • APIError

Fixes CSS-5674

Change of behavior: ModifyCloudAccess

Note that there is a change in behavior for when revoking a users's access to a cloud. In the old implementation, if we revoked the admin access, the user would be granted with the add-model access:

switch access {
case "admin":
	uca.Access = "add-model" // <- Here 
default:
	uca.Access = ""
}

But in the new implementation, we just drop the admin relation and do not create a separate tuple to grant add-model access.

Change of behavior: ModifyModelAccess

In the old implementation when we're revoking a user's model access, we'd do it like this (i.e., demoting user access by one level):

switch access {
case "admin":
	uma.Access = "write"
case "write":
	uma.Access = "read"
default:
	uma.Access = ""
}

But in the new implementation, we don't add new relations to lower accesses. We just drop the relation together with all higher accesses. For example, when revoking read access, we drop read, write and admin all together.

Engineering checklist

Check only items that apply

  • Documentation updated
  • Covered by unit tests
  • Covered by integration tests

@babakks babakks changed the base branch from main to feature-rebac October 2, 2023 12:56
alesstimec and others added 6 commits October 2, 2023 14:59
- removes the need for basic auth from JIMM as it will now use JWTs which Juju controllers trust
Signed-off-by: Babak K. Shandiz <[email protected]>
@babakks babakks force-pushed the css-5674/replace-db-with-openfga branch from dd04d06 to 6adda6d Compare October 2, 2023 14:01
@babakks babakks marked this pull request as ready for review October 4, 2023 13:40
uca = a
break
currentRelation := targetOfgaUser.GetCloudAccess(ctx, ct)
if currentRelation != ofganames.NoRelation {
Copy link
Contributor

Choose a reason for hiding this comment

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

Might this be better as a switch statement?

}
return nil
})
if err := api.GrantCloudAccess(ctx, ct, ut, access); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to do this? This is assigning access on the Juju side I believe, but Juju won't be checking it's own access permissions, it will just rely on whatever is in the JWT.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah looking at

// TODO (alesstimec) This will no longer be needed.
I'm pretty sure this isn't needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

agreed.. this should no longer be needed..

}
}

if err := api.GrantModelAccess(ctx, mt, ut, access); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

As above, I don't think we need these calls anymore.

existingRelations[tt.Tuple.Relation] = nil
}

if ct == lastCT {
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this come before we range over tts?


lastCT := ""
existingRelations := map[Relation]interface{}{}
for {
Copy link
Contributor

Choose a reason for hiding this comment

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

Also what do you think about in a future PR if we create an abstraction over this to have a cleaner way of getting all the results?

Copy link
Member Author

Choose a reason for hiding this comment

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

As I checked this is the only place we're iterating using continuation tokens. So, at the moment, there's no need to split this part away. But you're right. If we're going to do the pagination again, we should do it in a reusable way.

Copy link
Contributor

@ale8k ale8k left a comment

Choose a reason for hiding this comment

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

Some questions and potential changes

return errors.E(op, errors.CodeBadRequest, "failed to recognize given access", err)
}

err = j.doCloudAdmin(ctx, user, ct, func(c *dbmodel.Cloud, api API) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

The callback still takes dbmodel.Cloud, is this needed now? I'd presume not right?

if err != nil {
ale.Params["err"] = err.Error()
return errors.E(op, err)
return errors.E(err, "cannot update OpenFGA record after updating cloud")
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't the error more a tuple issue just from ofga?

Copy link
Contributor

Choose a reason for hiding this comment

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

also, please include op in the error

Copy link
Member Author

Choose a reason for hiding this comment

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

Since there's no Juju API call anymore, I changed the error message to failed to set cloud access.

}
return nil
})
if err := api.RevokeCloudAccess(ctx, ct, ut, access); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't think this is needed either, api typically hits the controller/model, we wanna keep this purely in JIMM

return err
}

err = targetOfgaUser.SetCloudAccess(ctx, ct, targetRelation)
Copy link
Contributor

Choose a reason for hiding this comment

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

Sometime we do err := p(); err != nil, and sometimes two lines, let's try keep it inline if's with errs where we can

requiredAccess = "read"
}

err = j.doModel(ctx, user, mt, requiredAccess, func(m *dbmodel.Model, api API) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Like doCloud, doCloudAdmin, doModelAdmin, we don't need the model passing in anymore do we?

return err
}

err = targetOfgaUser.UnsetModelAccess(ctx, mt, relationsToRevoke...)
Copy link
Contributor

Choose a reason for hiding this comment

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

We aren't transactional ATM are we? So this may remove some but not all?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, at last we decided to go with the transactional approach. To be precise, if we're removing a few tuples, all-or-none will be deleted.

internal/jujuapi/cloud.go Show resolved Hide resolved
internal/openfga/user.go Show resolved Hide resolved
@@ -318,6 +339,56 @@ func setResourceAccess[T ofganames.ResourceTagger](ctx context.Context, user *Us
return nil
}

// unsetMultipleResourceAccesses deletes relations that correspond to the requested resource access, atomically.
// Note that the action is idempotent (i.e., does not return error if any of the relations does not exist).
func unsetMultipleResourceAccesses[T ofganames.ResourceTagger](ctx context.Context, user *User, resource T, relations []Relation) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Honestly I'm a bit lost with ct and tts, tts is targetTuples I'm presuming? and CT is convertTag? Would prefer something like lastCT for lastConvertTag and just be explicit so it's easier on the brain initially

Copy link
Member Author

Choose a reason for hiding this comment

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

No. CT stands for continuation token and TT(s) is timestamped-tuple(s). But you're right. I've improved the namings.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think honestly just call it "lastContinuationToken", as I'll probably forget in another 3 months

Copy link
Member Author

Choose a reason for hiding this comment

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

I've already changed it to just token and lastToken. But can change it to continuationToken and lastContinuationToken if you think so.

Copy link
Contributor

Choose a reason for hiding this comment

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

I know it's not the "go" way, but we have a lot of context and I feel personally I'll just forget what last token is :D

Copy link
Member Author

Choose a reason for hiding this comment

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

Will sort it now.

Copy link
Contributor

@alesstimec alesstimec left a comment

Choose a reason for hiding this comment

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

LGTM, but please add logging so that we have an easier time debugging issues in the future..

},
targetRelation, err := ToCloudRelation(access)
if err != nil {
return errors.E(op, errors.CodeBadRequest, "failed to recognize given access", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

could we, please log the access? might be useful for debugging purposes

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you think it should be a debug entry? or error? If it's for debugging I think debug should suffice.

})

if err != nil {
return errors.E(op, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

please log this error

})

if err != nil {
return errors.E(op, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

please log this error

})

if err != nil {
return errors.E(op, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

please the log this error

})

if err != nil {
return errors.E(op, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

please log this error

Copy link
Contributor

@ale8k ale8k left a comment

Choose a reason for hiding this comment

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

lgtm bar ales' request for log lines

Signed-off-by: Babak K. Shandiz <[email protected]>
@babakks
Copy link
Member Author

babakks commented Oct 12, 2023

lgtm bar ales' request for log lines

I've already added them.

@babakks babakks merged commit 61c1370 into canonical:feature-rebac Oct 12, 2023
2 checks passed
@babakks babakks deleted the css-5674/replace-db-with-openfga branch October 12, 2023 10:42
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.

4 participants