Enhance policy framework to return failure reasons
<!--IssueSummary start-->
<details>
<summary>
Everyone can contribute. [Help move this issue forward](https://handbook.gitlab.com/handbook/marketing/developer-relations/contributor-success/community-contributors-workflows/#contributor-links) while earning points, leveling up and collecting rewards.
</summary>
- [Close this issue](https://contributors.gitlab.com/manage-issue?action=close&projectId=278964&issueIid=362708)
</details>
<!--IssueSummary end-->
## Problem
Our policy framework is very powerful at allowing us to allow/prevent operations based on certain conditions. It also represents a very effective way to implement broad permissions block such as when:
- the root namespace runs out of storage
- the CI_JOB_TOKEN [scope is limited](https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#limit-gitlab-cicd-job-token-access)
- the user is blocked
However, in some occasions we would like to tell the user why a certain operation was prevented.
Workarounds so far are not ideal and include things like:
- running a specific condition before the policy check in order to get more detailed message
- duplicating the specific condition for other related operations that are also prevented
- provide the user with other clues in the UI (e.g. banner) when a condition fails - this UX doesn't propagate to other use cases (API, GraphQL, background jobs, etc.)
- troubleshooting docs on error messages
### A real example
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84471#note_924411580: We prevent a lot of operations when a group runs out of storage. To effectively block all these operations in one go we use the policy framework. The problem is that the user gets a generic message such as `Insufficient permissions to create a new pipeline` because `create_pipeline` is prevented. This error message could be identical if the user is not a developer of the project.
## Idea
Could we expand our policy framework to allow error messages to be defined and returned? This way we could propagate them to the end-user whenever needed.
```ruby
rule { over_storage_limit }.policy do
failure_message 'Insufficient storage available'
prevent :create_pipeline
prevent :create_build
# ...
end
```
and we could provide a different method to check the ability which returns a result object
```ruby
check = Ability.check(current_user, :create_pipeline, project)
if check.allowed?
# do something
else
ServiceResponse.error(check.failure_message || 'Unable to create a pipeline')
end
```
### Acceptance criteria
`Ability.allowed?(current_user, :create_pipeline, project)` should still continue to work and return "message-less" boolean results for backward compatibility.
issue