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

Harness not modelling permission denied for user secrets #1229

Closed
DnPlas opened this issue May 27, 2024 · 7 comments · Fixed by #1248
Closed

Harness not modelling permission denied for user secrets #1229

DnPlas opened this issue May 27, 2024 · 7 comments · Fixed by #1248
Assignees
Labels
bug Something isn't working testing Related to ops.testing

Comments

@DnPlas
Copy link

DnPlas commented May 27, 2024

#1176 introduced support for user secrets in Harness, but it looks like it is not modelling the actual Juju behaviour when a user secret hasn't been granted to a charm and it tries to get a secret with self.model.get_secret(id=<id-from-config>).

On an actual model, if the charm config gets updated with a secret ID, but the charm hasn't been granted access to the secret, ops.model.ModelError: ERROR permission denied will be raised, but it seems that when testing, harness makes the charm to raise a different exception ops.model.SecretNotFoundError: Secret 'secret:16320250-1b76-4552-94d9-10ff484f88cb' not granted access to 'istio-pilot when calling the exact same piece of code.

Steps to reproduce

  1. Deploy any charm that can use user secrets
  2. Create a user secret
  3. Pass the secret ID through charm configuration juju config <charm-name> my-secret=secret:some-id
  4. Observe the charm state and logs. You should get something like this:
  File "/var/lib/juju/agents/unit-istio-pilot-0/charm/venv/ops/model.py", line 3397, in secret_get
    result = self._run('secret-get', *args, return_output=True, use_json=True)
  File "/var/lib/juju/agents/unit-istio-pilot-0/charm/venv/ops/model.py", line 3051, in _run
    raise ModelError(e.stderr) from e
ops.model.ModelError: ERROR permission denied
  1. Add a unit test for testing what happens when the secret hasn't been granted to the charm. For example:
def test_permission_denied(...):
  secret_content = {"tls-crt": "a-tls-crt", "tls-key": "a-tls-key"}
  secret_id = harness.add_user_secret(secret_content)
  harness.update_config({"tls-secret-id": secret_id})
  harness.begin()
  with pytest.raises(ModelError) as err:
    harness.charm.function_call_to_get_secret() # <--- this is a function that calls self.model.get_secret(id=<id from config>)
  1. The above will not raise because it is expecting a ModelError but a SecretNotFoundError exception is raised instead:
 ops.model.SecretNotFoundError: Secret 'secret:16320250-1b76-4552-94d9-10ff484f88cb' not granted access to 'istio-pilot
@DnPlas
Copy link
Author

DnPlas commented May 27, 2024

Extra information:

According to get_secret docs, it only raises SecretNotFoundError if the secret does not exist, but _ModelBackend.secret_get, which is called by the model.get_secret() method, will raise a ModelError under other conditions, which can include the permission denied scenario that I am describing.

@benhoyt
Copy link
Collaborator

benhoyt commented May 27, 2024

@IronCore864 @tonyandrewmeyer It makes sense to me to make the testing exception the same as the real one. So I think we just change the SecretNotFoundError here to ModelError. Sound reasonable?

@tonyandrewmeyer
Copy link
Contributor

@IronCore864 @tonyandrewmeyer It makes sense to me to make the testing exception the same as the real one.

I do agree with this in general.

So I think we just change the SecretNotFoundError here to ModelError. Sound reasonable?

However, I don't think this is the right fix here. I think this is a Juju bug. All of the other permission errors (like trying to do a get_info() when only having view permission, or trying to get a charm secret that you haven't been granted permission to) raise SecretNotFoundError). Apart from the inconsistency, behaving differently when there is a secret with no access and when there is no secret feels leaky to me.

So, in my opinion, we should open a Juju bug instead.

@tonyandrewmeyer tonyandrewmeyer added bug Something isn't working testing Related to ops.testing labels May 28, 2024
@tonyandrewmeyer
Copy link
Contributor

So, in my opinion, we should open a Juju bug instead.

Checked with the Juju team and they agree that it should be consistent in Juju, so I opened a Juju bug.

@tonyandrewmeyer
Copy link
Contributor

According to get_secret docs, it only raises SecretNotFoundError if the secret does not exist

Opened #1231 to make this explicit in the docs.

benhoyt pushed a commit that referenced this issue May 29, 2024
…#1231)

As noticed in #1229, `get_secret()` may raise `SecretNotFoundError` if
the secret does exist but the caller does not have permission to view
it, so make this explicit in the documentation.
@tonyandrewmeyer
Copy link
Contributor

Actually, I got this wrong. Charm secrets do return "permission denied" as well as user ones, so there must be an ops issue here.

@tonyandrewmeyer tonyandrewmeyer self-assigned this Jun 5, 2024
@tonyandrewmeyer
Copy link
Contributor

tonyandrewmeyer commented Jun 5, 2024

Juju 3.4.2, Microk8s, I get this behaviour:

hook tool issue user secret charm secret
secret-get no permission "permission denied" "permission denied"
secret-info-get no permission "not found" "not found"
secret-set no permission delayed permission denied delayed permission denied
secret-remove no permission delayed permission denied delayed permission denied
secret-grant no permission "not found" "not found"
secret-revoke no permission delayed permission denied delayed permission denied
secret-get doesn't exist "not found" "not found"
secret-info-get doesn't exist "not found" "not found"
secret-set doesn't exist delayed not found delayed not found
secret-remove doesn't exist delayed not found delayed not found
secret-grant doesn't exist "not found" "not found"
secret-revoke doesn't exist delayed not found delayed not found

For the "delayed" cases, the hook tool returns successfully, and then when the hook finishes (after ops is done) the charm goes into error state with something like this in the debug-log:

unit-dummycharm-1: 12:56:16 ERROR juju.worker.uniter.context cannot apply changes: removing secrets: permission denied
unit-dummycharm-1: 12:56:16 ERROR juju.worker.uniter.operation hook "config-changed" (via hook dispatching script: dispatch) failed: removing secrets: permission denied

Also discussing this with @barrettj12 who is working on the Juju ticket.

amandahla pushed a commit to amandahla/operator that referenced this issue Jun 26, 2024
Adjust Harness so that the errors match what Juju does in practice (I
tested each case - the results are in a table in a comment in canonical#1229).
* When Juju responds with a message including "not found", we raise
`SecretNotFoundError`
* When Juju responds with any other message (such as "permission
denied"), we raise `ModelError`
* When Juju succeeds and only fails at the completion of the hook, we
raise `RuntimeError` - in production, the charm cannot catch this, but
we do want to surface this error, so `RuntimeError` seems most
approprate

In addition:
* Update the documentation to clarify when secret content is cached
(this was a request from the discussion with the data platform team in
Madrid)
* Correct the documentation regarding when `ModelError` and when
`SecretNotFoundError` will be raised with the secret methods
* Don't clear the `Secret` object local cache of the secret content on
`set_contents`. The contents won't change until `refresh` is used, so
there's no point forcing the next call to get the content from Juju.

Juju may change the responses so that there is increased consistency
(probably via [this bug](https://bugs.launchpad.net/juju/+bug/2067336))
but it seems best if we fix the behaviour to match now, and then if Juju
changes in the future, we can update ops then (also deciding at that
time how to handle the issue that different Juju versions will have
different behaviour).

Fixes canonical#1229.
tonyandrewmeyer added a commit to tonyandrewmeyer/operator that referenced this issue Jun 26, 2024
Adjust Harness so that the errors match what Juju does in practice (I
tested each case - the results are in a table in a comment in canonical#1229).
* When Juju responds with a message including "not found", we raise
`SecretNotFoundError`
* When Juju responds with any other message (such as "permission
denied"), we raise `ModelError`
* When Juju succeeds and only fails at the completion of the hook, we
raise `RuntimeError` - in production, the charm cannot catch this, but
we do want to surface this error, so `RuntimeError` seems most
approprate

In addition:
* Update the documentation to clarify when secret content is cached
(this was a request from the discussion with the data platform team in
Madrid)
* Correct the documentation regarding when `ModelError` and when
`SecretNotFoundError` will be raised with the secret methods
* Don't clear the `Secret` object local cache of the secret content on
`set_contents`. The contents won't change until `refresh` is used, so
there's no point forcing the next call to get the content from Juju.

Juju may change the responses so that there is increased consistency
(probably via [this bug](https://bugs.launchpad.net/juju/+bug/2067336))
but it seems best if we fix the behaviour to match now, and then if Juju
changes in the future, we can update ops then (also deciding at that
time how to handle the issue that different Juju versions will have
different behaviour).

Fixes canonical#1229.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working testing Related to ops.testing
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants