Merge pull request #965 from grafana/dev

Merge dev to main
This commit is contained in:
Innokentii Konstantinov 2022-12-09 12:35:05 +08:00 committed by GitHub
commit 70372c2f22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
316 changed files with 12561 additions and 3186 deletions

3
.github/CODEOWNERS vendored Normal file
View file

@ -0,0 +1,3 @@
* @grafana/grafana-oncall-backend
/grafana-plugin @grafana/grafana-oncall-frontend

View file

@ -1,10 +1,10 @@
[
{
"type": "label",
"name": "type/docs",
"action": "addToProject",
"addToProject": {
"url": "https://github.com/orgs/grafana/projects/69"
}
[
{
"type": "label",
"name": "type/docs",
"action": "addToProject",
"addToProject": {
"url": "https://github.com/orgs/grafana/projects/69"
}
}
]

View file

@ -1,8 +1,9 @@
**What this PR does**:
# What this PR does
**Which issue(s) this PR fixes**:
## Which issue(s) this PR fixes
## Checklist
**Checklist**
- [ ] Tests updated
- [ ] Documentation added
- [ ] `CHANGELOG.md` updated
- [ ] `CHANGELOG.md` updated

View file

@ -27,7 +27,7 @@ jobs:
run: |
pre-commit run --all-files
test:
test-frontend:
runs-on: ubuntu-latest
container: python:3.9
steps:
@ -56,8 +56,12 @@ jobs:
docker run -v ${PWD}/docs/sources:/hugo/content/docs/oncall/latest -e HUGO_REFLINKSERRORLEVEL=ERROR --rm grafana/docs-base:latest /bin/bash -c 'make hugo'
unit-test-backend-mysql-rabbitmq:
name: "Backend Tests: MySQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
container: python:3.9
strategy:
matrix:
rbac_enabled: ["True", "False"]
env:
DJANGO_SETTINGS_MODULE: settings.ci-test
SLACK_CLIENT_OAUTH_ID: 1
@ -72,7 +76,6 @@ jobs:
env:
MYSQL_DATABASE: oncall_local_dev
MYSQL_ROOT_PASSWORD: local_dev_pwd
steps:
- uses: actions/checkout@v2
- name: Unit Test Backend
@ -80,11 +83,15 @@ jobs:
apt-get update && apt-get install -y netcat
cd engine/
pip install -r requirements.txt
./wait_for_test_mysql_start.sh && pytest --ds=settings.ci-test -x
./wait_for_test_mysql_start.sh && ONCALL_TESTING_RBAC_ENABLED=${{ matrix.rbac_enabled }} pytest -x
unit-test-backend-postgresql-rabbitmq:
name: "Backend Tests: PostgreSQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
container: python:3.9
strategy:
matrix:
rbac_enabled: ["True", "False"]
env:
DATABASE_TYPE: postgresql
DJANGO_SETTINGS_MODULE: settings.ci-test
@ -112,11 +119,15 @@ jobs:
run: |
cd engine/
pip install -r requirements.txt
pytest --ds=settings.ci-test -x
ONCALL_TESTING_RBAC_ENABLED=${{ matrix.rbac_enabled }} pytest -x
unit-test-backend-sqlite-redis:
name: "Backend Tests: SQLite + Redis (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
container: python:3.9
strategy:
matrix:
rbac_enabled: ["True", "False"]
env:
DATABASE_TYPE: sqlite3
BROKER_TYPE: redis
@ -131,7 +142,6 @@ jobs:
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- name: Unit Test Backend
@ -139,7 +149,7 @@ jobs:
apt-get update && apt-get install -y netcat
cd engine/
pip install -r requirements.txt
pytest --ds=settings.ci-test -x
ONCALL_TESTING_RBAC_ENABLED=${{ matrix.rbac_enabled }} pytest -x
docker-build:
runs-on: ubuntu-latest

12
.markdownlint.json Normal file
View file

@ -0,0 +1,12 @@
{
"default": true,
"MD013": {
"line_length": "120"
},
"MD024": {
"siblings_only": true
},
"MD033": {
"allowed_elements": ["img"]
}
}

View file

@ -39,10 +39,16 @@ repos:
rev: "v2.7.1"
hooks:
- id: prettier
types_or: [css, javascript, jsx, ts, tsx, json]
name: prettier
types_or: [css, javascript, jsx, ts, tsx]
files: ^grafana-plugin/src
additional_dependencies:
- prettier@^2.7.1
- id: prettier
name: prettier - json
types_or: [json]
additional_dependencies:
- prettier@^2.7.1
- repo: https://github.com/thibaudcolas/pre-commit-stylelint
rev: v13.13.1
@ -56,3 +62,13 @@ repos:
- stylelint-prettier@^2.0.0
- stylelint-config-standard@^22.0.0
- stylelint-config-prettier@^9.0.3
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.32.2
hooks:
- id: markdownlint
name: markdownlint
entry: markdownlint --ignore grafana-plugin/node_modules --ignore grafana-plugin/dist --ignore docs **/*.md
- id: markdownlint
name: markdownlint - docs
entry: markdownlint --ignore grafana-plugin/node_modules --ignore grafana-plugin/dist -c ./docs/.markdownlint.json ./docs/**/*.md

View file

@ -5,14 +5,42 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## v1.2.0 (TBD)
### Added
- RBAC permission support
- Add `time_zone` serializer validation for OnCall shifts and calendar/web schedules. In addition, add database migration
to update values that may be invalid
- Add a `permalinks.web` field, which is a permalink to the alert group web app page, to the alert group internal/public
API responses
- Added the ability to customize job-migrate `ttlSecondsAfterFinished` field in the helm chart
### Fixed
- Got 500 error when saving Outgoing Webhook ([#890](https://github.com/grafana/oncall/issues/890))
- v1.0.13 helm chart - update the OnCall backend pods image pull policy to "Always" (and explicitly set tag to `latest`).
This should resolve some recent issues experienced where the frontend/backend versions are not aligned.
### Changed
- When editing templates for alert group presentation or outgoing webhooks, errors and warnings are now displayed in
the UI as notification popups or displayed in the preview.
- Errors and warnings that occur when rendering templates during notification or webhooks will now render
and display the error/warning as the result.
## v1.1.5 (2022-11-24)
### Added
- Added a QR code in the "Mobile App Verification" tab on the user settings modal to connect the mobile
application to your OnCall instance
### Fixed
- UI bug fixes for Grafana 9.3 ([#860](https://github.com/grafana/oncall/pull/860))
- Bug fix for saving source link template ([#898](https://github.com/grafana/oncall/pull/898))
## v1.1.4 (2022-11-23)
### Fixed
@ -26,13 +54,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- For OSS installations of OnCall, initial configuration is now simplified. When running for local development, you no longer need to configure the plugin via the UI. This is achieved through passing one environment variable to both the backend & frontend containers, both of which have been preconfigured for you in `docker-compose-developer.yml`.
- The Grafana API URL **must be** passed as an environment variable, `GRAFANA_API_URL`, to the OnCall backend (and can be configured by updating this env var in your `./dev/.env.dev` file)
- The OnCall API URL can optionally be passed as an environment variable, `ONCALL_API_URL`, to the OnCall UI. If the environment variable is found, the plugin will "auto-configure", otherwise you will be shown a simple configuration form to provide this info.
- For Helm installations, if you are running Grafana externally (eg. `grafana.enabled` is set to `false` in your `values.yaml`), you will now be required to specify `externalGrafana.url` in `values.yaml`.
- `make start` will now idempotently check to see if a "127.0.0.1 grafana" record exists in `/etc/hosts` (using a tool called [`hostess`](https://github.com/cbednarski/hostess)). This is to support using `http://grafana:3000` as the `Organization.grafana_url` in two scenarios:
- For OSS installations of OnCall, initial configuration is now simplified. When running for local development, you no
longer need to configure the plugin via the UI. This is achieved through passing one environment variable to both the
backend & frontend containers, both of which have been preconfigured for you in `docker-compose-developer.yml`.
- The Grafana API URL **must be** passed as an environment variable, `GRAFANA_API_URL`, to the OnCall backend
(and can be configured by updating this env var in your `./dev/.env.dev` file)
- The OnCall API URL can optionally be passed as an environment variable, `ONCALL_API_URL`, to the OnCall UI.
If the environment variable is found, the plugin will "auto-configure", otherwise you will be shown a simple
configuration form to provide this info.
- For Helm installations, if you are running Grafana externally (eg. `grafana.enabled` is set to `false`
in your `values.yaml`), you will now be required to specify `externalGrafana.url` in `values.yaml`.
- `make start` will now idempotently check to see if a "127.0.0.1 grafana" record exists in `/etc/hosts`
(using a tool called [`hostess`](https://github.com/cbednarski/hostess)). This is to support using `http://grafana:3000`
as the `Organization.grafana_url` in two scenarios:
- `oncall_engine`/`oncall_celery` -> `grafana` Docker container communication
- public URL generation. There are some instances where `Organization.grafana_url` is referenced to generate public URLs to a Grafana plugin page. Without the `/etc/hosts` record, navigating to `http://grafana:3000/some_page` in your browser, you would obviously get an error from your browser.
- public URL generation. There are some instances where `Organization.grafana_url` is referenced to generate public
URLs to a Grafana plugin page. Without the `/etc/hosts` record, navigating to `http://grafana:3000/some_page` in
your browser, you would obviously get an error from your browser.
## v1.1.2 (2022-11-18)
@ -260,7 +298,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## 1.0.2 (2022-06-17)
- Fix Grafana Alerting integration to handle API changes in Grafana 9
- Improve public api endpoint for outgoing webhooks (/actions) by adding ability to create, update and delete outgoing webhook instance
- Improve public api endpoint for outgoing webhooks (/actions) by adding ability to create, update and delete
outgoing webhook instance
## 1.0.0 (2022-06-14)

View file

@ -2,7 +2,10 @@
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make
participation in our project and our community a harassment-free experience for everyone, regardless of age, body size,
disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race,
religion, or sexual identity and orientation.
## Our Standards
@ -24,19 +27,29 @@ Examples of unacceptable behavior by participants include:
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take
appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits,
issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any
contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the
project or its community. Examples of representing a project or community include using an official project e-mail address,
posting via an official social media account, or acting as an appointed representative at an online or offline event.
Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at conduct@grafana.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at
conduct@grafana.com. The project team will review and investigate all complaints, and will respond in a way that it deems
appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter
of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent
repercussions as determined by other members of the project's leadership.
## Attribution

View file

@ -1,38 +1,46 @@
---
title: Governance
---
# Governance
This document describes the rules and governance of the project. It is meant to be followed by all the developers of the project and the OnCall community. Common terminology used in this governance document are listed below:
This document describes the rules and governance of the project. It is meant to be followed by all the developers
of the project and the OnCall community. Common terminology used in this governance document are listed below:
- **Team members**: Any members of the private [team mailing list][team].
- **Maintainers**: Maintainers lead an individual project or parts thereof ([`MAINTAINERS.md`][maintainers]).
- **Projects**: A single repository in the Grafana GitHub organization and listed below is referred to as a project:
- oncall
- **The OnCall project**: The sum of all activities performed under this governance, concerning one or more repositories or the community.
- **The OnCall project**: The sum of all activities performed under this governance, concerning one or more
repositories or the community.
## Values
The OnCall developers and community are expected to follow the values defined in the [Code of Conduct][coc]. Furthermore, the OnCall community strives for kindness, giving feedback effectively, and building a welcoming environment. The OnCall developers generally decide by consensus and only resort to conflict resolution by a majority vote if consensus cannot be reached.
The OnCall developers and community are expected to follow the values defined in the [Code of Conduct][coc]. Furthermore,
the OnCall community strives for kindness, giving feedback effectively, and building a welcoming environment. The OnCall
developers generally decide by consensus and only resort to conflict resolution by a majority vote if
consensus cannot be reached.
## Projects
Each project must have a [`MAINTAINERS.md`][maintainers] file with at least one maintainer. Where a project has a release process, access and documentation should be such that more than one person can perform a release. Releases should be announced on the [announcements][announce] category at the GitHub Discussions. Any new projects should be first proposed on the [team mailing list][team] following the voting procedures listed below.
Each project must have a [`MAINTAINERS.md`][maintainers] file with at least one maintainer. Where a project has a release
process, access and documentation should be such that more than one person can perform a release. Releases should be
announced on the [announcements][announce] category at the GitHub Discussions. Any new projects should be first proposed
on the [team mailing list][team] following the voting procedures listed below.
## Decision making
### Team members
Team member status may be given to those who have made ongoing contributions to the OnCall project for at least 3 months. This is usually in the form of code improvements and/or notable work on documentation, but organizing events or user support could also be taken into account.
Team member status may be given to those who have made ongoing contributions to the OnCall project for at least 3 months.
This is usually in the form of code improvements and/or notable work on documentation, but organizing events or
user support could also be taken into account.
New members may be proposed by any existing member by email to the [team mailing list][team]. It is highly desirable to reach consensus about acceptance of a new member. However, the proposal is ultimately voted on by a formal [supermajority vote](#supermajority-vote).
New members may be proposed by any existing member by email to the [team mailing list][team]. It is highly desirable
to reach consensus about acceptance of a new member. However, the proposal is ultimately voted on by a
formal [supermajority vote](#supermajority-vote).
If the new member proposal is accepted, the proposed team member should be contacted privately via email to confirm or deny their acceptance of team membership. This email will also be CC'd to the [team mailing list][team] for record-keeping purposes.
If the new member proposal is accepted, the proposed team member should be contacted privately via email to confirm
or deny their acceptance of team membership. This email will also be CC'd to the [team mailing list][team] for
record-keeping purposes.
If they choose to accept, the [onboarding](#onboarding) procedure is followed.
@ -57,6 +65,7 @@ The current team members are:
- Yulia Shanyrova — [@Ukochka](https://github.com/Ukochka) ([Grafana Labs](https://grafana.com/))
- Maxim Mordasov — [@maskin25](https://github.com/maskin25) ([Grafana Labs](https://grafana.com/))
- Julia Artyukhina — [@Ferril](https://github.com/Ferril) ([Grafana Labs](https://grafana.com/))
- Joey Orlando - [@joeyorlando](https://github.com/joeyorlando) ([Grafana Labs](https://grafana.com/))
Previous team members:
@ -64,21 +73,31 @@ Previous team members:
### Maintainers
Maintainers lead one or more project(s) or parts thereof and serve as a point of conflict resolution amongst the contributors to this project. Ideally, maintainers are also team members, but exceptions are possible for suitable maintainers that, for whatever reason, are not yet team members.
Maintainers lead one or more project(s) or parts thereof and serve as a point of conflict resolution amongst the
contributors to this project. Ideally, maintainers are also team members, but exceptions are possible for suitable
maintainers that, for whatever reason, are not yet team members.
Changes in maintainership have to be announced on the [announcemount][announce] category at the GitHub Discussions. They are decided by [rough consensus](#consensus) and formalized by changing the [`MAINTAINERS.md`][maintainers] file of the respective repository.
Changes in maintainership have to be announced on the [announcemount][announce] category at the GitHub Discussions.
They are decided by [rough consensus](#consensus) and formalized by changing the [`MAINTAINERS.md`][maintainers]
file of the respective repository.
Maintainers are granted commit rights to all projects covered by this governance.
A maintainer or committer may resign by notifying the [team mailing list][team]. A maintainer with no project activity for a year is considered to have resigned. Maintainers that wish to resign are encouraged to propose another team member to take over the project.
A maintainer or committer may resign by notifying the [team mailing list][team]. A maintainer with no project activity
for a year is considered to have resigned. Maintainers that wish to resign are encouraged to propose another team
member to take over the project.
A project may have multiple maintainers, as long as the responsibilities are clearly agreed upon between them. This includes coordinating who handles which issues and pull requests.
A project may have multiple maintainers, as long as the responsibilities are clearly agreed upon between them. This
includes coordinating who handles which issues and pull requests.
### Technical decisions
Technical decisions that only affect a single project are made informally by the maintainer of this project, and [rough consensus](#consensus) is assumed. Technical decisions that span multiple parts of the project should be discussed and made on the the [GitHub Discussions][discussions].
Technical decisions that only affect a single project are made informally by the maintainer of this project, and
[rough consensus](#consensus) is assumed. Technical decisions that span multiple parts of the project should be
discussed and made on the the [GitHub Discussions][discussions].
Decisions are usually made by [rough consensus](#consensus). If no consensus can be reached, the matter may be resolved by [majority vote](#majority-vote).
Decisions are usually made by [rough consensus](#consensus). If no consensus can be reached, the matter may be resolved
by [majority vote](#majority-vote).
### Governance changes
@ -86,7 +105,9 @@ Changes to this document are made by Grafana Labs.
### Other matters
Any matter that needs a decision may be called to a vote by any member if they deem it necessary. For private or personnel matters, discussion and voting takes place on the [team mailing list][team], otherwise on the [GitHub Discussions][discussions].
Any matter that needs a decision may be called to a vote by any member if they deem it necessary. For private or
personnel matters, discussion and voting takes place on the [team mailing list][team], otherwise
on the [GitHub Discussions][discussions].
## Voting
@ -94,45 +115,67 @@ The OnCall project usually runs by informal consensus, however sometimes a forma
Depending on the subject matter, as laid out [above](#decision-making), different methods of voting are used.
For all votes, voting must be open for at least one week. The end date should be clearly stated in the call to vote. A vote may be called and closed early if enough votes have come in one way so that further votes cannot change the final decision.
For all votes, voting must be open for at least one week. The end date should be clearly stated in the call to vote.
A vote may be called and closed early if enough votes have come in one way so that further votes cannot
change the final decision.
In all cases, all and only [team members](#team-members) are eligible to vote, with the sole exception of the forced removal of a team member, in which said member is not eligible to vote.
In all cases, all and only [team members](#team-members) are eligible to vote, with the sole exception of the forced
removal of a team member, in which said member is not eligible to vote.
Discussion and votes on personnel matters (including but not limited to team membership and maintainership) are held in private on the [team mailing list][team]. All other discussion and votes are held in public on the [GitHub Discussions][discussions].
Discussion and votes on personnel matters (including but not limited to team membership and maintainership) are held in
private on the [team mailing list][team]. All other discussion and votes are held in public
on the [GitHub Discussions][discussions].
For public discussions, anyone interested is encouraged to participate. Formal power to object or vote is limited to [team members](#team-members).
For public discussions, anyone interested is encouraged to participate. Formal power to object or vote is limited to
[team members](#team-members).
### Consensus
The default decision making mechanism for the OnCall project is [rough][rough] consensus. This means that any decision on technical issues is considered supported by the [team][team] as long as nobody objects or the objection has been considered but not necessarily accommodated.
The default decision making mechanism for the OnCall project is [rough][rough] consensus. This means that any decision
on technical issues is considered supported by the [team][team] as long as nobody objects or the objection has been
considered but not necessarily accommodated.
Silence on any consensus decision is implicit agreement and equivalent to explicit agreement. Explicit agreement may be stated at will. Decisions may, but do not need to be called out and put up for decision on the [GitHub Discussions][discussions] at any time and by anyone.
Silence on any consensus decision is implicit agreement and equivalent to explicit agreement. Explicit agreement may
be stated at will. Decisions may, but do not need to be called out and put up for decision on the
[GitHub Discussions][discussions] at any time and by anyone.
Consensus decisions can never override or go against the spirit of an earlier explicit vote.
If any [team member](#team-members) raises objections, the team members work together towards a solution that all involved can accept. This solution is again subject to rough consensus.
If any [team member](#team-members) raises objections, the team members work together towards a solution that all
involved can accept. This solution is again subject to rough consensus.
In case no consensus can be found, but a decision one way or the other must be made, any [team member](#team-members) may call a formal [majority vote](#majority-vote).
In case no consensus can be found, but a decision one way or the other must be made, any [team member](#team-members)
may call a formal [majority vote](#majority-vote).
### Majority vote
Majority votes must be called explicitly in a separate thread on the appropriate mailing list. The subject must be prefixed with `[VOTE]`. In the body, the call to vote must state the proposal being voted on. It should reference any discussion leading up to this point.
Majority votes must be called explicitly in a separate thread on the appropriate mailing list.
The subject must be prefixed with `[VOTE]`. In the body, the call to vote must state the proposal being voted on.
It should reference any discussion leading up to this point.
Votes may take the form of a single proposal, with the option to vote yes or no, or the form of multiple alternatives.
A vote on a single proposal is considered successful if more vote in favor than against.
If there are multiple alternatives, members may vote for one or more alternatives, or vote “no” to object to all alternatives. It is not possible to cast an “abstain” vote. A vote on multiple alternatives is considered decided in favor of one alternative if it has received the most votes in favor, and a vote from more than half of those voting. Should no alternative reach this quorum, another vote on a reduced number of options may be called separately.
If there are multiple alternatives, members may vote for one or more alternatives, or vote “no” to object to all
alternatives. It is not possible to cast an “abstain” vote. A vote on multiple alternatives is considered decided in
favor of one alternative if it has received the most votes in favor, and a vote from more than half of those voting.
Should no alternative reach this quorum, another vote on a reduced number of options may be called separately.
### Supermajority vote
Supermajority votes must be called explicitly in a separate thread on the appropriate mailing list. The subject must be prefixed with `[VOTE]`. In the body, the call to vote must state the proposal being voted on. It should reference any discussion leading up to this point.
Supermajority votes must be called explicitly in a separate thread on the appropriate mailing list.
The subject must be prefixed with `[VOTE]`. In the body, the call to vote must state the proposal being voted on.
It should reference any discussion leading up to this point.
Votes may take the form of a single proposal, with the option to vote yes or no, or the form of multiple alternatives.
A vote on a single proposal is considered successful if at least two thirds of those eligible to vote vote in favor.
If there are multiple alternatives, members may vote for one or more alternatives, or vote “no” to object to all alternatives. A vote on multiple alternatives is considered decided in favor of one alternative if it has received the most votes in favor, and a vote from at least two thirds of those eligible to vote. Should no alternative reach this quorum, another vote on a reduced number of options may be called separately.
If there are multiple alternatives, members may vote for one or more alternatives, or vote “no” to object to
all alternatives. A vote on multiple alternatives is considered decided in favor of one alternative if it has received
the most votes in favor, and a vote from at least two thirds of those eligible to vote. Should no alternative reach
this quorum, another vote on a reduced number of options may be called separately.
## On- / Offboarding
@ -141,7 +184,8 @@ If there are multiple alternatives, members may vote for one or more alternative
The new member is
- added to the list of [team members](#team-members). Ideally by sending a PR of their own, at least approving said PR.
- announced on the [GitHub Discussions][discussions] by an existing team member. Ideally, the new member replies in this thread, acknowledging team membership.
- announced on the [GitHub Discussions][discussions] by an existing team member. Ideally, the new member
replies in this thread, acknowledging team membership.
- added to the projects with commit rights.
- added to the [team mailing list][team].
@ -149,15 +193,16 @@ The new member is
The ex-member is
- removed from the list of [team members](#team-members). Ideally by sending a PR of their own, at least approving said PR. In case of forced removal, no approval is needed.
- removed from the projects. Optionally, they can retain maintainership of one or more repositories if the [team](#team-members) agrees.
- removed from the list of [team members](#team-members). Ideally by sending a PR of their own, at least approving said PR.
In case of forced removal, no approval is needed.
- removed from the projects. Optionally, they can retain maintainership of one or more repositories
if the [team](#team-members) agrees.
- removed from the team mailing list and demoted to a normal member of the other mailing lists.
- not allowed to call themselves an active team member any more, nor allowed to imply this to be the case.
- added to a list of previous members if they so choose.
If needed, we reserve the right to publicly announce removal.
[announce]: https://github.com/grafana/oncall/discussions/categories/announcements
[coc]: https://github.com/grafana/oncall/blob/dev/CODE_OF_CONDUCT.md
[maintainers]: https://github.com/grafana/oncall/blob/dev/MAINTAINERS.md

View file

@ -8,12 +8,12 @@ The default license for this project is [AGPL-3.0-only](LICENSE).
The following directories and their subdirectories are licensed under Apache-2.0:
```
```text
n/a
```
The following directories and their subdirectories are licensed under their original upstream licenses:
```
```text
n/a
```

View file

@ -1,3 +1,5 @@
# Maintainers
The following are the main/default maintainers:
- Ildar Iskhakov — [@iskhakov](https://github.com/iskhakov) ([Grafana Labs](https://grafana.com/))
@ -6,6 +8,7 @@ The following are the main/default maintainers:
Some parts of the codebase have other maintainers, the package paths also include all sub-packages:
Some parts of the codebase have other maintainers:
- `docs`:
- Eve Meelan - [@Eve832](https://github.com/Eve832) ([Grafana Labs](https://grafana.com/))
- Alyssa Wada - [@alyssawada](https://github.com/alyssawada) ([Grafana Labs](https://grafana.com/))

View file

@ -120,6 +120,9 @@ shell:
dbshell:
$(call run_engine_docker_command,python manage.py dbshell)
engine-manage:
$(call run_engine_docker_command,python manage.py $(CMD))
exec-engine:
docker exec -it oncall_engine bash

View file

@ -1,3 +1,5 @@
# Grafana OnCall
<img width="400px" src="docs/img/logo.png">
[![Latest Release](https://img.shields.io/github/v/release/grafana/oncall?display_name=tag&sort=semver)](https://github.com/grafana/oncall/releases)
@ -26,31 +28,34 @@ We prepared multiple environments:
1. Download [`docker-compose.yml`](docker-compose.yml):
```bash
curl -fsSL https://raw.githubusercontent.com/grafana/oncall/dev/docker-compose.yml -o docker-compose.yml
```
```bash
curl -fsSL https://raw.githubusercontent.com/grafana/oncall/dev/docker-compose.yml -o docker-compose.yml
```
2. Set variables:
```bash
echo "DOMAIN=http://localhost:8080
COMPOSE_PROFILES=with_grafana # Remove this line if you want to use existing grafana
SECRET_KEY=my_random_secret_must_be_more_than_32_characters_long" > .env
```
```bash
echo "DOMAIN=http://localhost:8080
COMPOSE_PROFILES=with_grafana # Remove this line if you want to use existing grafana
SECRET_KEY=my_random_secret_must_be_more_than_32_characters_long" > .env
```
3. Launch services:
```bash
docker-compose up -d
```
```bash
docker-compose pull && docker-compose up -d
```
4. Go to [OnCall Plugin Configuration](http://localhost:3000/plugins/grafana-oncall-app), using log in credentials as defined above: `admin`/`admin` (or find OnCall plugin in configuration->plugins) and connect OnCall _plugin_ with OnCall _backend_:
4. Go to [OnCall Plugin Configuration](http://localhost:3000/plugins/grafana-oncall-app), using log in credentials
as defined above: `admin`/`admin` (or find OnCall plugin in configuration->plugins) and connect OnCall _plugin_
with OnCall _backend_:
```
OnCall backend URL: http://engine:8080
```
```text
OnCall backend URL: http://engine:8080
```
5. Enjoy! Check our [OSS docs](https://grafana.com/docs/grafana-cloud/oncall/open-source/) if you want to set up Slack, Telegram, Twilio or SMS/calls through Grafana Cloud.
5. Enjoy! Check our [OSS docs](https://grafana.com/docs/grafana-cloud/oncall/open-source/) if you want to set up
Slack, Telegram, Twilio or SMS/calls through Grafana Cloud.
## Update version
@ -65,13 +70,14 @@ docker-compose up -d
```
After updating the engine, you'll also need to click the "Update" button on the [plugin version page](http://localhost:3000/plugins/grafana-oncall-app?page=version-history).
See [Grafana docs](https://grafana.com/docs/grafana/latest/administration/plugin-management/#update-a-plugin) for more info on updating Grafana plugins.
See [Grafana docs](https://grafana.com/docs/grafana/latest/administration/plugin-management/#update-a-plugin) for more
info on updating Grafana plugins.
## Join community
<a href="https://github.com/grafana/oncall/discussions/categories/community-calls"><img width="200px" src="docs/img/community_call.png"></a>
<a href="https://github.com/grafana/oncall/discussions"><img width="200px" src="docs/img/GH_discussions.png"></a>
<a href="https://slack.grafana.com/"><img width="200px" src="docs/img/slack.png"></a>
[<img width="200px" src="docs/img/community_call.png">](https://github.com/grafana/oncall/discussions/categories/community-calls)
[<img width="200px" src="docs/img/GH_discussions.png">](https://github.com/grafana/oncall/discussions)
[<img width="200px" src="docs/img/slack.png">](https://slack.grafana.com/)
## Stargazers over time

View file

@ -1,6 +1,9 @@
# Reporting security issues
If you think you have found a security vulnerability, please send a report to [security@grafana.com](mailto:security@grafana.com). This address can be used for all of Grafana Labs's open source and commercial products (including but not limited to Grafana, Grafana Cloud, Grafana Enterprise, and grafana.com). We can accept only vulnerability reports at this address.
If you think you have found a security vulnerability, please send a report to
[security@grafana.com](mailto:security@grafana.com). This address can be used for all of Grafana Labs's open source and
commercial products (including but not limited to Grafana, Grafana Cloud, Grafana Enterprise, and grafana.com).
We can accept only vulnerability reports at this address.
Please encrypt your message to us; please use our PGP key. The key fingerprint is:
@ -8,13 +11,18 @@ F988 7BEA 027A 049F AE8E 5CAA D125 8932 BE24 C5CA
The key is available from [keyserver.ubuntu.com](https://keyserver.ubuntu.com/pks/lookup?search=0xF9887BEA027A049FAE8E5CAAD1258932BE24C5CA&fingerprint=on&op=index).
Grafana Labs will send you a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
Grafana Labs will send you a response indicating the next steps in handling your report. After the initial reply
to your report, the security team will keep you informed of the progress towards a fix and full announcement,
and may ask for additional information or guidance.
**Important:** We ask you to not disclose the vulnerability before it have been fixed and announced, unless you received a response from the Grafana Labs security team that you can do so.
**Important:** We ask you to not disclose the vulnerability before it have been fixed and announced, unless you
received a response from the Grafana Labs security team that you can do so.
## Security announcements
We maintain a category on the community site called [Security Announcements](https://community.grafana.com/c/support/security-announcements),
where we will post a summary, remediation, and mitigation details for any patch containing security fixes.
We maintain a category on the community site called
[Security Announcements](https://community.grafana.com/c/support/security-announcements), where we will post a summary,
remediation, and mitigation details for any patch containing security fixes.
You can also subscribe to email updates to this category if you have a grafana.com account and sign on to the community site or track updates via an [RSS feed](https://community.grafana.com/c/support/security-announcements.rss).
You can also subscribe to email updates to this category if you have a grafana.com account and sign on to the
community site or track updates via an [RSS feed](https://community.grafana.com/c/support/security-announcements.rss).

View file

@ -11,37 +11,49 @@
- [Troubleshooting](#troubleshooting)
- [ld: library not found for -lssl](#ld-library-not-found-for--lssl)
- [Could not build wheels for cryptography which use PEP 517 and cannot be installed directly](#could-not-build-wheels-for-cryptography-which-use-pep-517-and-cannot-be-installed-directly)
- [django.db.utils.OperationalError: (1366, "Incorrect string value ...")](#djangodbutilsoperationalerror-1366-incorrect-string-value)
- [django.db.utils.OperationalError: (1366, "Incorrect string value")](#djangodbutilsoperationalerror-1366-incorrect-string-value)
- [/bin/sh: line 0: cd: grafana-plugin: No such file or directory](#binsh-line-0-cd-grafana-plugin-no-such-file-or-directory)
- [IDE Specific Instructions](#ide-specific-instructions)
- [PyCharm](#pycharm-professional-edition)
- [PyCharm](#pycharm)
Related: [How to develop integrations](/engine/config_integrations/README.md)
## Running the project
By default everything runs inside Docker. These options can be modified via the [`COMPOSE_PROFILES`](#compose_profiles) environment variable.
By default everything runs inside Docker. These options can be modified via the [`COMPOSE_PROFILES`](#compose_profiles)
environment variable.
1. Firstly, ensure that you have `docker` [installed](https://docs.docker.com/get-docker/) and running on your machine. **NOTE**: the `docker-compose-developer.yml` file uses some syntax/features that are only supported by Docker Compose v2. For instructions on how to enable this (if you haven't already done so), see [here](https://www.docker.com/blog/announcing-compose-v2-general-availability/). Ensure you have Docker Compose version 2.10 or above installed - update instructions are [here](https://docs.docker.com/compose/install/linux/).
2. Run `make init start`. By default this will run everything in Docker, using SQLite as the database and Redis as the message broker/cache. See [Running in Docker](#running-in-docker) below for more details on how to swap out/disable which components are run in Docker.
1. Firstly, ensure that you have `docker` [installed](https://docs.docker.com/get-docker/) and running on your machine.
**NOTE**: the `docker-compose-developer.yml` file uses some syntax/features that are only supported by Docker Compose
v2. For instructions on how to enable this (if you haven't already done so),
see [here](https://www.docker.com/blog/announcing-compose-v2-general-availability/). Ensure you have Docker Compose
version 2.10 or above installed - update instructions are [here](https://docs.docker.com/compose/install/linux/).
2. Run `make init start`. By default this will run everything in Docker, using SQLite as the database and Redis as the
message broker/cache. See [`COMPOSE_PROFILES`](#compose_profiles) below for more details on how to swap
out/disable which components are run in Docker.
3. Open Grafana in a browser [here](http://localhost:3000/plugins/grafana-oncall-app) (login: `oncall`, password: `oncall`).
4. You should now see the OnCall plugin configuration page. Fill out the configuration options as follows:
- OnCall backend URL: http://host.docker.internal:8080 (this is the URL that is running the OnCall API; it should be accessible from Grafana)
- Grafana URL: http://grafana:3000 (this is the URL OnCall will use to talk to the Grafana Instance)
- OnCall backend URL: <http://host.docker.internal:8080> (this is the URL that is running the OnCall API; it should be
accessible from Grafana)
- Grafana URL: <http://grafana:3000> (this is the URL OnCall will use to talk to the Grafana Instance)
5. Enjoy! Check our [OSS docs](https://grafana.com/docs/grafana-cloud/oncall/open-source/) if you want to set up Slack, Telegram, Twilio or SMS/calls through Grafana Cloud.
5. Enjoy! Check our [OSS docs](https://grafana.com/docs/grafana-cloud/oncall/open-source/) if you want to set up Slack,
Telegram, Twilio or SMS/calls through Grafana Cloud.
6. (Optional) Install `pre-commit` hooks by running `make install-precommit-hook`
**Note**: on subsequent startups you can simply run `make start`, this is a bit faster because it skips the frontend build step.
**Note**: on subsequent startups you can simply run `make start`, this is a bit faster because it skips the frontend
build step.
### `COMPOSE_PROFILES`
This configuration option represents a comma-separated list of [`docker-compose` profiles](https://docs.docker.com/compose/profiles/). It allows you to swap-out, or disable, certain components in Docker.
This configuration option represents a comma-separated list of [`docker-compose` profiles](https://docs.docker.com/compose/profiles/).
It allows you to swap-out, or disable, certain components in Docker.
This option can be configured in two ways:
1. Setting a `COMPOSE_PROFILES` environment variable in `dev/.env.dev`. This allows you to avoid having to set `COMPOSE_PROFILES` for each `make` command you execute afterwards.
1. Setting a `COMPOSE_PROFILES` environment variable in `dev/.env.dev`. This allows you to avoid having to set
`COMPOSE_PROFILES` for each `make` command you execute afterwards.
2. Passing in a `COMPOSE_PROFILES` argument when running `make` commands. For example:
```bash
@ -66,16 +78,29 @@ The default is `engine,oncall_ui,redis,grafana`. This runs:
### `GRAFANA_VERSION`
If you would like to change the version of Grafana being run, simply pass in a `GRAFANA_VERSION` environment variable to `make start` (or alternatively set it in your `.env.dev` file). The value of this environment variable should be a valid `grafana/grafana` published Docker [image tag](https://hub.docker.com/r/grafana/grafana/tags).
If you would like to change the version of Grafana being run, simply pass in a `GRAFANA_VERSION` environment variable
to `make start` (or alternatively set it in your `.env.dev` file). The value of this environment variable should be a
valid `grafana/grafana` published Docker [image tag](https://hub.docker.com/r/grafana/grafana/tags).
### Running backend services outside Docker
By default everything runs inside Docker. If you would like to run the backend services outside of Docker (for integrating w/ PyCharm for example), follow these instructions:
By default everything runs inside Docker. If you would like to run the backend services outside of Docker
(for integrating w/ PyCharm for example), follow these instructions:
1. Create a Python 3.9 virtual environment using a method of your choosing (ex. [venv](https://docs.python.org/3.9/library/venv.html) or [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv)). Make sure the virtualenv is "activated".
2. `postgres` is a dependency on some of our Python dependencies (notably `psycopg2` ([docs](https://www.psycopg.org/docs/install.html#prerequisites))). Please visit [here](https://www.postgresql.org/download/) for installation instructions.
1. Create a Python 3.9 virtual environment using a method of your choosing (ex.
[venv](https://docs.python.org/3.9/library/venv.html) or [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv)).
Make sure the virtualenv is "activated".
2. `postgres` is a dependency on some of our Python dependencies (notably `psycopg2`
([docs](https://www.psycopg.org/docs/install.html#prerequisites))). Please visit
[here](https://www.postgresql.org/download/) for installation instructions.
3. `make backend-bootstrap` - installs all backend dependencies
4. Modify your `.env.dev` by copying the contents of one of `.env.mysql.dev`, `.env.postgres.dev`, or `.env.sqlite.dev` into `.env.dev` (you should exclude the `GF_` prefixed environment variables). In most cases where you are running stateful services via `docker-compose` and backend services outside of docker you will simply need to change the database host to `localhost` (or in the case of `sqlite` update the file-path to your `sqlite` database file).
4. Modify your `.env.dev` by copying the contents of one of `.env.mysql.dev`, `.env.postgres.dev`,
or `.env.sqlite.dev` into `.env.dev` (you should exclude the `GF_` prefixed environment variables).
> In most cases where you are running stateful services via `docker-compose`, and backend services outside of
> docker, you will simply need to change the database host to `localhost` (or in the case of `sqlite` update
> the file-path to your `sqlite` database file). You will need to change the broker host to `localhost` as well.
5. `make backend-migrate` - runs necessary database migrations
6. Open two separate shells and then run the following:
@ -92,6 +117,9 @@ make start # start all of the docker containers
make stop # stop all of the docker containers
make restart # restart all docker containers
make build # rebuild images (e.g. when changing requirements.txt)
# run Django's `manage.py` script, inside of a docker container, passing `$CMD` as arguments.
# e.g. `make engine-manage CMD="makemigrations"` - https://docs.djangoproject.com/en/4.1/ref/django-admin/#django-admin-makemigrations
make engine-manage CMD="..."
# this will remove all of the images, containers, volumes, and networks
# associated with your local OnCall developer setup
@ -105,7 +133,7 @@ make exec-engine # exec into engine container's bash
make test # run backend tests
# run Django's `manage.py` script, passing `$CMD` as arguments.
# e.g. `make backend-manage-command makemigrations` - https://docs.djangoproject.com/en/4.1/ref/django-admin/#django-admin-makemigrations
# e.g. `make backend-manage-command CMD="makemigrations"` - https://docs.djangoproject.com/en/4.1/ref/django-admin/#django-admin-makemigrations
make backend-manage-command CMD="..."
# run both frontend and backend linters
@ -115,11 +143,13 @@ make lint
## Setting environment variables
If you need to override any additional environment variables, you should set these in a root `.env.dev` file. This file is automatically picked up by the OnCall engine Docker containers. This file is ignored from source control and also overrides any defaults that are set in other `.env*` files
If you need to override any additional environment variables, you should set these in a root `.env.dev` file.
This file is automatically picked up by the OnCall engine Docker containers. This file is ignored from source control
and also overrides any defaults that are set in other `.env*` files
## Slack application setup
For Slack app configuration check our docs: https://grafana.com/docs/grafana-cloud/oncall/open-source/#slack-setup
For Slack app configuration check our docs: <https://grafana.com/docs/grafana-cloud/oncall/open-source/#slack-setup>
## Update drone build
@ -144,7 +174,7 @@ drone sign --save grafana/oncall .drone.yml
**Problem:**
```
```bash
make backend-bootstrap
...
ld: library not found for -lssl
@ -155,7 +185,7 @@ make backend-bootstrap
**Solution:**
```
```bash
export LDFLAGS=-L/usr/local/opt/openssl/lib
make backend-bootstrap
```
@ -166,30 +196,38 @@ Happens on Apple Silicon
**Problem:**
```
build/temp.macosx-12-arm64-3.9/_openssl.c:575:10: fatal error: 'openssl/opensslv.h' file not found
#include <openssl/opensslv.h>
^~~~~~~~~~~~~~~~~~~~
1 error generated.
error: command '/usr/bin/clang' failed with exit code 1
----------------------------------------
ERROR: Failed building wheel for cryptography
```bash
build/temp.macosx-12-arm64-3.9/_openssl.c:575:10: fatal error: 'openssl/opensslv.h' file not found
#include <openssl/opensslv.h>
^~~~~~~~~~~~~~~~~~~~
1 error generated.
error: command '/usr/bin/clang' failed with exit code 1
----------------------------------------
ERROR: Failed building wheel for cryptography
```
**Solution:**
```
<!-- markdownlint-disable MD013 -->
```bash
LDFLAGS="-L$(brew --prefix openssl@1.1)/lib" CFLAGS="-I$(brew --prefix openssl@1.1)/include" pip install `cat engine/requirements.txt | grep cryptography`
```
### django.db.utils.OperationalError: (1366, "Incorrect string value ...")
<!-- markdownlint-enable MD013 -->
### django.db.utils.OperationalError: (1366, "Incorrect string value")
**Problem:**
```
<!-- markdownlint-disable MD013 -->
```bash
django.db.utils.OperationalError: (1366, "Incorrect string value: '\\xF0\\x9F\\x98\\x8A\\xF0\\x9F...' for column 'cached_name' at row 1")
```
<!-- markdownlint-enable MD013 -->
**Solution:**
Recreate the database with the correct encoding.
@ -200,18 +238,19 @@ Recreate the database with the correct encoding.
When running `make init`:
```
```bash
/bin/sh: line 0: cd: grafana-plugin: No such file or directory
make: *** [init] Error 1
```
This arises when the environment variable `[CDPATH](https://www.theunixschool.com/2012/04/what-is-cdpath.html)` is set _and_ when the current path (`.`) is not explicitly part of `CDPATH`.
This arises when the environment variable `[CDPATH](https://www.theunixschool.com/2012/04/what-is-cdpath.html)` is
set _and_ when the current path (`.`) is not explicitly part of `CDPATH`.
**Solution:**
Either make `.` part of `CDPATH` in your .rc file setup, or temporarily override the variable when running `make` commands:
```
```bash
$ CDPATH="." make init
# Setting CDPATH to empty seems to also work - only tested on zsh, YMMV
$ CDPATH="" make init
@ -221,15 +260,19 @@ $ CDPATH="" make init
When running `make init start`:
```
<!-- markdownlint-disable MD013 -->
```bash
Error response from daemon: open /var/lib/docker/overlay2/ac57b871108ee1b98ff4455e36d2175eae90cbc7d4c9a54608c0b45cfb7c6da5/committed: is a directory
make: *** [start] Error 1
```
**Solution:**
clear everything in docker by resetting or:
<!-- markdownlint-enable MD013 -->
```
**Solution:**
clear everything in docker by resetting or:
```bash
make cleanup
```
@ -240,7 +283,8 @@ make cleanup
1. Follow the instructions listed in ["Running backend services outside Docker"](#running-backend-services-outside-docker).
2. Open the project in PyCharm
3. Settings &rarr; Project OnCall
- In Python Interpreter click the gear and create a new Virtualenv from existing environment selecting the venv created in Step 1.
- In Python Interpreter click the gear and create a new Virtualenv from existing environment selecting the
venv created in Step 1.
- In Project Structure make sure the project root is the content root and add /engine to Sources
4. Under Settings &rarr; Languages & Frameworks &rarr; Django
- Enable Django support

View file

@ -13,6 +13,9 @@ x-oncall-volumes: &oncall-volumes
# https://stackoverflow.com/a/60456034
- ${ENTERPRISE_ENGINE:-/dev/null}:/etc/app/extensions/engine_enterprise
- ${SQLITE_DB_FILE:-/dev/null}:/var/lib/oncall/oncall.db
# this is mounted for testing purposes. Some of the authorization tests
# reference this file
- ./grafana-plugin/src/plugin.json:/etc/grafana-plugin/src/plugin.json
x-env-files: &oncall-env-files
- ./dev/.env.dev
@ -178,7 +181,7 @@ services:
container_name: mysql
labels: *oncall-labels
image: mysql:5.7
platform: linux/x86_64
platform: linux/amd64
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
restart: always
environment:
@ -205,7 +208,7 @@ services:
container_name: mysql_to_create_grafana_db
labels: *oncall-labels
image: mysql:5.7
platform: linux/x86_64
platform: linux/amd64
command: bash -c "mysql -h mysql -uroot -pempty -e 'CREATE DATABASE IF NOT EXISTS grafana CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'"
depends_on:
mysql:

5
docs/.markdownlint.json Normal file
View file

@ -0,0 +1,5 @@
{
"extends": "../.markdownlint.json",
"MD025": false,
"MD036": false
}

View file

@ -1,8 +1,10 @@
# Grafana Cloud Documentation
Source for documentation at https://grafana.com/docs/oncall/
Source for documentation at <https://grafana.com/docs/oncall/>
## Preview the website
Run `make docs`. This launches a preview of the website with the current grafana docs at `http://localhost:3002/docs/oncall/latest/` which will refresh automatically when changes are made to content in the `sources` directory.
Run `make docs`. This launches a preview of the website with the current grafana docs at
`http://localhost:3002/docs/oncall/latest/` which will refresh automatically when changes are made to
content in the `sources` directory.
Make sure Docker is running.

View file

@ -18,15 +18,22 @@ weight: 1000
<img src="/static/img/docs/oncall/oncall-logo.png" class="no-shadow" width="700px">
Grafana OnCall is an open source incident response management tool built to help teams improve their collaboration and
resolve incidents faster. Some of the core strengths of Grafana OnCall include:
Grafana OnCall is an open source incident response management tool built to help teams improve their collaboration and resolve incidents faster. Some of the core strengths of Grafana OnCall include:
- **Support for a broad set of monitoring systems:** Grafana OnCall supports integrations with many monitoring systems, including Grafana, Prometheus, Alertmanager, Zabbix, and more.
- **Reduce alert noise:** Automatic alert grouping helps avoid alert storms and reduce noise during incidents. Auto-resolve settings can resolve without human intervention when the resolve conditions are met, enabling you to control alert noise and reduce alert fatigue.
- **Automatic escalation to on-call rotations:** Grafana OnCalls flexible calendar integration allows you to define your on-call rotations while managing on-call schedules in your preferred calendar application with iCal format. Configurable alert escalation automatically escalates alerts to on-call team members, notifies slack channels, and more.
- **ChatOps focused:** Grafana OnCall integrates closely with your slack workspace to deliver alert notifications to individuals and groups, making daily alerts more visible and easier to manage.
- **Highly customizable**: With customizable alert grouping and routing, you can decide which alerts you want to be notified of and how, ensuring the right people are notified for the right issues.
- **Massive scalability:** Grafana OnCall is equipped with a full API and Terraform capabilities. Ready for GitOps and large organization configuration.
- **Support for a broad set of monitoring systems:** Grafana OnCall supports integrations with many monitoring systems,
including Grafana, Prometheus, Alertmanager, Zabbix, and more.
- **Reduce alert noise:** Automatic alert grouping helps avoid alert storms and reduce noise during incidents.
Auto-resolve settings can resolve without human intervention when the resolve conditions are met, enabling you to
control alert noise and reduce alert fatigue.
- **Automatic escalation to on-call rotations:** Grafana OnCalls flexible calendar integration allows you to define
your on-call rotations while managing on-call schedules in your preferred calendar application with iCal format.
Configurable alert escalation automatically escalates alerts to on-call team members, notifies slack channels, and more.
- **ChatOps focused:** Grafana OnCall integrates closely with your slack workspace to deliver alert notifications to
individuals and groups, making daily alerts more visible and easier to manage.
- **Highly customizable**: With customizable alert grouping and routing, you can decide which alerts you want to be
notified of and how, ensuring the right people are notified for the right issues.
- **Massive scalability:** Grafana OnCall is equipped with a full API and Terraform capabilities. Ready for GitOps
and large organization configuration.
{{< section >}}

View file

@ -8,16 +8,17 @@ weight: 900
# Configure alert behavior for Grafana OnCall
The available alert configurations in Grafana OnCall allow you to define how certain alerts are handled and ensure that alerts are routed, escalated, and grouped to fit your specific alerting needs. Grafana OnCall can receive alerts from any monitoring system that sends alerts via webhook.
The available alert configurations in Grafana OnCall allow you to define how certain alerts are handled and ensure that
alerts are routed, escalated, and grouped to fit your specific alerting needs. Grafana OnCall can receive alerts from
any monitoring system that sends alerts via webhook.
## About alert behavior
## About alert behavior
Once Grafana OnCall receives an alert, the following occurs, based on the alert content:
- Default or customized alert templates are applied to deliver the most useful alert fields with the most valuable information, in a readable format.
- Default or customized alert templates are applied to deliver the most useful alert fields with the most valuable information,
in a readable format.
- Alerts are grouped based on your alert grouping configurations, combining similar or related alerts to reduce alert noise.
- Alerts automatically resolve if an alert from the monitoring system matches the resolve condition for that alert.
{{< section >}}
{{< section >}}

View file

@ -15,7 +15,10 @@ weight: 300
# Configure alert templates
Grafana OnCall can integrate with any monitoring systems that can send alerts using webhooks with JSON payloads. By default, webhooks deliver raw JSON payloads. When Grafana OnCall receives an alert and parses its payload, a default pre configured alert template is applied to modify the alert payload to be more human readable. These alert templates are customizable for any integration.
Grafana OnCall can integrate with any monitoring systems that can send alerts using webhooks with JSON payloads. By
default, webhooks deliver raw JSON payloads. When Grafana OnCall receives an alert and parses its payload, a default
pre configured alert template is applied to modify the alert payload to be more human readable. These alert templates
are customizable for any integration.
See Format alerts with alert templates in this document to learn more about how to customize alert templates.
@ -23,13 +26,15 @@ See Format alerts with alert templates in this document to learn more about how
Once Grafana OnCall receives an alert, the following occurs, based on the alert content:
- Default or customized alert templates are applied to deliver the most useful alert fields with the most valuable information, in a readable format.
- Default or customized alert templates are applied to deliver the most useful alert fields with the most valuable information,
in a readable format.
- Alerts are grouped based on your alert grouping configurations, combining similar or related alerts to reduce alert noise.
- Alerts automatically resolve if an alert from the monitoring system matches the resolve condition for that alert.
## Alert payload
Alerts received by Grafana OnCall contain metadata as keys and values in a JSON object. The following is an example of an alert from Grafana OnCall:
Alerts received by Grafana OnCall contain metadata as keys and values in a JSON object. The following is an example of
an alert from Grafana OnCall:
```json
{
@ -68,16 +73,21 @@ The JSON payload is converted. For example:
- `{{ payload.message }}` -> Message
- `{{ payload.imageUrl }}` -> Image Url
The result is that each field of the alert in OnCall is now mapped to the JSON payload keys. This also true for the alert behavior:
The result is that each field of the alert in OnCall is now mapped to the JSON payload keys. This also true for the
alert behavior:
- `{{ payload.ruleId }}` -> Grouping Id
- `{{ 1 if payload.state == 'OK' else 0 }}` -> Resolve Signal
Grafana OnCall provides a pre configured default Jinja template for supported integrations. If your monitoring system is not in the Grafana OnCall integrations list, you can create a generic `webhook` integration, send an alert, and configure your templates.
Grafana OnCall provides a pre configured default Jinja template for supported integrations. If your monitoring system is
not in the Grafana OnCall integrations list, you can create a generic `webhook` integration, send an alert, and configure
your templates.
## Customize alerts with alert templates
Alert templates allow you to format any alert fields recognized by Grafana OnCall. You can customize default alert templates for all the different ways you receive your alerts such as web, slack, SMS, and email. For more advanced customization, use Jinja templates.
Alert templates allow you to format any alert fields recognized by Grafana OnCall. You can customize default alert
templates for all the different ways you receive your alerts such as web, slack, SMS, and email. For more advanced
customization, use Jinja templates.
As a best practice, add _Playbooks_, _Useful links_, or _Checklists_ to the alert message.
@ -97,23 +107,28 @@ To customize alert templates in Grafana OnCall:
4. Edit the alert behavior as needed:
- `Grouping Id` - This output groups other alerts into a single alert group.
- `Acknowledge Condition` - The output should be `ok`, `true`, or `1` to auto-acknowledge the alert group. For example, `{{ 1 if payload.state == 'OK' else 0 }}`.
- `Resolve Condition` - The output should be `ok`, `true` or `1` to auto-resolve the alert group. For example, `{{ 1 if payload.state == 'OK' else 0 }}`.
- `Acknowledge Condition` - The output should be `ok`, `true`, or `1` to auto-acknowledge the alert group.
For example, `{{ 1 if payload.state == 'OK' else 0 }}`.
- `Resolve Condition` - The output should be `ok`, `true` or `1` to auto-resolve the alert group.
For example, `{{ 1 if payload.state == 'OK' else 0 }}`.
- `Source Link` - Used to customize the URL link to provide as the "source" of the alert.
## Advanced Jinja templates
Grafana OnCall uses [Jinja templating language](http://jinja.pocoo.org/docs/2.10/) to format alert groups for the Web, Slack, phone calls, SMS messages, and more because the JSON format is not easily readable by humans. As a result, you can decide what you want to see when an alert group is triggered as well as how it should be presented.
Grafana OnCall uses [Jinja templating language](http://jinja.pocoo.org/docs/2.10/) to format alert groups for the Web,
Slack, phone calls, SMS messages, and more because the JSON format is not easily readable by humans. As a result, you
can decide what you want to see when an alert group is triggered as well as how it should be presented.
Jinja2 offers simple but multi-faceted functionality by using loops, conditions, functions, and more.
> **NOTE:** Every alert from a monitoring system comes in the key/value format.
Grafana OnCall has rules about which of the keys match to: `__title`, `message`, `image`, `grouping`, and `auto-resolve__`.
Grafana OnCall has rules about which of the keys match to: `__title`, `message`, `image`, `grouping`, and `auto-resolve__`.
### Loops
Monitoring systems can send an array of values. In this example, you can use Jinja to iterate and format the alert using a Grafana example:
Monitoring systems can send an array of values. In this example, you can use Jinja to iterate and format the alert
using a Grafana example:
```.jinja2
*Values:*

View file

@ -16,26 +16,25 @@ weight: 500
# Configure outgoing webhooks for Grafana OnCall
Outgoing webhooks allow you to send alert details to a specified URL from Grafana OnCall. Once an outgoing webhook is configured, you can use it as a notification method in escalation chains.
Outgoing webhooks allow you to send alert details to a specified URL from Grafana OnCall. Once an outgoing webhook is
configured, you can use it as a notification method in escalation chains.
To automatically send alert data to a destination URL via outgoing webhook:
1. In Grafana OnCall, navigate to **Outgoing Webhooks** and click **+ Create**.
This is also the place to edit and delete existing outgoing webhooks.
2. Provide a name for your outgoing webhook and enter the destination URL.
3. If the destination requires authentication, enter your credentials.
You can enter a username and password (HTTP) or an authorization header formatted in JSON.
4. Configure the webhook payload in the **Data** field.
5. Click **Create Webhook**.
The format you use to call the variables must match the structure of how the fields are nested in the alert payload. The **Data** field can use the following four variables to auto-populate the webhook payload with information about the first alert in the alert group:
The format you use to call the variables must match the structure of how the fields are nested in the alert payload.
The **Data** field can use the following four variables to auto-populate the webhook payload with information about
the first alert in the alert group:
- `{{ alert_payload }}`
- `{{ alert_group_id }}`
<br>
`alert_payload` is always the first level of any variable you want to call.
@ -48,4 +47,5 @@ The following is an example of an entry in the **Data** field that might return
}
```
> **NOTE:** If you receive an error message and cannot create an outgoing webhook, verify that your JSON is formatted correctly.
> **NOTE:** If you receive an error message and cannot create an outgoing webhook, verify that your JSON is
> formatted correctly.

View file

@ -14,49 +14,55 @@ weight: 1100
# Configure and manage on-call schedules
Grafana OnCall allows you to use any calendar service that uses the iCal format to create customized on-call schedules for team members. Using Grafana OnCall, you can create a primary calendar that acts as a read-only schedule, and an override calendar that allows all team members to modify schedules as they change.
Grafana OnCall allows you to use any calendar service that uses the iCal format to create customized on-call schedules
for team members. Using Grafana OnCall, you can create a primary calendar that acts as a read-only schedule, and an
override calendar that allows all team members to modify schedules as they change.
To learn more about creating on-call calendars, see the following topics:
# Configure and manage on-call schedules
## Configure and manage on-call schedules
You can use any calendar with an iCal address to schedule on-call times for users. During these times, notifications configured in escalation chains with the **Notify users from an on-call schedule** setting will be sent to the the person scheduled. You can also schedule multiple users for overlapping times, and assign prioritization labels for the user that you would like to notify.
You can use any calendar with an iCal address to schedule on-call times for users. During these times, notifications
configured in escalation chains with the **Notify users from an on-call schedule** setting will be sent to the the person
scheduled. You can also schedule multiple users for overlapping times, and assign prioritization labels for the user
that you would like to notify.
When you create a schedule, you will be able to select a Slack channel, associated with your OnCall account, that will notify users when there are errors or notifications regarding the assigned on-call shifts.
When you create a schedule, you will be able to select a Slack channel, associated with your OnCall account, that will
notify users when there are errors or notifications regarding the assigned on-call shifts.
## Create an on-call schedule calendar
Create a primary calendar and an optional override calendar to schedule on-call shifts for team members.
1. In the **Scheduling** section of Grafana OnCall, click **+ Create schedule**.
1. Give the schedule a name.
1. Create a new calendar in your calendar service and locate the secret iCal URL. For example, in a Google calendar, this URL can be found in **Settings > Settings for my calendars > Integrate calendar**.
1. Create a new calendar in your calendar service and locate the secret iCal URL. For example, in a Google calendar,
this URL can be found in **Settings > Settings for my calendars > Integrate calendar**.
1. Copy the secret iCal URL. In OnCall, paste it into the **Primary schedule for iCal URL** field.
The permissions you set when you create the calendar determine who can modify the calendar.
1. Click **Create Schedule**.
1. Schedule on-call times for team members.
Use the Grafana username of team members as the event name to schedule their on-call times. You can take advantage of all of the features of your calendar service.
Use the Grafana username of team members as the event name to schedule their on-call times. You can take advantage
of all of the features of your calendar service.
1. Create overlapping schedules (optional).
When you create schedules that overlap, you can prioritize a schedule by adding a level marker. For example, if users AliceGrafana and BobGrafana have overlapping schedules, but BobGrafana is the primary contact, you would name his event `[L1] BobGrafana`, AliceGrafana maintains the default `[L0]` status, and would not receive notifications during the overlapping time. You can prioritize up to and including a level 9 prioritization, or `[L9]`.
When you create schedules that overlap, you can prioritize a schedule by adding a level marker. For example, if users
AliceGrafana and BobGrafana have overlapping schedules, but BobGrafana is the primary contact, you would name his
event `[L1] BobGrafana`, AliceGrafana maintains the default `[L0]` status, and would not receive notifications during
the overlapping time. You can prioritize up to and including a level 9 prioritization, or `[L9]`.
# Create an override calendar (optional)
You can use an override calendar to allow team members to schedule on-call duties that will override the primary schedule. An override option allows flexibility without modifying the primary schedule. Events scheduled on the override calendar will always override overlapping events on the primary calendar.
You can use an override calendar to allow team members to schedule on-call duties that will override the primary schedule.
An override option allows flexibility without modifying the primary schedule. Events scheduled on the override calendar
will always override overlapping events on the primary calendar.
1. Create a new calendar using the same calendar service you used to create the primary calendar.
Be sure to set permissions that allow team members to edit the calendar.
1. In the scheduling section of Grafana OnCall, select the primary calendar you want to override.
1. Click **Edit**.
1. Enter the secret iCal URL in the **Overrides schedule iCal URL** field and click **Update**.

View file

@ -16,15 +16,20 @@ weight: 1300
# Manage users and teams for Grafana OnCall
Grafana OnCall is configured based on the teams you've created on the organization level of your Grafana instance, in **Configuration > Teams**. Administrators can create a different configuration for each team, and can navigate between team configurations in the **Select Team** dropdown menu in the **Incidents** section of Grafana OnCall.
Grafana OnCall is configured based on the teams you've created on the organization level of your Grafana instance,
in **Configuration > Teams**. Administrators can create a different configuration for each team, and can navigate
between team configurations in the **Select Team** dropdown menu in the **Incidents** section of Grafana OnCall.
Users can edit their contact information, but user permissions are assigned at the Cloud portal level.
## Configure user notification policies
Administrators can configure how each user will receive notifications when they are are scheduled to receive them in escalation chains. Users can verify phone numbers and email addresses.
Administrators can configure how each user will receive notifications when they are are scheduled to receive them in
escalation chains. Users can verify phone numbers and email addresses. Only users with the **Admin** or **Editor** role
are allowed to get notifications.
> **NOTE**: You cannot add users or manage permissions in Grafana OnCall. Most user settings are found on the organizational level of your Grafana instance in **Configuration > Users**.
> **NOTE**: You cannot add users or manage permissions in Grafana OnCall. Most user settings are found on the
> organizational level of your Grafana instance in **Configuration > Users**.
1. Find users.
@ -32,17 +37,21 @@ Administrators can configure how each user will receive notifications when they
1. Configure user settings.
Add and verify a phone number, a Slack username, and a Telegram account if you want to receive notifications using these mediums.
Add and verify a phone number, a Slack username, and a Telegram account if you want to receive notifications
using these mediums.
> **NOTE:** To edit a user's profile username, email, or role, you must do so in the **Users** tab in the **Configuration** menu of your Grafana instance.
> **NOTE:** To edit a user's profile username, email, or role, you must do so in the **Users** tab in
> the **Configuration** menu of your Grafana instance.
1. Configure notification settings.
Specify the notification medium and frequency for each user. Notification steps will be followed in the order they are listed.
Specify the notification medium and frequency for each user. Notification steps will be followed in the order
they are listed.
The settings you specify in **Default Notifications** dictate how a user is notified for most escalation thresholds.
**Important Notifications** are labeled in escalation chains. If an escalation event is marked as an important notification, it will bypass **Default Notification** settings and notify the user by the method specified.
**Important Notifications** are labeled in escalation chains. If an escalation event is marked as an important notification,
it will bypass **Default Notification** settings and notify the user by the method specified.
## Configure Telegram user settings in OnCall
@ -50,7 +59,8 @@ Administrators can configure how each user will receive notifications when they
1. Click **Connect automatically** for the bot to message you and to bring up your telegram account.
1. Click **Start** when the OnCall bot messages you.
If you want to connect manually, you can click the URL provided and then **SEND MESSAGE**. In your Telegram account, click **Start**.
If you want to connect manually, you can click the URL provided and then **SEND MESSAGE**. In your Telegram account,
click **Start**.
## Configure Slack user settings in OnCall

View file

@ -10,7 +10,8 @@ weight: 700
Escalation chains and routes for Grafana OnCall
Administrators can create escalation policies to automatically send alert group notifications to recipients. These policies define how, where, and when to send notifications.
Administrators can create escalation policies to automatically send alert group notifications to recipients.
These policies define how, where, and when to send notifications.
See the following topics for more information:

View file

@ -16,7 +16,9 @@ weight: 100
# Configure and manage Escalation Chains
Escalation policies dictate how users and groups are notified when an alert notification is created. They can be very simple, or very complex. You can define as many escalation configurations for an integration as you need, and you can send notifications for certain alerts to a designated place when certain conditions are met, or not met.
Escalation policies dictate how users and groups are notified when an alert notification is created. They can be very
simple, or very complex. You can define as many escalation configurations for an integration as you need, and you can
send notifications for certain alerts to a designated place when certain conditions are met, or not met.
Escalation policies have three main parts:
@ -26,8 +28,12 @@ Escalation policies have three main parts:
## Escalation chains
An escalation chain can have many steps, or only one step. For example, steps can be configured to notify multiple users in some order, notify users that are scheduled for on-call shifts, ping groups in Slack, use outgoing webhooks to integrate with other services, such as JIRA, and do a number of other automated notification tasks.
An escalation chain can have many steps, or only one step. For example, steps can be configured to notify multiple users
in some order, notify users that are scheduled for on-call shifts, ping groups in Slack, use outgoing webhooks to
integrate with other services, such as JIRA, and do a number of other automated notification tasks.
## Routes
An escalation workflow can employ **routes** that administrators can configure to filter alerts by regular expressions in their payloads. Notifications for these alerts can be sent to individuals, or they can make use of a new or existing escalation chain.
An escalation workflow can employ **routes** that administrators can configure to filter alerts by regular expressions
in their payloads. Notifications for these alerts can be sent to individuals, or they can make use of a new
or existing escalation chain.

View file

@ -20,35 +20,49 @@ Set up escalation chains and routes to configure escalation behavior for alert g
## Configure escalation chains
You can create and edit escalation chains in two places: within **Integrations**, by clicking on an integration tile, and in **Escalation Chains**. The following steps are for the **Integrations** workflow, but are generally applicable in both situations.
You can create and edit escalation chains in two places: within **Integrations**, by clicking on an integration tile,
and in **Escalation Chains**. The following steps are for the **Integrations** workflow, but are generally applicable
in both situations.
You can use **escalation chains** and **routes** to determine ordered escalation procedures. Escalation chains allow you to set up a series of alert group notification actions that trigger if certain conditions that you specify are met or not met.
You can use **escalation chains** and **routes** to determine ordered escalation procedures. Escalation chains allow
you to set up a series of alert group notification actions that trigger if certain conditions that you specify are
met or not met.
1. Click on the integration tile for which you want to define escalation policies.
The **Escalations** section for the notification is in the pane to the right of the list of notifications.
You can click **Change alert template and grouping** to customize the look of the alert. You can also do this by clicking the **Settings** (gear) icon in the integration tile.
You can click **Change alert template and grouping** to customize the look of the alert. You can also do this by
clicking the **Settings** (gear) icon in the integration tile.
1. Create an escalation chain.
In the escalation pane, click **Escalate to** to choose from previously added escalation chains, or create a new one by clicking **Make a copy** or **Create a new chain**. This will be the name of the escalation policy you define.
In the escalation pane, click **Escalate to** to choose from previously added escalation chains, or create a new one
by clicking **Make a copy** or **Create a new chain**. This will be the name of the escalation policy you define.
1. Add escalation steps.
Click **Add escalation step** to choose from a set of actions and specify their triggering conditions. By default, the first step is to notify a slack channel or user. Specify users or channels or toggle the switch to turn this step off.
Click **Add escalation step** to choose from a set of actions and specify their triggering conditions. By default, the
first step is to notify a slack channel or user. Specify users or channels or toggle the switch to turn this step off.
To mark an escalation as **Important**, select the option from the step **Start** dropdown menu. User notification policies can be separately defined for **Important** and **Default** escalations.
To mark an escalation as **Important**, select the option from the step **Start** dropdown menu. User notification
policies can be separately defined for **Important** and **Default** escalations.
## Create a route
To add a route, click **Add Route**.
You can set up a single route and specify notification escalation steps, or you can add multiple routes, each with its own configuration.
You can set up a single route and specify notification escalation steps, or you can add multiple routes, each with
its own configuration.
Each route added to an escalation policy follows an `IF`, `ELSE IF`, or `ELSE` path and depends on the type of alert you specify using a regular expression that matches content in the payload body of the alert. You can also specify where to send the notification for each route.
Each route added to an escalation policy follows an `IF`, `ELSE IF`, or `ELSE` path and depends on the type of alert you
specify using a regular expression that matches content in the payload body of the alert. You can also specify where
to send the notification for each route.
For example, you can send notifications for alerts with `\"severity\": \"critical\"` in the payload to an escalation chain called `Bob_OnCall`. You can create a different route for alerts with the payload `\"namespace\" *: *\"synthetic-monitoring-dev-.*\"` and select a escalation chain called `NotifySecurity`.
For example, you can send notifications for alerts with `\"severity\": \"critical\"` in the payload to an escalation
chain called `Bob_OnCall`. You can create a different route for alerts with the payload
`\"namespace\" *: *\"synthetic-monitoring-dev-.*\"` and select a escalation chain called `NotifySecurity`.
You can set up escalation steps for each route in a chain.
> **NOTE:** When you modify an escalation chain or a route, it will modify that escalation chain across all integrations that use it.
> **NOTE:** When you modify an escalation chain or a route, it will modify that escalation chain across
> all integrations that use it.

View file

@ -13,74 +13,92 @@ weight: 300
# Get started with Grafana OnCall
Grafana OnCall is an incident response tool built to help DevOps and SRE teams improve their collaboration and resolve incidents faster.
Grafana OnCall is an incident response tool built to help DevOps and SRE teams improve their collaboration and resolve
incidents faster.
With a centralized view of all your alerts, automated alert escalation and grouping, and on-call scheduling, Grafana OnCall helps ensure that alert notifications reach the right people, at the right time using the right notification method.
With a centralized view of all your alerts, automated alert escalation and grouping, and on-call scheduling, Grafana
OnCall helps ensure that alert notifications reach the right people, at the right time using the right notification method.
The following diagram details an example alert workflow with Grafana OnCall:
<img src="/static/img/docs/oncall/oncall-alertworkflow.png" class="no-shadow" width="700px">
These procedures introduce you to initial Grafana OnCall configuration steps, including monitoring system integration, how to set up escalation chains, and how to use your calendar service for on-call scheduling.
These procedures introduce you to initial Grafana OnCall configuration steps, including monitoring system integration,
how to set up escalation chains, and how to use your calendar service for on-call scheduling.
## Before you begin
Grafana OnCall is available for Grafana Cloud as well as Grafana open source users. You must have a Grafana Cloud account or use [Open Source Grafana OnCall]({{< relref "../open-source" >}})
Grafana OnCall is available for Grafana Cloud as well as Grafana open source users. You must have a Grafana Cloud account
or use [Open Source Grafana OnCall]({{< relref "../open-source" >}})
## Install Open Source Grafana OnCall
For Open Source Grafana OnCall installation guidance, refer to [Open Source Grafana OnCall]({{< relref "../open-source" >}})
>**Note:** If you are using Grafana OnCall with your Grafana Cloud instance there are no install steps. Access Grafana OnCall from your Grafana Cloud account and skip ahead to “Get alerts into Grafana OnCall”
For Open Source Grafana OnCall installation guidance, refer to
[Open Source Grafana OnCall]({{< relref "../open-source" >}})
> **Note:** If you are using Grafana OnCall with your Grafana Cloud instance there are no install steps. Access Grafana
> OnCall from your Grafana Cloud account and skip ahead to “Get alerts into Grafana OnCall”
## Get alerts into Grafana OnCall
Once youve installed Grafana OnCall or accessed it from your Grafana Cloud instance, you can begin integrating with monitoring systems, configuring escalation chains, and get alerts into Grafana OnCall.
Once youve installed Grafana OnCall or accessed it from your Grafana Cloud instance, you can begin integrating with
monitoring systems, configuring escalation chains, and get alerts into Grafana OnCall.
### Integrate with a monitoring system
Regardless of where your alerts originate, you can send them to Grafana OnCall via available integrations or customizable webhooks. To start receiving alerts in Grafana OnCall, use the following steps to configure your first integration and send a demo alert.
Regardless of where your alerts originate, you can send them to Grafana OnCall via available integrations or customizable
webhooks. To start receiving alerts in Grafana OnCall, use the following steps to configure your first integration and
send a demo alert.
#### Configure your first integration
#### Configure your first integration:
1. In Grafana OnCall, navigate to the **Integrations** tab and click **+ New integration for receiving alerts**.
2. Select an integration from the provided options, if the integration youre looking for isnt listed, then select Webhook.
3. Follow the configuration steps on the integration settings page.
4. Complete any necessary configurations in your monitoring system to send alerts to Grafana OnCall.
#### Send a demo alert
1. In the integration tab, click **Send demo alert** then navigate to the **Alert Groups** tab to see your test alert firing.
2. Explore the alert by clicking on the title of the alert.
3. Acknowledge and resolve the test alert.
For more information on Grafana OnCall integrations and further configuration guidance, refer to, [Grafana OnCall integrations]({{< relref "../integrations" >}})
For more information on Grafana OnCall integrations and further configuration guidance, refer to
[Grafana OnCall integrations]({{< relref "../integrations" >}})
### Configure Escalation Chains
Escalation Chains are customizable automated alert routing steps that enable you to specify who is notified for a certain alert. In addition to escalation chains, you can configure Routes to send alerts to different escalation chains depending on the alert details.
Escalation Chains are customizable automated alert routing steps that enable you to specify who is notified for a certain
alert. In addition to escalation chains, you can configure Routes to send alerts to different escalation chains depending
on the alert details.
Once your integration is configured, you can set up an escalation chain to determine how alerts from your integration are handled. Multi-step escalation chains help ensure thorough alert escalation to prevent alerts from being missed.
Once your integration is configured, you can set up an escalation chain to determine how alerts from your integration
are handled. Multi-step escalation chains help ensure thorough alert escalation to prevent alerts from being missed.
To configure Escalation Chains:
1. Navigate to the **Escalation Chains** tab and click **+ New Escalation Chain**
2. Give your Escalation Chain a useful name and click **Create**
3. Add a series of escalation steps from the available dropdown options.
4. To link your Escalation Chain to your integration, navigate back to the **Integrations tab**, Select your newly created Escalation Chain from the “**Escalate to**” dropdown.
4. To link your Escalation Chain to your integration, navigate back to the **Integrations tab**, Select your newly
created Escalation Chain from the “**Escalate to**” dropdown.
Alerts from this integration will now follow the escalation steps configured in your Escalation Chain.
For more information on Escalation Chains and more ways to customize them, refer to [Configure and manage Escalation Chains]({{< relref "../escalation-policies/configure-escalation-chains" >}})
For more information on Escalation Chains and more ways to customize them, refer to
[Configure and manage Escalation Chains]({{< relref "../escalation-policies/configure-escalation-chains" >}})
## Get notified of an alert
In order for Grafana OnCall to notify you of an alert, you must configure how you want to be notified. Personal notification policies, chatops integrations, and on-call schedules allow you to automate how users are notified of alerts.
In order for Grafana OnCall to notify you of an alert, you must configure how you want to be notified. Personal notification
policies, chatops integrations, and on-call schedules allow you to automate how users are notified of alerts.
### Configure personal notification policies
Personal notification policies determine how a user is notified for a certain type of alert. Get notified by SMS, phone call, or Slack mentions. Administrators can configure how users receive notification for certain types of alerts. For more information on personal notification policies, refer to [Manage users and teams for Grafana OnCall]({{< relref "../configure-user-settings" >}})
Personal notification policies determine how a user is notified for a certain type of alert. Get notified by SMS,
phone call, or Slack mentions. Administrators can configure how users receive notification for certain types of alerts.
For more information on personal notification policies, refer to
[Manage users and teams for Grafana OnCall]({{< relref "../configure-user-settings" >}})
To configure users personal notification policies:
@ -88,10 +106,11 @@ To configure users personal notification policies:
2. Select a user from the user list and click **Edit**
3. Configure **Default Notifications** and **Important Notification**
### Configure Slack for Grafana OnCall
Grafana OnCall integrates closely with your Slack workspace to deliver alert notifications to individuals, user groups, and channels. Slack notifications can be triggered by steps in an escalation chain or as a step in users personal notification policies.
Grafana OnCall integrates closely with your Slack workspace to deliver alert notifications to individuals, user groups,
and channels. Slack notifications can be triggered by steps in an escalation chain or as a step in users personal
notification policies.
To configure Slack for Grafana OnCall:
@ -102,12 +121,13 @@ To configure Slack for Grafana OnCall:
5. Click Allow to allow Grafana OnCall to access Slack.
6. Ensure users verify their Slack accounts in their user profile in Grafana OnCall.
For further instruction on connecting to your Slack workspace, refer to [Slack integration for Grafana OnCall]({{< relref "../integrations/chatops-integrations/configure-slack/" >}})
For further instruction on connecting to your Slack workspace, refer to
[Slack integration for Grafana OnCall]({{< relref "../integrations/chatops-integrations/configure-slack/" >}})
### Add your on-call schedule
Grafana OnCall allows you to manage your on-call schedule in your preferred calendar app such as Google Calendar or Microsoft Outlook.
Grafana OnCall allows you to manage your on-call schedule in your preferred calendar app such as Google Calendar or
Microsoft Outlook.
To integrate your on-call calendar with Grafana OnCall:
@ -116,6 +136,5 @@ To integrate your on-call calendar with Grafana OnCall:
3. Copy the iCal URL associated with your on-call calendar from your calendar integration settings.
4. Configure the rest of the schedule settings and click Create Schedule
For more information on on-call schedules, refer to [Configure and manage on-call schedules]({{< relref "../calendar-schedules" >}})
For more information on on-call schedules, refer to
[Configure and manage on-call schedules]({{< relref "../calendar-schedules" >}})

View file

@ -16,13 +16,18 @@ weight: 500
# Grafana OnCall integrations
Integrations allow you to connect monitoring systems of your choice to send alerts to Grafana OnCall. Regardless of where your alerts originate, you can configure alerts to be sent to Grafana OnCall for alert escalation and notification. Grafana OnCall receives alerts in JSON format via a POST request, OnCall then parses alert data using preconfigured alert templates to determine alert grouping, apply routes, and determine correct escalation.
Integrations allow you to connect monitoring systems of your choice to send alerts to Grafana OnCall. Regardless of where
your alerts originate, you can configure alerts to be sent to Grafana OnCall for alert escalation and notification.
Grafana OnCall receives alerts in JSON format via a POST request, OnCall then parses alert data using preconfigured
alert templates to determine alert grouping, apply routes, and determine correct escalation.
There are many integrations that are directly supported by Grafana OnCall. Those that arent currently listed in the Integrations menu can be connected using the webhook integration and configured alert templates.
There are many integrations that are directly supported by Grafana OnCall. Those that arent currently listed in the
Integrations menu can be connected using the webhook integration and configured alert templates.
## Configure and manage integrations
You can configure and manage your integrations from the **Integrations** tab in Grafana OnCall. The following sections describe how to configure and customize your integrations to ensure alerts are treated appropriately.
You can configure and manage your integrations from the **Integrations** tab in Grafana OnCall. The following sections
describe how to configure and customize your integrations to ensure alerts are treated appropriately.
### Connect an integration to Grafana OnCall
@ -35,7 +40,8 @@ To configure an integration for Grafana OnCall:
### Manage Grafana OnCall integrations
To manage existing integrations, navigate to the **Integrations** tab in Grafana OnCall and select the integration you want to manage.
To manage existing integrations, navigate to the **Integrations** tab in Grafana OnCall and select the integration
you want to manage.
#### Customize alert templates and grouping
@ -50,9 +56,11 @@ To customize alert grouping for an integration:
1. Click **Change alert template and grouping**.
2. Select **Alert Behavior** from the dropdown menu next to **Edit template for**.
3. Edit the **grouping id**, **acknowledge condition**, and **resolve condition** templates as needed to customize your alert behavior.
3. Edit the **grouping id**, **acknowledge condition**, and **resolve condition** templates as needed to customize
your alert behavior.
For more information on alert templates, see [Configure alerts templates]({{< relref "../alert-behavior/alert-templates" >}})
For more information on alert templates, see
[Configure alerts templates]({{< relref "../alert-behavior/alert-templates" >}})
#### Add Routes
@ -62,7 +70,8 @@ To add a route to an integration using regular expression:
2. Click **+ Add Route**.
3. Use python style regex to match on your alert content.
4. Click **Create Route**.
5. Select an escalation chain for “**IF** alert payload matches regex” and “**ELSE**” to specify where to route each type of alert.
5. Select an escalation chain for “**IF** alert payload matches regex” and “**ELSE**” to specify where to route each
type of alert.
#### Edit integration name

View file

@ -15,11 +15,15 @@ weight: 100
# Available integrations
Grafana OnCall can connect directly to the monitoring services where your alerts originate. All currently available integrations are listed in the Grafana OnCall **Create Integration** section.
Grafana OnCall can connect directly to the monitoring services where your alerts originate. All currently available
integrations are listed in the Grafana OnCall **Create Integration** section.
If the integration you're looking for isn't currently listed, see [Webhook integrations for Grafana OnCall]({{< relref "../available-integrations/configure-webhook" >}}) to integration your monitoring system with Grafana OnCall.
If the integration you're looking for isn't currently listed, see
[Webhook integrations for Grafana OnCall]({{< relref "../available-integrations/configure-webhook" >}}) to integration
your monitoring system with Grafana OnCall.
> **Note:** Some integrations are available for Grafana Cloud instances only. See individual integration guides for more information.
> **Note:** Some integrations are available for Grafana Cloud instances only. See individual integration
> guides for more information.
The following integrations are currently available for Grafana OnCall and have documentation:

View file

@ -18,16 +18,15 @@ weight: 300
The Alertmanager integration for Grafana OnCall handles alerts sent by client applications such as the Prometheus server.
Grafana OnCall provides<!--[grouping](#alertmanager-grouping-amp-oncall-grouping)--> grouping abilities when processing alerts from Alertmanager, including initial deduplicating, grouping, and routing the alerts to Grafana OnCall.
Grafana OnCall provides<!--[grouping](#alertmanager-grouping-amp-oncall-grouping)--> grouping abilities when processing
alerts from Alertmanager, including initial deduplicating, grouping, and routing the alerts to Grafana OnCall.
## Configure Alertmanager integration for Grafana OnCall
You must have an Admin role to create integrations in Grafana OnCall.
1. In the **Integrations** tab, click **+ New integration for receiving alerts**.
2. Select **Alertmanager** from the list of available integrations.
3. Follow the instructions in the **How to connect** window to get your unique integration URL and identify next steps.
<!--![123](../_images/connect-new-monitoring.png)-->
@ -36,16 +35,16 @@ You must have an Admin role to create integrations in Grafana OnCall.
Update the `receivers` section of your Alertmanager configuration to use a unique integration URL:
```
```yaml
route:
receiver: 'oncall'
receiver: "oncall"
group_by: [alertname, datacenter, app]
receivers:
- name: 'oncall'
webhook_configs:
- url: <integation-url>
send_resolved: true
- name: "oncall"
webhook_configs:
- url: <integation-url>
send_resolved: true
```
## Configure grouping with Alertmanager and Grafana OnCall
@ -55,14 +54,19 @@ You can use the alert grouping mechanics of Alertmanager and Grafana OnCall to c
Alertmanager offers three alert grouping options:
- `group_by` provides two options, `instance` or `job`.
- `group_wait` sets the length of time to initially wait before sending a notification for a particular group of alerts. For example, `group_wait` can be set to 45s.
- `group_wait` sets the length of time to initially wait before sending a notification for a particular group of alerts.
For example, `group_wait` can be set to 45s.
Setting a high value for `group_wait` reduces alert noise and minimizes interruption, but it may introduce delays in receiving alert notifications. To set an appropriate wait time, consider whether the group of alerts will be the same as those previously sent.
Setting a high value for `group_wait` reduces alert noise and minimizes interruption, but it may introduce delays in
receiving alert notifications. To set an appropriate wait time, consider whether the group of alerts will be the same
as those previously sent.
- `group_interval` sets the length of time to wait before sending notifications about new alerts that have been added to a group of alerts that have been previously alerted on. This setting is usually set to five minutes or more.
- `group_interval` sets the length of time to wait before sending notifications about new alerts that have been added to
a group of alerts that have been previously alerted on. This setting is usually set to five minutes or more.
During high alert volume periods, Alertmanager will send alerts at each `group_interval`, which can mean a lot of distraction. Grafana OnCall grouping will help manage this in the following ways:
During high alert volume periods, Alertmanager will send alerts at each `group_interval`, which can mean a lot of
distraction. Grafana OnCall grouping will help manage this in the following ways:
- Grafana OnCall groups alerts based on the first label of each alert.
- Grafana OnCall marks an incident as resolved only when the amount of grouped alerts with state `resolved` equals the amount of alerts with state `firing`.
- Grafana OnCall marks an incident as resolved only when the amount of grouped alerts with state `resolved` equals
the amount of alerts with state `firing`.

View file

@ -18,53 +18,53 @@ weight: 100
Grafana Alerting for Grafana OnCall can be set up using two methods:
- Grafana Alerting: Grafana OnCall is connected to the same Grafana instance being used to manage Grafana OnCall.
- Grafana (Other Grafana): Grafana OnCall is connected to one or more Grafana instances separate from the one being used to manage Grafana OnCall.
- Grafana (Other Grafana): Grafana OnCall is connected to one or more Grafana instances separate from the one being
used to manage Grafana OnCall.
## Configure Grafana Alerting for Grafana OnCall
You must have an Admin role to create integrations in Grafana OnCall.
1. In the **Integrations** tab, click **+ New integration for receiving alerts**.
2. Select **Grafana Alerting** by clicking the **Quick connect** button or select **Grafana (Other Grafana)** from the integrations list.
3. Follow the configuration steps that display in the **How to connect** window to retrieve your unique integration URL and complete any necessary configurations.
2. Select **Grafana Alerting** by clicking the **Quick connect** button or select **Grafana (Other Grafana)** from
the integrations list.
3. Follow the configuration steps that display in the **How to connect** window to retrieve your unique integration URL
and complete any necessary configurations.
### Configure Grafana Cloud Alerting
Use the following method if you are connecting Grafana OnCall with alerts coming from the same Grafana instance from which Grafana OnCall is being managed.
Use the following method if you are connecting Grafana OnCall with alerts coming from the same Grafana instance from
which Grafana OnCall is being managed.
1. In Grafana OnCall, navigate to the **Integrations** tab and select **New Integration for receiving alerts**.
1. Click **Quick connect** in the **Grafana Alerting** tile. This will automatically create the integration in Grafana
OnCall as well as the required contact point in Alerting.
1. Click **Quick connect** in the **Grafana Alerting** tile. This will automatically create the integration in Grafana OnCall as well as the required contact point in Alerting.
> **Note:** You must connect the contact point with a notification policy. For more information, see [Contact points in Grafana Alerting](https://grafana.com/docs/grafana/latest/alerting/unified-alerting/contact-points/)
1. Determine the escalation chain for the new integration by either selecting an existing one or by creating a new escalation chain.
1. In Grafana Cloud Alerting, navigate to **Alerting > Contact Points** and find a contact point with a name matching the integration you created in Grafana OnCall.
> **Note:** You must connect the contact point with a notification policy. For more information, see
> [Contact points in Grafana Alerting](https://grafana.com/docs/grafana/latest/alerting/unified-alerting/contact-points/)
1. Determine the escalation chain for the new integration by either selecting an existing one or by creating a new
escalation chain.
1. In Grafana Cloud Alerting, navigate to **Alerting > Contact Points** and find a contact point with a name matching
the integration you created in Grafana OnCall.
1. Click the **Edit** (pencil) icon, then click **Test**. This will send a test alert to Grafana OnCall.
### Configure Grafana (Other Grafana)
Connect Grafana OnCall with alerts coming from a Grafana instance that is different from the instance that Grafana OnCall is being managed:
Connect Grafana OnCall with alerts coming from a Grafana instance that is different from the instance that Grafana
OnCall is being managed:
1. In Grafana OnCall, navigate to the **Integrations** tab and select **New Integration for receiving alerts**.
2. Select the **Grafana (Other Grafana)** tile.
3. Follow the configuration steps that display in the **How to connect** window to retrieve your unique integration URL and complete any necessary configurations.
4. Determine the escalation chain for the new integration by either selecting an existing one or by creating a new escalation chain.
3. Follow the configuration steps that display in the **How to connect** window to retrieve your unique integration URL
and complete any necessary configurations.
4. Determine the escalation chain for the new integration by either selecting an existing one or by creating a
new escalation chain.
5. Go to the other Grafana instance to connect to Grafana OnCall and navigate to **Alerting > Contact Points**.
6. Select **New Contact Point**.
7. Choose the contact point type `webhook`, then paste the URL generated in step 3 into the URL field.
> **Note:** You must connect the contact point with a notification policy. For more information, see [Contact points in Grafana Alerting](https://grafana.com/docs/grafana/latest/alerting/unified-alerting/contact-points/).
> **Note:** You must connect the contact point with a notification policy. For more information,
> see [Contact points in Grafana Alerting](https://grafana.com/docs/grafana/latest/alerting/unified-alerting/contact-points/).
8. Click the **Edit** (pencil) icon, then click **Test**. This will send a test alert to Grafana OnCall.

View file

@ -16,14 +16,16 @@ weight: 700
# Webhook integrations for Grafana OnCall
Grafana OnCall directly supports many integrations, those that arent currently listed in the Integrations menu can be connected using the webhook integration and configured alert templates.
Grafana OnCall directly supports many integrations, those that arent currently listed in the Integrations menu can be
connected using the webhook integration and configured alert templates.
With the webhook integration, you can connect to any alert source that isn't listed in the **Create Integration** page.
There are two available formats, **Webhook** and **Formatted Webhook**.
- **Webhook** will pull all of the raw JSON payload and display it in the manner that it's received.
- **Formatted Webhook** can be used if the alert payload sent by your monitoring service is formatted in a way that OnCall recognizes.
- **Formatted Webhook** can be used if the alert payload sent by your monitoring service is formatted in a way that
OnCall recognizes.
The following fields are recognized, but none are required:
@ -39,7 +41,8 @@ To configure a webhook integration:
1. In the **Integrations** tab, click **+ New integration for receiving alerts**.
2. Select either **Webhook** or **Formatted Webhook** integration.
3. Follow the configuration steps in the **How to connect** section of the integration settings.
4. Use the unique webhook URL to complete any configuration in your monitoring service to send POST requests. Use any http client, e.g. curl to send POST requests with any payload.
4. Use the unique webhook URL to complete any configuration in your monitoring service to send POST requests. Use any
http client, e.g. curl to send POST requests with any payload.
For example:
@ -57,4 +60,5 @@ For example:
}'
```
To learn how to use custom alert templates for formatted webhooks, see [Configure alerts templates]({{< relref "../../../alert-behavior/alert-templates/" >}}).
To learn how to use custom alert templates for formatted webhooks, see
[Configure alerts templates]({{< relref "../../../alert-behavior/alert-templates/" >}}).

View file

@ -15,7 +15,9 @@ weight: 500
# Zabbix integration for Grafana OnCall
Zabbix is an open-source monitoring software tool for diverse IT components, including networks, servers, virtual machines, and cloud services. Zabbix provides monitoring for metrics such as network utilization, CPU load, and disk space consumption.
Zabbix is an open-source monitoring software tool for diverse IT components, including networks, servers, virtual
machines, and cloud services. Zabbix provides monitoring for metrics such as network utilization, CPU load, and disk
space consumption.
## Configure Zabbix integration for Grafana OnCall
@ -44,13 +46,15 @@ This integration is available for Grafana Cloud OnCall. You must have an Admin r
docker exec -it zabbix-appliance bash
```
1. Place the [grafana_oncall.sh](#grafana_oncallsh-script) script in the `AlertScriptsPath` directory specified within the Zabbix server configuration file (zabbix_server.conf).
1. Place the [grafana_oncall.sh](#grafana_oncallsh-script) script in the `AlertScriptsPath` directory specified within
the Zabbix server configuration file (zabbix_server.conf).
```bash
grep AlertScriptsPath /etc/zabbix/zabbix_server.conf
```
> **Note:** The script must be executable by the user running the zabbix_server binary (usually "zabbix") on the Zabbix server. For example, `chmod +x grafana_oncall.sh`
> **Note:** The script must be executable by the user running the zabbix_server binary (usually "zabbix") on the
> Zabbix server. For example, `chmod +x grafana_oncall.sh`
```bash
ls -lh /usr/lib/zabbix/alertscripts/grafana_oncall.sh
@ -92,6 +96,7 @@ To send alerts to Grafana OnCall, the {ALERT.SEND_TO} value must be set in the [
1. Specify **Send to** OnCall using the unique integration URL from the above step in the testing window that opens.
Create a test message with a body and optional subject and click **Test**.
<!--![](../_images/zabbix-4.png)
WHERE DID SLACK COME FROM?! 1. View the Grafana OnCall incident that appears in the Slack channel.
![](../_images/zabbix-5.png)-->
@ -103,9 +108,11 @@ Use the following procedure to configure grouping and auto-resolve.
1. Provide a parameter as an identifier for group differentiation to Grafana OnCall.
1. Append that variable to the subject of the action as `ONCALL_GROUP: ID`, where `ID` is any of the Zabbix [macros](https://www.zabbix.com/documentation/4.2/manual/appendix/macros/supported_by_location).
For example, `{EVENT.ID}`. The Grafana OnCall script [grafana_oncall.sh](#grafana_oncallsh-script) extracts this event and passes the `alert_uid` to Grafana OnCall.
For example, `{EVENT.ID}`. The Grafana OnCall script [grafana_oncall.sh](#grafana_oncallsh-script) extracts this event
and passes the `alert_uid` to Grafana OnCall.
1. To enable auto-resolve within Grafana Oncall, the "Resolved" keyword is required in the **Default subject** field in **Recovered operations**.
1. To enable auto-resolve within Grafana Oncall, the "Resolved" keyword is required in the **Default subject** field
in **Recovered operations**.
<!--![](../_images/zabbix-6.png)-->

View file

@ -17,7 +17,9 @@ weight: 300
# Available ChatOps integrations
Grafana OnCall directly supports the export of alert notifications to some popular messaging applications like Slack and Telegram. You can use outgoing webhooks to applications that aren't directly supported. For information on configuring outgoing webhooks, see [Send alert group notifications by webhook]({{< relref "../../alert-behavior/outgoing-webhooks/" >}}).
Grafana OnCall directly supports the export of alert notifications to some popular messaging applications like Slack and
Telegram. You can use outgoing webhooks to applications that aren't directly supported. For information on configuring
outgoing webhooks, see [Send alert group notifications by webhook]({{< relref "../../alert-behavior/outgoing-webhooks/" >}}).
To configure supported messaging apps, see the following topics:

View file

@ -17,16 +17,20 @@ weight: 100
# Slack integration for Grafana OnCall
The Slack integration for Grafana OnCall incorporates your Slack workspace directly into your incident response workflow to help your team focus on alert resolution with less friction.
Integrating your Slack workspace with Grafana OnCall allows users and teams to be notified of alerts directly in Slack with automated alert escalation steps and user notification preferences. There are a number of alert actions that users can take directly from Slack, including acknowledge, resolve, add resolution notes, and more.
The Slack integration for Grafana OnCall incorporates your Slack workspace directly into your incident response workflow
to help your team focus on alert resolution with less friction.
Integrating your Slack workspace with Grafana OnCall allows users and teams to be notified of alerts directly in Slack
with automated alert escalation steps and user notification preferences. There are a number of alert actions that users
can take directly from Slack, including acknowledge, resolve, add resolution notes, and more.
## Before you begin
To install the Slack integration, you must have Admin permissions in your Grafana instance as well as the Slack workspace that youd like to integrate with.
To install the Slack integration, you must have Admin permissions in your Grafana instance as well as the Slack workspace
that youd like to integrate with.
For Open Source Grafana OnCall Slack installation guidance, refer to [Open Source Grafana OnCall]({{< relref "../../../open-source/" >}}).
For Open Source Grafana OnCall Slack installation guidance, refer to
[Open Source Grafana OnCall]({{< relref "../../../open-source/" >}}).
## Install Slack integration for Grafana OnCall
@ -36,34 +40,44 @@ For Open Source Grafana OnCall Slack installation guidance, refer to [Open Sourc
4. Provide your Slack workspace URL and sign with your Slack credentials.
5. Click **Allow** to give Grafana OnCall permission to access your Slack workspace.
## Post-install configuration for Slack integration
Configure the following additional settings to ensure Grafana OnCall alerts are routed to the intended Slack channels and users:
Configure the following additional settings to ensure Grafana OnCall alerts are routed to the intended Slack channels
and users:
1. From your **Slack integration** settings, select a default slack channel in the first dropdown menu. This is where alerts will be sent unless otherwise specified in escalation chains.
2. In **Additional Settings**, configure alert reminders for alerts to retrigger after being acknowledged for some amount of time.
1. From your **Slack integration** settings, select a default slack channel in the first dropdown menu. This is where
alerts will be sent unless otherwise specified in escalation chains.
2. In **Additional Settings**, configure alert reminders for alerts to retrigger after being acknowledged for some
amount of time.
3. Ensure all users verify their slack account in their Grafana OnCall **users info**.
### Configure Escalation Chains with Slack notifications
Once your Slack integration is configured you can configure Escalation Chains to notify via Slack messages for alerts in Grafana OnCall.
There are two Slack notification options that you can configure into escalation chains, notify whole Slack channel and notify Slack user group:
Once your Slack integration is configured you can configure Escalation Chains to notify via Slack messages for alerts
in Grafana OnCall.
1. In Grafana OnCall, navigate to the **Escalation Chains** tab then select an existing escalation chain or click **+ New escalation chain**.
There are two Slack notification options that you can configure into escalation chains, notify whole Slack channel and
notify Slack user group:
1. In Grafana OnCall, navigate to the **Escalation Chains** tab then select an existing escalation chain or
click **+ New escalation chain**.
2. Click the dropdown for **Add escalation step**.
3. Configure your escalation chain with automated Slack notifications.
### Configure user notifications with Slack mentions
To be notified of alerts in Grafana OnCall via Slack mentions:
1. Navigate to the **Users** tab in Grafana OnCall, click **Edit** next to a user.
2. In the **User Info** tab, edit or configure notification steps by clicking + Add Notification step
3. select **Notify by** in the first dropdown and select **Slack mentions** in the second dropdown to receive alert notifications via Slack mentions.
3. select **Notify by** in the first dropdown and select **Slack mentions** in the second dropdown to receive alert
notifications via Slack mentions.
### Configure on-call notifications in Slack
The Slack integration for Grafana Oncall supports automated Slack on-call notifications that notify individuals and teams of their on-call shifts. Admins can configure shift notification behavior in Notification preferences:
1. When an on-call shift notification is sent to a person or channel, click the gear icon to access **Notifications preferences**.
The Slack integration for Grafana Oncall supports automated Slack on-call notifications that notify individuals and
teams of their on-call shifts. Admins can configure shift notification behavior in Notification preferences:
1. When an on-call shift notification is sent to a person or channel, click the gear icon to
access **Notifications preferences**.
2. Configure on-call notifications for future shift notifications.

View file

@ -18,48 +18,63 @@ weight: 500
# Microsoft Teams integration for Grafana OnCall
The Microsoft Teams integration for Grafana OnCall embeds your MS Teams channels directly into your incident response workflow to help your team focus on alert resolution.
The Microsoft Teams integration for Grafana OnCall embeds your MS Teams channels directly into your incident response
workflow to help your team focus on alert resolution.
Integrating MS Teams with Grafana OnCall allows users to be notified of alerts directly in MS Teams with automated escalation steps and user notification preferences. Users can also take action on alerts directly from MS Teams, including acknowledge, unacknowledge, resolve, and silence.
Integrating MS Teams with Grafana OnCall allows users to be notified of alerts directly in MS Teams with automated escalation
steps and user notification preferences. Users can also take action on alerts directly from MS Teams, including
acknowledge, unacknowledge, resolve, and silence.
## Before you begin
>NOTE: **This integration is available to Grafana Cloud instances of Grafana OnCall only.**
> NOTE: **This integration is available to Grafana Cloud instances of Grafana OnCall only.**
The following is required to connect to Microsoft Teams to Grafana OnCall:
- You must have Admin permissions in your Grafana Cloud instance.
- You must have Owner permissions in Microsoft Teams.
- Install the Grafana OnCall app from the [Microsoft Marketplace](https://appsource.microsoft.com/en-us/product/office/WA200004307).
## Install Microsoft Teams integration for Grafana OnCall
1. From the **ChatOps** tab in Grafana OnCall, select **Microsoft Teams** in the side menu.
1. Click **+Connect Microsoft Teams channel**.
2. Follow the steps provided to connect to your Teams channels, then click **Done**.
3. To add additional teams and channels click **+Connect Microsoft Teams channel** again and repeat step 3 as needed.
1. Follow the steps provided to connect to your Teams channels, then click **Done**.
1. To add additional teams and channels click **+Connect Microsoft Teams channel** again and repeat step 3 as needed.
## Post-install configuration for Microsoft Teams integration
Configure the following settings to ensure Grafana OnCall alerts are routed to the intended Teams channels and users:
- Set a default channel from the list of connected MS Teams channels. This is where alerts will be sent unless otherwise specified in escalation chains.
- Set a default channel from the list of connected MS Teams channels. This is where alerts will be sent unless otherwise
specified in escalation chains.
- Ensure all users verify their MS Teams account in their Grafana OnCall user profile.
### Connect Microsoft Teams user to Grafana OnCall
1. From the **Users** tab in Grafana OnCall, click **View my profile**.
1. Navigate to **Microsoft Teams username**, click **Connect**.
2. Follow the steps provided to connect your Teams user.
3. Navigate back to your Grafana OnCall profile and verify that your Microsoft Teams account is linked to your Grafana OnCall user.
1. Follow the steps provided to connect your Teams user.
1. Navigate back to your Grafana OnCall profile and verify that your Microsoft Teams account is linked to your Grafana
OnCall user.
### Configure user notifications with Microsoft Teams
To be notified of Grafana OnCall alerts via MS Teams:
1. Navigate to the **Users** tab in Grafana OnCall, click **Edit** next to a user.
1. In the **User Info** tab, edit or configure notification steps by clicking **+Add Notification step**
1. Select **Notify by** in the first dropdown and select **Microsoft Teams** in the second dropdown to receive alert notifications in Teams.
1. Select **Notify by** in the first dropdown and select **Microsoft Teams** in the second dropdown to receive alert
notifications in Teams.
### Configure escalation chains to post to Microsoft Teams channels
Once your MS Teams integration is configured you can add an escalation step at the integration level to automatically send alerts from a specific integration to a channel in MS Teams.
Once your MS Teams integration is configured you can add an escalation step at the integration level to automatically
send alerts from a specific integration to a channel in MS Teams.
To automatically send alerts from an integration to MS Teams channels:
1. Navigate to the **Integrations** tab in Grafana OnCall, select an existing integration or click **+New integration for receiving alerts**.
1. Navigate to the **Integrations** tab in Grafana OnCall, select an existing integration or
click **+New integration for receiving alerts**.
1. From the integrations settings, navigate to the escalation chain panel.
1. Enable **Post to Microsoft Teams channel** by selecting a channel to connect from the dropdown.
1. Enable **Post to Microsoft Teams channel** by selecting a channel to connect from the dropdown.

View file

@ -21,14 +21,16 @@ You can manage alerts either directly in your personal Telegram DMs or in a dedi
## Configure Telegram user settings in Grafana OnCall
To receive alert group contents, escalation logs and to be able to perform actions (acknowledge, resolve, silence) in Telegram DMs, please refer to the following steps:
To receive alert group contents, escalation logs and to be able to perform actions (acknowledge, resolve, silence) in
Telegram DMs, please refer to the following steps:
1. In your profile, find the Telegram setting and click **Connect**.
1. Click **Connect automatically** for the bot to message you and to bring up your telegram account.
1. Click **Start** when the OnCall bot messages you and wait for the connection confirmation.
1. Done! Now you can receive alerts directly to your Telegram DMs.
If you want to connect manually, you can click the URL provided and then **SEND MESSAGE**. In your Telegram account, click **Start**.
If you want to connect manually, you can click the URL provided and then **SEND MESSAGE**. In your Telegram account,
click **Start**.
## (Optional) Connect to a Telegram channel
@ -37,7 +39,8 @@ In case you want to manage alerts in a dedicated Telegram channel, please use th
> **NOTE:** Only Grafana users with the administrator role can configure OnCall settings.
1. In OnCall, click on the **ChatOps** tab and select Telegram in the side menu.
1. Click **Connect Telegram channel** and follow the instructions, mirrored here for reference. A unique verification code will be generated that you must use to activate the channel.
1. Click **Connect Telegram channel** and follow the instructions, mirrored here for reference. A unique verification
code will be generated that you must use to activate the channel.
1. In your team Telegram account, create a new channel, and set it to **Private**.
1. In **Manage Channel**, make sure **Sign messages** is enabled.
1. Create a new discussion group.
@ -50,5 +53,7 @@ In case you want to manage alerts in a dedicated Telegram channel, please use th
1. In OnCall, send the provided verification code to the channel.
1. Make sure users connect to Telegram in their OnCall user profile.
Each alert group is assigned a dedicated discussion. Users can perform actions (acknowledge, resolve, silence), and discuss alerts in the comments section of the discussions.
In case an integration route is not configured to use a Telegram channel, users will receive messages with alert group contents, logs and actions in their DMs.
Each alert group is assigned a dedicated discussion. Users can perform actions (acknowledge, resolve, silence), and
discuss alerts in the comments section of the discussions.
In case an integration route is not configured to use a Telegram channel, users will receive messages with alert group
contents, logs and actions in their DMs.

View file

@ -27,7 +27,8 @@ curl "api_endpoint_here" --header "Authorization: "api_key_here""
Grafana OnCall uses API keys to allow access to the API. You can request a new OnCall API key in OnCall -> Settings page.
An API key is specific to a user and a Grafana stack. If you want to switch to a different stack configuration, request a different API key.
An API key is specific to a user and a Grafana stack. If you want to switch to a different stack configuration,
request a different API key.
## Pagination
@ -44,7 +45,8 @@ The OnCall API returns them in pages. Note that the page size may vary.
## Rate Limits
Grafana OnCall provides rate limits to ensure alert group notifications will be delivered to your Slack workspace even when some integrations produce a large number of alerts.
Grafana OnCall provides rate limits to ensure alert group notifications will be delivered to your Slack workspace even
when some integrations produce a large number of alerts.
### Monitoring integrations Rate Limits

View file

@ -63,11 +63,16 @@ curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/" \
}'
```
<!-- markdownlint-disable MD013 -->
| Parameter | Required | Description |
| --------- | :------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mode` | No | Default setting is `wipe`. `wipe` will remove the payload of all Grafana OnCall group alerts. This is useful if you sent sensitive data to OnCall. All metadata will remain. `DELETE` will trigger the removal of alert groups, alerts, and all related metadata. It will also remove alert group notifications in Slack and other destinations. |
> **NOTE:** `DELETE` can take a few moments to delete alert groups because Grafana OnCall interacts with 3rd party APIs such as Slack. Please check objects using `GET` to be sure the data is removed.
<!-- markdownlint-enable MD013 -->
> **NOTE:** `DELETE` can take a few moments to delete alert groups because Grafana OnCall interacts with 3rd party APIs
> such as Slack. Please check objects using `GET` to be sure the data is removed.
**HTTP request**

View file

@ -32,6 +32,8 @@ The above command returns JSON structured in the following way:
}
```
<!-- markdownlint-disable MD013 -->
| Parameter | Required | Description |
| ---------------------------------- | :--------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `escalation_chain_id` | Yes | Each escalation policy is assigned to a specific escalation chain. |
@ -47,6 +49,8 @@ The above command returns JSON structured in the following way:
| `notify_if_time_from` | If type = `notify_if_time_from_to` | UTC time represents the beginning of the time period, for example `09:00:00Z`. |
| `notify_if_time_to` | If type = `notify_if_time_from_to` | UTC time represents the end of the time period, for example `18:00:00Z`. |
<!-- markdownlint-enable MD013 -->
**HTTP request**
`POST {{API_URL}}/api/v1/escalation_policies/`

View file

@ -67,7 +67,8 @@ The above command returns JSON structured in the following way:
```
Integrations are sources of alerts and alert groups for Grafana OnCall.
For example, to learn how to integrate Grafana OnCall with Alertmanager see [Alertmanager]({{< relref "../integrations/available-integrations/configure-alertmanager/" >}}).
For example, to learn how to integrate Grafana OnCall with Alertmanager see
[Alertmanager]({{< relref "../integrations/available-integrations/configure-alertmanager/" >}}).
**HTTP request**
@ -278,7 +279,8 @@ The above command returns JSON structured in the following way:
# Delete integration
Deleted integrations will stop recording new alerts from monitoring. Integration removal won't trigger removal of related alert groups or alerts.
Deleted integrations will stop recording new alerts from monitoring. Integration removal won't trigger removal of
related alert groups or alerts.
```shell
curl "{{API_URL}}/api/v1/integrations/CFRPV98RPR1U8/" \

View file

@ -43,6 +43,8 @@ The above command returns JSON structured in the following way:
}
```
<!-- markdownlint-disable MD013 -->
| Parameter | Unique | Required | Description |
| -------------------------------- | :----: | :--------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | Yes | Yes | On-call shift name. |
@ -52,9 +54,9 @@ The above command returns JSON structured in the following way:
| `level` | No | Optional | Priority level. The higher the value, the higher the priority. If two events overlap in one schedule, Grafana OnCall will choose the event with higher level. For example: Alex is on-call from 8AM till 11AM with level 1, Bob is on-call from 9AM till 11AM with level 2. At 10AM Grafana OnCall will notify Bob. At 8AM OnCall will notify Alex. |
| `start` | No | Yes | Start time of the on-call shift. This parameter takes a date format as `yyyy-MM-dd'T'HH:mm:ss` (for example "2020-09-05T08:00:00"). |
| `duration` | No | Yes | Duration of the event. |
| `frequency` | No | If type = `recurrent_event` or `rolling_users` | One of: `hourly`, `daily`, `weekly`, `monthly`. |
| `frequency` | No | If type = `recurrent_event` or `rolling_users` | One of: `hourly`, `daily`, `weekly`, `monthly`. |
| `interval` | No | Optional | This parameter takes a positive integer that represents the intervals that the recurrence rule repeats. |
| `until` | No | Optional | When the recurrence rule ends (endless if None). This parameter takes a date format as `yyyy-MM-dd'T'HH:mm:ss` (for example "2020-09-05T08:00:00"). |
| `until` | No | Optional | When the recurrence rule ends (endless if None). This parameter takes a date format as `yyyy-MM-dd'T'HH:mm:ss` (for example "2020-09-05T08:00:00"). |
| `week_start` | No | Optional | Start day of the week in iCal format. One of: `SU` (Sunday), `MO` (Monday), `TU` (Tuesday), `WE` (Wednesday), `TH` (Thursday), `FR` (Friday), `SA` (Saturday). Default: `SU`. |
| `by_day` | No | Optional | List of days in iCal format. Valid values are: `SU`, `MO`, `TU`, `WE`, `TH`, `FR`, `SA`. |
| `by_month` | No | Optional | List of months. Valid values are `1` to `12`. |
@ -63,6 +65,8 @@ The above command returns JSON structured in the following way:
| `rolling_users` | No | Optional | List of lists with on-call users (for `rolling_users` event type). Grafana OnCall will iterate over lists of users for every time frame specified in `frequency`. For example: there are two lists of users in `rolling_users` : [[Alex, Bob], [Alice]] and `frequency` = `daily` . This means that the first day Alex and Bob will be notified. The next day: Alice. The day after: Alex and Bob again and so on. |
| `start_rotation_from_user_index` | No | Optional | Index of the list of users in `rolling_users`, from which on-call rotation starts. By default, the start index is `0` |
<!-- markdownlint-enable MD013 -->
Please see [RFC 5545](https://tools.ietf.org/html/rfc5545#section-3.3.10) for more information about recurrence rules.
**HTTP request**

View file

@ -31,6 +31,8 @@ The above command returns JSON structured in the following way:
}
```
<!-- markdownlint-disable MD013 -->
| Parameter | Required | Description |
| ----------- | :------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `user_id` | Yes | User ID |
@ -39,6 +41,8 @@ The above command returns JSON structured in the following way:
| `duration` | Optional | A time in secs when type `wait` is chosen for `type`. |
| `important` | Optional | Boolean value indicates if a rule is "important". Default is `false`. |
<!-- markdownlint-enable MD013 -->
**HTTP request**
`POST {{API_URL}}/api/v1/personal_notification_rules/`

View file

@ -46,14 +46,18 @@ Routes allow you to direct different alerts to different messenger channels and
- Alerts for different engineering groups
- Snoozing spam & debugging alerts
<!-- markdownlint-disable MD013 -->
| Parameter | Unique | Required | Description |
| --------------------- | :----: | :------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `integration_id` | No | Yes | Each route is assigned to a specific integration. |
| `escalation_chain_id` | No | Yes | Each route is assigned a specific escalation chain. |
| `routing_regex` | Yes | Yes | Python Regex query (use https://regex101.com/ for debugging). OnCall chooses the route for an alert in case there is a match inside the whole alert payload. |
| `routing_regex` | Yes | Yes | Python Regex query (use <https://regex101.com/> for debugging). OnCall chooses the route for an alert in case there is a match inside the whole alert payload. |
| `position` | Yes | Optional | Route matching is performed one after another starting from position=`0`. Position=`-1` will put the route to the end of the list before `is_the_last_route`. A new route created with a position of an existing route will move the old route (and all following routes) down in the list. |
| `slack` | Yes | Optional | Dictionary with Slack-specific settings for a route. |
<!-- markdownlint-enable MD013 -->
**HTTP request**
`POST {{API_URL}}/api/v1/routes/`

View file

@ -41,6 +41,8 @@ The above command returns JSON structured in the following way:
}
```
<!-- markdownlint-disable MD013 -->
| Parameter | Unique | Required | Description |
| -------------------- | :----: | :--------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | Yes | Yes | Schedule name. |
@ -52,6 +54,8 @@ The above command returns JSON structured in the following way:
| `slack` | No | Optional | Dictionary with Slack-specific settings for a schedule. Includes `channel_id` and `user_group_id` fields, that take a channel ID and a user group ID from Slack. |
| `shifts` | No | Optional | List of shifts. Used for manually added on-call shifts in Schedules with type `calendar`. |
<!-- markdownlint-enable MD013 -->
**HTTP request**
`POST {{API_URL}}/api/v1/schedules/`

View file

@ -38,12 +38,16 @@ The above command returns JSON structured in the following way:
}
```
<!-- markdownlint-disable MD013 -->
| Parameter | Unique | Description |
| --------- | :----: | :---------------------------------------------------------------------------------------------------- |
| `id` | Yes | User Group ID |
| `type` | No | [Slack-defined user groups](https://slack.com/intl/en-ru/help/articles/212906697-Create-a-user-group) |
| `slack` | No | Metadata retrieved from Slack. |
<!-- markdownlint-enable MD013 -->
**HTTP request**
`GET {{API_URL}}/api/v1/user_groups/`

View file

@ -9,7 +9,9 @@ weight: 300
# Grafana OnCall open source guide
Grafana OnCall is a developer-friendly incident response tool that's available to Grafana open source and Grafana Cloud users. The OSS version of Grafana OnCall provides the same reliable on-call management solution along with the flexibility of a self-managed environment.
Grafana OnCall is a developer-friendly incident response tool that's available to Grafana open source and Grafana Cloud
users. The OSS version of Grafana OnCall provides the same reliable on-call management solution along with the
flexibility of a self-managed environment.
This guide describes the necessary installation and configuration steps needed to configure OSS Grafana OnCall.
@ -22,27 +24,33 @@ There are three Grafana OnCall OSS environments available:
- **Production** environment for reliable cloud installation using Helm: [Production Environment](#production-environment)
## Production Environment
We suggest using our official helm chart for the reliable production deployment of Grafana OnCall. It will deploy Grafana OnCall engine and celery workers, along with RabbitMQ cluster, Redis Cluster, and the database.
>**Note:** The Grafana OnCall engine currently supports one instance of the Grafana OnCall plugin at a time.
We suggest using our official helm chart for the reliable production deployment of Grafana OnCall. It will deploy
Grafana OnCall engine and celery workers, along with RabbitMQ cluster, Redis Cluster, and the database.
Check the [helm chart](https://github.com/grafana/oncall/tree/dev/helm/oncall) for more details.
> **Note:** The Grafana OnCall engine currently supports one instance of the Grafana OnCall plugin at a time.
We'll always be happy to provide assistance with production deployment in [our communities](https://github.com/grafana/oncall#join-community)!
Check the [helm chart](https://github.com/grafana/oncall/tree/dev/helm/oncall) for more details.
We'll always be happy to provide assistance with production deployment in [our communities](https://github.com/grafana/oncall#join-community)!
## Update Grafana OnCall OSS
To update an OSS installation of Grafana OnCall, please see the update docs:
- **Hobby** playground environment: [README.md](https://github.com/grafana/oncall#update-version)
- **Production** Helm environment: [Helm update](https://github.com/grafana/oncall/tree/dev/helm/oncall#update)
## Slack Setup
The Slack integration for Grafana OnCall leverages Slack API features to provide a customizable and useful integration. Refer to the following steps to configure the Slack integration:
The Slack integration for Grafana OnCall leverages Slack API features to provide a customizable and useful integration.
Refer to the following steps to configure the Slack integration:
1. Ensure your Grafana OnCall environment is up and running.
1. Grafana OnCall must be accessible through HTTPS. For development purposes, use [localtunnel](https://github.com/localtunnel/localtunnel). For production purposes, consider establishing a proper web server with HTTPS termination.
For localtunnel, refer to the following configuration:
1. Grafana OnCall must be accessible through HTTPS. For development purposes, use [localtunnel](https://github.com/localtunnel/localtunnel).
For production purposes, consider establishing a proper web server with HTTPS termination.
For localtunnel, refer to the following configuration:
```bash
# Choose the unique prefix instead of pretty-turkey-83
@ -55,107 +63,109 @@ lt --port 8080 -s pretty-turkey-83 --print-requests
1. [Create a Slack Workspace](https://slack.com/create) for development, or use your company workspace.
1. Go to https://api.slack.com/apps and click **Create an App** .
1. Go to <https://api.slack.com/apps> and click **Create an App** .
1. Select `From an app manifest` option and select your workspace.
1. Replace the text with the following YAML code block . Be sure to replace `<YOUR_BOT_NAME>` and `<ONCALL_ENGINE_PUBLIC_URL>` fields with the appropriate information.
1. Replace the text with the following YAML code block . Be sure to replace `<YOUR_BOT_NAME>` and `<ONCALL_ENGINE_PUBLIC_URL>`
fields with the appropriate information.
```yaml
_metadata:
major_version: 1
minor_version: 1
display_information:
name: <YOUR_BOT_NAME>
features:
app_home:
home_tab_enabled: true
messages_tab_enabled: true
messages_tab_read_only_enabled: false
bot_user:
display_name: <YOUR_BOT_NAME>
always_online: true
shortcuts:
- name: Create a new incident
type: message
callback_id: incident_create
description: Creates a new OnCall incident
- name: Add to resolution note
type: message
callback_id: add_resolution_note
description: Add this message to resolution note
slash_commands:
- command: /oncall
url: <ONCALL_ENGINE_PUBLIC_URL>/slack/interactive_api_endpoint/
description: oncall
should_escape: false
oauth_config:
redirect_urls:
- <ONCALL_ENGINE_PUBLIC_URL>/api/internal/v1/complete/slack-install-free/
- <ONCALL_ENGINE_PUBLIC_URL>/api/internal/v1/complete/slack-login/
scopes:
user:
- channels:read
- chat:write
- identify
- users.profile:read
bot:
- app_mentions:read
- channels:history
- channels:read
- chat:write
- chat:write.customize
- chat:write.public
- commands
- files:write
- groups:history
- groups:read
- im:history
- im:read
- im:write
- mpim:history
- mpim:read
- mpim:write
- reactions:write
- team:read
- usergroups:read
- usergroups:write
- users.profile:read
- users:read
- users:read.email
- users:write
settings:
event_subscriptions:
request_url: <ONCALL_ENGINE_PUBLIC_URL>/slack/event_api_endpoint/
bot_events:
- app_home_opened
- app_mention
- channel_archive
- channel_created
- channel_deleted
- channel_rename
- channel_unarchive
- member_joined_channel
- message.channels
- message.im
- subteam_created
- subteam_members_changed
- subteam_updated
- user_change
interactivity:
is_enabled: true
request_url: <ONCALL_ENGINE_PUBLIC_URL>/slack/interactive_api_endpoint/
org_deploy_enabled: false
socket_mode_enabled: false
```
```yaml
_metadata:
major_version: 1
minor_version: 1
display_information:
name: <YOUR_BOT_NAME>
features:
app_home:
home_tab_enabled: true
messages_tab_enabled: true
messages_tab_read_only_enabled: false
bot_user:
display_name: <YOUR_BOT_NAME>
always_online: true
shortcuts:
- name: Create a new incident
type: message
callback_id: incident_create
description: Creates a new OnCall incident
- name: Add to resolution note
type: message
callback_id: add_resolution_note
description: Add this message to resolution note
slash_commands:
- command: /oncall
url: <ONCALL_ENGINE_PUBLIC_URL>/slack/interactive_api_endpoint/
description: oncall
should_escape: false
oauth_config:
redirect_urls:
- <ONCALL_ENGINE_PUBLIC_URL>/api/internal/v1/complete/slack-install-free/
- <ONCALL_ENGINE_PUBLIC_URL>/api/internal/v1/complete/slack-login/
scopes:
user:
- channels:read
- chat:write
- identify
- users.profile:read
bot:
- app_mentions:read
- channels:history
- channels:read
- chat:write
- chat:write.customize
- chat:write.public
- commands
- files:write
- groups:history
- groups:read
- im:history
- im:read
- im:write
- mpim:history
- mpim:read
- mpim:write
- reactions:write
- team:read
- usergroups:read
- usergroups:write
- users.profile:read
- users:read
- users:read.email
- users:write
settings:
event_subscriptions:
request_url: <ONCALL_ENGINE_PUBLIC_URL>/slack/event_api_endpoint/
bot_events:
- app_home_opened
- app_mention
- channel_archive
- channel_created
- channel_deleted
- channel_rename
- channel_unarchive
- member_joined_channel
- message.channels
- message.im
- subteam_created
- subteam_members_changed
- subteam_updated
- user_change
interactivity:
is_enabled: true
request_url: <ONCALL_ENGINE_PUBLIC_URL>/slack/interactive_api_endpoint/
org_deploy_enabled: false
socket_mode_enabled: false
```
1. Set environment variables by navigating to your Grafana OnCall, then click **Env Variables** and set the following:
```
SLACK_CLIENT_OAUTH_ID = Basic Information -> App Credentials -> Client ID
SLACK_CLIENT_OAUTH_SECRET = Basic Information -> App Credentials -> Client Secret
SLACK_SIGNING_SECRET = Basic Information -> App Credentials -> Signing Secret
SLACK_INSTALL_RETURN_REDIRECT_HOST = << OnCall external URL >>
```
```text
SLACK_CLIENT_OAUTH_ID = Basic Information -> App Credentials -> Client ID
SLACK_CLIENT_OAUTH_SECRET = Basic Information -> App Credentials -> Client Secret
SLACK_SIGNING_SECRET = Basic Information -> App Credentials -> Signing Secret
SLACK_INSTALL_RETURN_REDIRECT_HOST = << OnCall external URL >>
```
1. In OnCall, navigate to **ChatOps**, select Slack and click **Install Slack integration**.
@ -163,17 +173,25 @@ lt --port 8080 -s pretty-turkey-83 --print-requests
## Telegram Setup
The Telegram integration for Grafana OnCall is designed for collaborative team work and improved incident response. Refer to the following steps to configure the Telegram integration:
The Telegram integration for Grafana OnCall is designed for collaborative team work and improved incident response.
Refer to the following steps to configure the Telegram integration:
1. Ensure your Grafana OnCall environment is up and running.
2. Create a Telegram bot using [BotFather](https://t.me/BotFather) and save the token provided by BotFather. Please make sure to disable **Group Privacy** for the bot (Bot Settings -> Group Privacy -> Turn off).
3. Paste the token provided by BotFather to the `TELEGRAM_TOKEN` variable on the **Env Variables** page of your Grafana OnCall instance.
4. Set the `TELEGRAM_WEBHOOK_HOST` variable to the external address of your Grafana OnCall instance. Please note that `TELEGRAM_WEBHOOK_HOST` must start with `https://` and be publicly available (meaning that it can be reached by Telegram servers). If your host is private or local, consider using a reverse proxy (e.g. [ngrok](https://ngrok.com)).
5. Now you can connect Telegram accounts on the **Users** page and receive alert groups to Telegram direct messages. Alternatively, in case you want to connect Telegram channels to your Grafana OnCall environment, navigate to the **ChatOps** tab.
1. Ensure your Grafana OnCall environment is up and running.
2. Create a Telegram bot using [BotFather](https://t.me/BotFather) and save the token provided by BotFather. Please make
sure to disable **Group Privacy** for the bot (Bot Settings -> Group Privacy -> Turn off).
3. Paste the token provided by BotFather to the `TELEGRAM_TOKEN` variable on the **Env Variables** page of your
Grafana OnCall instance.
4. Set the `TELEGRAM_WEBHOOK_HOST` variable to the external address of your Grafana OnCall instance. Please note
that `TELEGRAM_WEBHOOK_HOST` must start with `https://` and be publicly available (meaning that it can be reached by
Telegram servers). If your host is private or local, consider using a reverse proxy (e.g. [ngrok](https://ngrok.com)).
5. Now you can connect Telegram accounts on the **Users** page and receive alert groups to Telegram direct messages.
Alternatively, in case you want to connect Telegram channels to your Grafana OnCall environment, navigate
to the **ChatOps** tab.
## Grafana OSS-Cloud Setup
The benefits of connecting to Grafana Cloud include:
- Cloud OnCall could monitor OSS OnCall uptime using heartbeat
- SMS for user notifications
- Phone calls for user notifications.
@ -182,13 +200,16 @@ To connect to Grafana Cloud, refer to the **Cloud** page in your OSS Grafana OnC
## Twilio Setup
Grafana OnCall supports Twilio SMS and phone call notifications delivery. If you prefer to configure SMS and phone call notifications using Twilio, complete the following steps:
Grafana OnCall supports Twilio SMS and phone call notifications delivery. If you prefer to configure SMS and phone call
notifications using Twilio, complete the following steps:
1. Set `GRAFANA_CLOUD_NOTIFICATIONS_ENABLED` as **False** to ensure the Grafana OSS <-> Cloud connector is disabled.
1. From your **OnCall** environment, select **Env Variables** and configure all variables starting with `TWILIO_`.
1. From your **OnCall** environment, select **Env Variables** and configure all variables starting with `TWILIO_`.
## Email Setup
Grafana OnCall is capable of sending emails using SMTP as a user notification step. To setup email notifications, populate the following env variables with your SMTP server credentials:
Grafana OnCall is capable of sending emails using SMTP as a user notification step. To setup email notifications, populate
the following env variables with your SMTP server credentials:
- `EMAIL_HOST` - SMTP server host
- `EMAIL_HOST_USER` - SMTP server user

View file

@ -51,7 +51,7 @@ class AlertGroupTelegramRenderer(AlertGroupBaseRenderer):
status_verbose = self.alert_group.get_acknowledge_text()
# First line in the invisible link with id of organization.
# It is needed to add info about organization to the telegram message for the oncall-gateway.
text = f"<a href='{self.alert_group.channel.organization.web_link_with_id}'>&#8205;</a>"
text = f"<a href='{self.alert_group.channel.organization.web_link_with_uuid}'>&#8205;</a>"
text += f"{status_emoji} #{self.alert_group.inside_organization_number}, {title}\n"
text += f"{status_verbose}, alerts: {alerts_count_str}\n"
text += f"Source: {self.alert_group.channel.short_name}\n"

View file

@ -1,9 +1,12 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from django.conf import settings
from apps.base.messaging import get_messaging_backend_from_id
from apps.slack.slack_formatter import SlackFormatter
from common.jinja_templater import apply_jinja_template
from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning
class TemplateLoader:
@ -172,9 +175,15 @@ class AlertTemplater(ABC):
"amixr_incident_id": self.incident_id, # TODO: decide on variable names
"amixr_link": self.link, # TODO: decide on variable names
}
templated_attr, success = apply_jinja_template(attr_template, data, **context)
if success:
return templated_attr
try:
if attr == "title":
return apply_jinja_template(
attr_template, data, result_length_limit=settings.JINJA_RESULT_TITLE_MAX_LENGTH, **context
)
else:
return apply_jinja_template(attr_template, data, **context)
except (JinjaTemplateError, JinjaTemplateWarning) as e:
return e.fallback_message
return None

View file

@ -189,12 +189,12 @@ class Alert(models.Model):
# set web_title_cache to web title to allow alert group searching based on web_title_cache
web_title_template = template_manager.get_attr_template("title", alert_receive_channel, render_for="web")
if web_title_template:
web_title_cache = apply_jinja_template(web_title_template, raw_request_data)[0] or None
web_title_cache = apply_jinja_template(web_title_template, raw_request_data)
else:
web_title_cache = None
if grouping_id_template is not None:
group_distinction, _ = apply_jinja_template(grouping_id_template, raw_request_data)
group_distinction = apply_jinja_template(grouping_id_template, raw_request_data)
# Insert random uuid to prevent grouping of demo alerts or alerts with group_distinction=None
if is_demo or not group_distinction:
@ -204,13 +204,13 @@ class Alert(models.Model):
group_distinction = hashlib.md5(str(group_distinction).encode()).hexdigest()
if resolve_condition_template is not None:
is_resolve_signal, _ = apply_jinja_template(resolve_condition_template, payload=raw_request_data)
is_resolve_signal = apply_jinja_template(resolve_condition_template, payload=raw_request_data)
if isinstance(is_resolve_signal, str):
is_resolve_signal = is_resolve_signal.strip().lower() in ["1", "true", "ok"]
else:
is_resolve_signal = False
if acknowledge_condition_template is not None:
is_acknowledge_signal, _ = apply_jinja_template(acknowledge_condition_template, payload=raw_request_data)
is_acknowledge_signal = apply_jinja_template(acknowledge_condition_template, payload=raw_request_data)
if isinstance(is_acknowledge_signal, str):
is_acknowledge_signal = is_acknowledge_signal.strip().lower() in ["1", "true", "ok"]
else:

View file

@ -1,7 +1,6 @@
import logging
import typing
from collections import namedtuple
from typing import Optional
from typing import Optional, TypedDict
from urllib.parse import urljoin
from uuid import uuid1
@ -46,8 +45,10 @@ def generate_public_primary_key_for_alert_group():
return new_public_primary_key
class Permalinks(typing.TypedDict):
slack: str
class Permalinks(TypedDict):
slack: Optional[str]
telegram: Optional[str]
web: str
class AlertGroupQuerySet(models.QuerySet):
@ -401,12 +402,12 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
raise NotImplementedError
@property
def slack_permalink(self):
def slack_permalink(self) -> Optional[str]:
if self.slack_message is not None:
return self.slack_message.permalink
@property
def telegram_permalink(self) -> typing.Optional[str]:
def telegram_permalink(self) -> Optional[str]:
"""
This property will attempt to access an attribute, `prefetched_telegram_messages`, representing a list of
prefetched telegram messages. If this attribute does not exist, it falls back to performing a query.
@ -429,10 +430,11 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
return {
"slack": self.slack_permalink,
"telegram": self.telegram_permalink,
"web": self.web_link,
}
@property
def web_link(self):
def web_link(self) -> str:
return urljoin(self.channel.organization.web_link, f"?page=incident&id={self.public_primary_key}")
@property

View file

@ -1,6 +1,7 @@
import json
import logging
import re
from json import JSONDecodeError
from django.conf import settings
from django.core.validators import MinLengthValidator
@ -9,7 +10,8 @@ from django.db.models import F
from django.utils import timezone
from requests.auth import HTTPBasicAuth
from common.jinja_templater import jinja_template_env
from common.jinja_templater import apply_jinja_template
from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
logger = logging.getLogger(__name__)
@ -103,13 +105,19 @@ class CustomButton(models.Model):
if self.forward_whole_payload:
post_kwargs["json"] = alert.raw_request_data
elif self.data:
rendered_data = jinja_template_env.from_string(self.data).render(
{
"alert_payload": self._escape_alert_payload(alert.raw_request_data),
"alert_group_id": alert.group.public_primary_key,
}
)
post_kwargs["json"] = json.loads(rendered_data)
try:
rendered_data = apply_jinja_template(
self.data,
alert_payload=self._escape_alert_payload(alert.raw_request_data),
alert_group_id=alert.group.public_primary_key,
)
except (JinjaTemplateError, JinjaTemplateWarning) as e:
post_kwargs["json"] = {"error": e.fallback_message}
try:
post_kwargs["json"] = json.loads(rendered_data)
except JSONDecodeError:
post_kwargs["data"] = rendered_data
return post_kwargs
def _escape_alert_payload(self, payload: dict):

View file

@ -76,7 +76,7 @@ def update_web_title_cache(alert_receive_channel_pk, alert_group_pks):
if web_title_template:
if alert_group.pk in first_alert_map:
raw_request_data = first_alert_map[alert_group.pk]["raw_request_data"]
web_title_cache = apply_jinja_template(web_title_template, raw_request_data)[0] or None
web_title_cache = apply_jinja_template(web_title_template, raw_request_data)
else:
web_title_cache = None
else:

View file

@ -53,13 +53,13 @@ def notify_user_task(
organization = alert_group.channel.organization
if not user.is_notification_allowed:
task_logger.info(f"notify_user_task: user {user.pk} notification is not allowed for role {user.role}")
task_logger.info(f"notify_user_task: user {user.pk} notification is not allowed")
UserNotificationPolicyLogRecord(
author=user,
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
reason=f"notification is not allowed for user with role {user.role}",
reason=f"notification is not allowed for user",
alert_group=alert_group,
notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ALLOWED_USER_ROLE,
notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_FORBIDDEN,
).save()
return
@ -252,9 +252,9 @@ def perform_notification(log_record_pk):
UserNotificationPolicyLogRecord(
author=user,
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
reason=f"notification is not allowed for user with role {user.role}",
reason=f"notification is not allowed for user",
alert_group=alert_group,
notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ALLOWED_USER_ROLE,
notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_FORBIDDEN,
).save()
return

View file

@ -4,7 +4,6 @@ from apps.alerts.incident_appearance.renderers.phone_call_renderer import AlertG
from apps.alerts.models import AlertGroup
from apps.alerts.tasks.delete_alert_group import delete_alert_group
from apps.slack.models import SlackMessage
from common.constants.role import Role
@pytest.mark.django_db
@ -14,7 +13,7 @@ def test_render_for_phone_call(
make_alert_group,
make_alert,
):
organization, slack_team_identity = make_organization_with_slack_team_identity()
organization, _ = make_organization_with_slack_team_identity()
alert_receive_channel = make_alert_receive_channel(organization, integration_slack_channel_id="CWER1ASD")
alert_group = make_alert_group(alert_receive_channel)
@ -59,7 +58,7 @@ def test_delete(
organization, slack_team_identity = make_organization_with_slack_team_identity()
slack_channel = make_slack_channel(slack_team_identity, name="general", slack_id="CWER1ASD")
user = make_user(organization=organization, role=Role.ADMIN)
user = make_user(organization=organization)
alert_receive_channel = make_alert_receive_channel(organization, integration_slack_channel_id="CWER1ASD")

View file

@ -8,9 +8,9 @@ from apps.alerts.escalation_snapshot.serializers.escalation_policy_snapshot impo
from apps.alerts.escalation_snapshot.snapshot_classes import EscalationPolicySnapshot
from apps.alerts.escalation_snapshot.utils import eta_for_escalation_step_notify_if_time
from apps.alerts.models import AlertGroupLogRecord, EscalationPolicy
from apps.api.permissions import LegacyAccessControlRole
from apps.schedules.ical_utils import list_users_to_notify_from_ical
from apps.schedules.models import CustomOnCallShift, OnCallScheduleCalendar
from common.constants.role import Role
def get_escalation_policy_snapshot_from_model(escalation_policy):
@ -213,8 +213,8 @@ def test_escalation_step_notify_on_call_schedule_viewer_user(
make_schedule,
make_on_call_shift,
):
organization, user, _, channel_filter, alert_group, reason = escalation_step_test_setup
viewer = make_user_for_organization(organization=organization, role=Role.VIEWER)
organization, _, _, channel_filter, alert_group, reason = escalation_step_test_setup
viewer = make_user_for_organization(organization=organization, role=LegacyAccessControlRole.VIEWER)
schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar)
# create on_call_shift with user to notify
@ -263,7 +263,7 @@ def test_escalation_step_notify_user_group(
make_slack_user_group,
make_escalation_policy,
):
organization, user, _, channel_filter, alert_group, reason = escalation_step_test_setup
organization, _, _, channel_filter, alert_group, reason = escalation_step_test_setup
slack_team_identity = make_slack_team_identity()
organization.slack_team_identity = slack_team_identity
organization.save()
@ -295,7 +295,7 @@ def test_escalation_step_notify_if_time(
escalation_step_test_setup,
make_escalation_policy,
):
organization, user, _, channel_filter, alert_group, reason = escalation_step_test_setup
_, _, _, channel_filter, alert_group, reason = escalation_step_test_setup
# current time is not between from_time and to_time, step returns eta
now = timezone.now()
@ -358,7 +358,7 @@ def test_escalation_step_notify_if_time(
def test_escalation_step_notify_if_num_alerts_in_window(
mocked_execute_tasks, escalation_step_test_setup, make_escalation_policy, make_alert
):
organization, user, _, channel_filter, alert_group, reason = escalation_step_test_setup
_, _, _, channel_filter, alert_group, reason = escalation_step_test_setup
make_alert(alert_group=alert_group, raw_request_data={})
make_alert(alert_group=alert_group, raw_request_data={})
@ -419,7 +419,7 @@ def test_escalation_step_trigger_custom_button(
make_custom_action,
make_escalation_policy,
):
organization, _, alert_receive_channel, channel_filter, alert_group, reason = escalation_step_test_setup
organization, _, _, channel_filter, alert_group, reason = escalation_step_test_setup
custom_button = make_custom_action(organization=organization)

View file

@ -3,9 +3,11 @@ from unittest.mock import patch
import pytest
from apps.alerts.tasks.notify_user import notify_user_task, perform_notification
from apps.api.permissions import LegacyAccessControlRole
from apps.base.models.user_notification_policy import UserNotificationPolicy
from apps.base.models.user_notification_policy_log_record import UserNotificationPolicyLogRecord
from common.constants.role import Role
NOTIFICATION_UNAUTHORIZED_MSG = "notification is not allowed for user"
@pytest.mark.django_db
@ -131,7 +133,9 @@ def test_notify_user_perform_notification_error_if_viewer(
make_user_notification_policy_log_record,
):
organization = make_organization()
user_1 = make_user(organization=organization, role=Role.VIEWER, _verified_phone_number="1234567890")
user_1 = make_user(
organization=organization, role=LegacyAccessControlRole.VIEWER, _verified_phone_number="1234567890"
)
user_notification_policy = make_user_notification_policy(
user=user_1,
step=UserNotificationPolicy.Step.NOTIFY,
@ -150,11 +154,8 @@ def test_notify_user_perform_notification_error_if_viewer(
error_log_record = UserNotificationPolicyLogRecord.objects.last()
assert error_log_record.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED
assert error_log_record.reason == f"notification is not allowed for user with role {user_1.role}"
assert (
error_log_record.notification_error_code
== UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ALLOWED_USER_ROLE
)
assert error_log_record.reason == NOTIFICATION_UNAUTHORIZED_MSG
assert error_log_record.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_FORBIDDEN
@pytest.mark.django_db
@ -165,7 +166,9 @@ def test_notify_user_error_if_viewer(
make_alert_group,
):
organization = make_organization()
user_1 = make_user(organization=organization, role=Role.VIEWER, _verified_phone_number="1234567890")
user_1 = make_user(
organization=organization, role=LegacyAccessControlRole.VIEWER, _verified_phone_number="1234567890"
)
alert_receive_channel = make_alert_receive_channel(organization=organization)
alert_group = make_alert_group(alert_receive_channel=alert_receive_channel)
@ -173,8 +176,5 @@ def test_notify_user_error_if_viewer(
error_log_record = UserNotificationPolicyLogRecord.objects.last()
assert error_log_record.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED
assert error_log_record.reason == f"notification is not allowed for user with role {user_1.role}"
assert (
error_log_record.notification_error_code
== UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ALLOWED_USER_ROLE
)
assert error_log_record.reason == NOTIFICATION_UNAUTHORIZED_MSG
assert error_log_record.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_FORBIDDEN

View file

@ -1,5 +1,294 @@
from .actions import ActionPermission # noqa: F401
from .constants import ALL_BASE_ACTIONS, MODIFY_ACTIONS, READ_ACTIONS # noqa: F401
from .methods import MethodPermission # noqa: F401
from .owner import IsOwner, IsOwnerOrAdmin, IsOwnerOrAdminOrEditor # noqa: F401
from .roles import AnyRole, IsAdmin, IsAdminOrEditor, IsEditor, IsStaff, IsViewer # noqa: F401
import enum
import typing
from rest_framework import permissions
from rest_framework.authentication import BasicAuthentication, SessionAuthentication
from rest_framework.request import Request
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSet, ViewSetMixin
from common.utils import getattrd
ACTION_PREFIX = "grafana-oncall-app"
RBAC_PERMISSIONS_ATTR = "rbac_permissions"
RBAC_OBJECT_PERMISSIONS_ATTR = "rbac_object_permissions"
ViewSetOrAPIView = typing.Union[ViewSet, APIView]
class GrafanaAPIPermission(typing.TypedDict):
action: str
class Resources(enum.Enum):
ALERT_GROUPS = "alert-groups"
INTEGRATIONS = "integrations"
ESCALATION_CHAINS = "escalation-chains"
SCHEDULES = "schedules"
CHATOPS = "chatops"
OUTGOING_WEBHOOKS = "outgoing-webhooks"
MAINTENANCE = "maintenance"
API_KEYS = "api-keys"
NOTIFICATIONS = "notifications"
NOTIFICATION_SETTINGS = "notification-settings"
USER_SETTINGS = "user-settings"
OTHER_SETTINGS = "other-settings"
class Actions(enum.Enum):
READ = "read"
WRITE = "write"
ADMIN = "admin"
TEST = "test"
EXPORT = "export"
UPDATE_SETTINGS = "update-settings"
class LegacyAccessControlRole(enum.IntEnum):
ADMIN = 0
EDITOR = 1
VIEWER = 2
@classmethod
def choices(cls):
return tuple((option.value, option.name) for option in cls)
class LegacyAccessControlCompatiblePermission:
def __init__(self, resource: Resources, action: Actions, fallback_role: LegacyAccessControlRole) -> None:
self.value = f"{ACTION_PREFIX}.{resource.value}:{action.value}"
self.fallback_role = fallback_role
def get_most_authorized_role(
permissions: typing.List[LegacyAccessControlCompatiblePermission],
) -> LegacyAccessControlRole:
if not permissions:
return LegacyAccessControlRole.VIEWER
# ex. Admin is 0, Viewer is 2, thereby min makes sense here
return min({p.fallback_role for p in permissions}, key=lambda r: r.value)
def user_is_authorized(user, required_permissions: typing.List[LegacyAccessControlCompatiblePermission]) -> bool:
if user.organization.is_rbac_permissions_enabled:
user_permissions = [u["action"] for u in user.permissions]
required_permissions = [p.value for p in required_permissions]
return all(permission in user_permissions for permission in required_permissions)
return user.role <= get_most_authorized_role(required_permissions).value
class RBACPermission(permissions.BasePermission):
class Permissions:
ALERT_GROUPS_READ = LegacyAccessControlCompatiblePermission(
Resources.ALERT_GROUPS, Actions.READ, LegacyAccessControlRole.VIEWER
)
ALERT_GROUPS_WRITE = LegacyAccessControlCompatiblePermission(
Resources.ALERT_GROUPS, Actions.WRITE, LegacyAccessControlRole.EDITOR
)
INTEGRATIONS_READ = LegacyAccessControlCompatiblePermission(
Resources.INTEGRATIONS, Actions.READ, LegacyAccessControlRole.VIEWER
)
INTEGRATIONS_TEST = LegacyAccessControlCompatiblePermission(
Resources.INTEGRATIONS, Actions.TEST, LegacyAccessControlRole.EDITOR
)
INTEGRATIONS_WRITE = LegacyAccessControlCompatiblePermission(
Resources.INTEGRATIONS, Actions.WRITE, LegacyAccessControlRole.ADMIN
)
ESCALATION_CHAINS_READ = LegacyAccessControlCompatiblePermission(
Resources.ESCALATION_CHAINS, Actions.READ, LegacyAccessControlRole.VIEWER
)
ESCALATION_CHAINS_WRITE = LegacyAccessControlCompatiblePermission(
Resources.ESCALATION_CHAINS, Actions.WRITE, LegacyAccessControlRole.ADMIN
)
SCHEDULES_READ = LegacyAccessControlCompatiblePermission(
Resources.SCHEDULES, Actions.READ, LegacyAccessControlRole.VIEWER
)
SCHEDULES_WRITE = LegacyAccessControlCompatiblePermission(
Resources.SCHEDULES, Actions.WRITE, LegacyAccessControlRole.ADMIN
)
SCHEDULES_EXPORT = LegacyAccessControlCompatiblePermission(
Resources.SCHEDULES, Actions.EXPORT, LegacyAccessControlRole.EDITOR
)
CHATOPS_READ = LegacyAccessControlCompatiblePermission(
Resources.CHATOPS, Actions.READ, LegacyAccessControlRole.VIEWER
)
CHATOPS_WRITE = LegacyAccessControlCompatiblePermission(
Resources.CHATOPS, Actions.WRITE, LegacyAccessControlRole.EDITOR
)
CHATOPS_UPDATE_SETTINGS = LegacyAccessControlCompatiblePermission(
Resources.CHATOPS, Actions.UPDATE_SETTINGS, LegacyAccessControlRole.ADMIN
)
OUTGOING_WEBHOOKS_READ = LegacyAccessControlCompatiblePermission(
Resources.OUTGOING_WEBHOOKS, Actions.READ, LegacyAccessControlRole.VIEWER
)
OUTGOING_WEBHOOKS_WRITE = LegacyAccessControlCompatiblePermission(
Resources.OUTGOING_WEBHOOKS, Actions.WRITE, LegacyAccessControlRole.ADMIN
)
MAINTENANCE_READ = LegacyAccessControlCompatiblePermission(
Resources.MAINTENANCE, Actions.READ, LegacyAccessControlRole.VIEWER
)
MAINTENANCE_WRITE = LegacyAccessControlCompatiblePermission(
Resources.MAINTENANCE, Actions.WRITE, LegacyAccessControlRole.EDITOR
)
API_KEYS_READ = LegacyAccessControlCompatiblePermission(
Resources.API_KEYS, Actions.READ, LegacyAccessControlRole.ADMIN
)
API_KEYS_WRITE = LegacyAccessControlCompatiblePermission(
Resources.API_KEYS, Actions.WRITE, LegacyAccessControlRole.ADMIN
)
NOTIFICATIONS_READ = LegacyAccessControlCompatiblePermission(
Resources.NOTIFICATIONS, Actions.READ, LegacyAccessControlRole.EDITOR
)
NOTIFICATION_SETTINGS_READ = LegacyAccessControlCompatiblePermission(
Resources.NOTIFICATION_SETTINGS, Actions.READ, LegacyAccessControlRole.VIEWER
)
NOTIFICATION_SETTINGS_WRITE = LegacyAccessControlCompatiblePermission(
Resources.NOTIFICATION_SETTINGS, Actions.WRITE, LegacyAccessControlRole.EDITOR
)
USER_SETTINGS_READ = LegacyAccessControlCompatiblePermission(
Resources.USER_SETTINGS, Actions.READ, LegacyAccessControlRole.VIEWER
)
USER_SETTINGS_WRITE = LegacyAccessControlCompatiblePermission(
Resources.USER_SETTINGS, Actions.WRITE, LegacyAccessControlRole.EDITOR
)
USER_SETTINGS_ADMIN = LegacyAccessControlCompatiblePermission(
Resources.USER_SETTINGS, Actions.ADMIN, LegacyAccessControlRole.ADMIN
)
OTHER_SETTINGS_READ = LegacyAccessControlCompatiblePermission(
Resources.OTHER_SETTINGS, Actions.READ, LegacyAccessControlRole.VIEWER
)
OTHER_SETTINGS_WRITE = LegacyAccessControlCompatiblePermission(
Resources.OTHER_SETTINGS, Actions.WRITE, LegacyAccessControlRole.ADMIN
)
@staticmethod
def _get_view_action(request: Request, view: ViewSetOrAPIView) -> str:
"""
For right now this needs to support being used in both a ViewSet as well as APIView, we use both interchangably
Note: `request.method` is returned uppercase
"""
return view.action if isinstance(view, ViewSetMixin) else request.method.lower()
def has_permission(self, request: Request, view: ViewSetOrAPIView) -> bool:
action = self._get_view_action(request, view)
rbac_permissions: RBACPermissionsAttribute = getattr(view, RBAC_PERMISSIONS_ATTR, None)
# first check that the rbac_permissions dict attribute is defined
assert (
rbac_permissions is not None
), f"Must define a {RBAC_PERMISSIONS_ATTR} dict on the ViewSet that is consuming the RBACPermission class"
action_required_permissions: typing.Union[None, typing.List] = rbac_permissions.get(action, None)
# next check that the action in question is defined within the rbac_permissions dict attribute
assert (
action_required_permissions is not None
), f"""Each action must be defined within the {RBAC_PERMISSIONS_ATTR} dict on the ViewSet.
\nIf an action requires no permissions, its value should explicitly be set to an empty list"""
return user_is_authorized(request.user, action_required_permissions)
def has_object_permission(self, request: Request, view: ViewSetOrAPIView, obj: typing.Any) -> bool:
rbac_object_permissions: RBACObjectPermissionsAttribute = getattr(view, RBAC_OBJECT_PERMISSIONS_ATTR, None)
if rbac_object_permissions:
action = self._get_view_action(request, view)
for permission_class, actions in rbac_object_permissions.items():
if action in actions:
return permission_class.has_object_permission(request, view, obj)
return False
# has_object_permission is called after has_permission, so return True if in view there is not
# RBAC_OBJECT_PERMISSIONS_ATTR attr which mean no additional check involving object required
return True
class IsOwner(permissions.BasePermission):
def __init__(self, ownership_field: typing.Optional[str] = None) -> None:
self.ownership_field = ownership_field
def has_object_permission(self, request: Request, _view: ViewSet, obj: typing.Any) -> bool:
owner = obj if self.ownership_field is None else getattrd(obj, self.ownership_field)
return owner == request.user
class HasRBACPermissions(permissions.BasePermission):
def __init__(self, required_permissions: typing.List[LegacyAccessControlCompatiblePermission]) -> None:
self.required_permissions = required_permissions
def has_object_permission(self, request: Request, _view: ViewSetOrAPIView, _obj: typing.Any) -> bool:
return user_is_authorized(request.user, self.required_permissions)
class IsOwnerOrHasRBACPermissions(permissions.BasePermission):
def __init__(
self,
required_permissions: typing.List[LegacyAccessControlCompatiblePermission],
ownership_field: typing.Optional[str] = None,
) -> None:
self.IsOwner = IsOwner(ownership_field)
self.HasRBACPermissions = HasRBACPermissions(required_permissions)
def has_object_permission(self, request: Request, view: ViewSetOrAPIView, obj: typing.Any) -> bool:
return self.IsOwner.has_object_permission(request, view, obj) or self.HasRBACPermissions.has_object_permission(
request, view, obj
)
class IsStaff(permissions.BasePermission):
STAFF_AUTH_CLASSES = [BasicAuthentication, SessionAuthentication]
def has_permission(self, request: Request, _view: ViewSet) -> bool:
user = request.user
if not any(isinstance(request._authenticator, x) for x in self.STAFF_AUTH_CLASSES):
return False
if user and user.is_authenticated:
return user.is_staff
return False
RBACPermissionsAttribute = typing.Dict[str, typing.List[LegacyAccessControlCompatiblePermission]]
RBACObjectPermissionsAttribute = typing.Dict[permissions.BasePermission, typing.List[str]]
# The below is legacy, it is only needed currently for backward compatibility w/ users running
# older "pinned" version of Grafana in Grafana Cloud
_DONT_USE_LEGACY_VIEWER_PERMISSIONS = []
_DONT_USE_LEGACY_EDITOR_PERMISSIONS = ["update_incidents", "update_own_settings", "view_other_users"]
_DONT_USE_LEGACY_ADMIN_PERMISSIONS = _DONT_USE_LEGACY_EDITOR_PERMISSIONS + [
"update_alert_receive_channels",
"update_escalation_policies",
"update_notification_policies",
"update_general_log_channel_id",
"update_other_users_settings",
"update_integrations",
"update_schedules",
"update_custom_actions",
"update_api_tokens",
"update_teams",
"update_maintenances",
"update_global_settings",
"send_demo_alert",
]
DONT_USE_LEGACY_PERMISSION_MAPPING: typing.Dict[LegacyAccessControlRole, typing.List[str]] = {
LegacyAccessControlRole.VIEWER: _DONT_USE_LEGACY_VIEWER_PERMISSIONS,
LegacyAccessControlRole.EDITOR: _DONT_USE_LEGACY_EDITOR_PERMISSIONS,
LegacyAccessControlRole.ADMIN: _DONT_USE_LEGACY_ADMIN_PERMISSIONS,
}

View file

@ -1,27 +0,0 @@
from typing import Any
from rest_framework import permissions
from rest_framework.request import Request
from rest_framework.viewsets import ViewSet
class ActionPermission(permissions.BasePermission):
def has_permission(self, request: Request, view: ViewSet) -> bool:
for permission, actions in getattr(view, "action_permissions", {}).items():
if view.action in actions:
return permission().has_permission(request, view)
return False
def has_object_permission(self, request: Request, view: ViewSet, obj: Any) -> bool:
# action_object_permissions attr should be used in case permission check require lookup
# for some object's properties e.g. team.
if getattr(view, "action_object_permissions", None):
for permission, actions in getattr(view, "action_object_permissions", {}).items():
if view.action in actions:
return permission().has_object_permission(request, view, obj)
return False
else:
# has_object_permission is called after has_permission, so return True if in view there is not
# action_object_permission attr which mean no additional check involving object required
return True

View file

@ -1,14 +0,0 @@
READ_ACTIONS = (
"list",
"retrieve",
"metadata",
)
MODIFY_ACTIONS = (
"create",
"update",
"partial_update",
"destroy",
)
ALL_BASE_ACTIONS = READ_ACTIONS + MODIFY_ACTIONS

View file

@ -1,12 +0,0 @@
from rest_framework import permissions
from rest_framework.request import Request
from rest_framework.viewsets import ViewSet
class MethodPermission(permissions.BasePermission):
def has_permission(self, request: Request, view: ViewSet) -> bool:
for permission, methods in getattr(view, "method_permissions", {}).items():
if request.method in methods:
return permission().has_permission(request, view)
return False

View file

@ -1,24 +0,0 @@
from typing import Any
from rest_framework import permissions
from rest_framework.request import Request
from rest_framework.viewsets import ViewSet
from apps.api.permissions.roles import IsAdmin, IsEditor
from common.utils import getattrd
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request: Request, view: ViewSet, obj: Any) -> bool:
ownership_field = getattr(view, "ownership_field", None)
if ownership_field is None:
owner = obj
else:
owner = getattrd(obj, ownership_field)
return owner == request.user
IsOwnerOrAdmin = IsOwner | IsAdmin
IsOwnerOrAdminOrEditor = IsOwner | IsAdmin | IsEditor

View file

@ -1,49 +0,0 @@
from typing import Any
from rest_framework import permissions
from rest_framework.authentication import BasicAuthentication, SessionAuthentication
from rest_framework.request import Request
from rest_framework.viewsets import ViewSet
from common.constants.role import Role
class RolePermission(permissions.BasePermission):
ROLE = None
def has_permission(self, request: Request, view: ViewSet) -> bool:
return request.user.role == type(self).ROLE
def has_object_permission(self, request: Request, view: ViewSet, obj: Any) -> bool:
return self.has_permission(request, view)
class IsAdmin(RolePermission):
ROLE = Role.ADMIN
class IsEditor(RolePermission):
ROLE = Role.EDITOR
class IsViewer(RolePermission):
ROLE = Role.VIEWER
IsAdminOrEditor = IsAdmin | IsEditor
AnyRole = IsAdmin | IsEditor | IsViewer
class IsStaff(permissions.BasePermission):
STAFF_AUTH_CLASSES = [BasicAuthentication, SessionAuthentication]
def has_permission(self, request: Request, view: ViewSet) -> bool:
user = request.user
if not any(isinstance(request._authenticator, x) for x in self.STAFF_AUTH_CLASSES):
return False
if user and user.is_authenticated:
return user.is_staff
return False
def has_object_permission(self, request: Request, view: ViewSet, obj: Any) -> bool:
return self.has_permission(request, view)

View file

@ -0,0 +1,428 @@
import typing
import pytest
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin
from . import (
RBAC_PERMISSIONS_ATTR,
GrafanaAPIPermission,
HasRBACPermissions,
IsOwner,
IsOwnerOrHasRBACPermissions,
LegacyAccessControlCompatiblePermission,
RBACObjectPermissionsAttribute,
RBACPermission,
RBACPermissionsAttribute,
get_most_authorized_role,
user_is_authorized,
)
class MockedOrg:
def __init__(self, org_has_rbac_enabled: bool) -> None:
self.is_rbac_permissions_enabled = org_has_rbac_enabled
class MockedUser:
def __init__(
self, permissions: typing.List[LegacyAccessControlCompatiblePermission], org_has_rbac_enabled=True
) -> None:
self.permissions = [GrafanaAPIPermission(action=perm.value) for perm in permissions]
self.role = get_most_authorized_role(permissions)
self.organization = MockedOrg(org_has_rbac_enabled)
class MockedSchedule:
def __init__(self, user: MockedUser) -> None:
self.user = user
class MockedRequest:
def __init__(self, user: typing.Optional[MockedUser] = None, method: typing.Optional[str] = None) -> None:
if user:
self.user = user
if method:
self.method = method
class MockedViewSet(ViewSetMixin):
def __init__(
self,
action: str,
rbac_permissions: typing.Optional[RBACPermissionsAttribute] = None,
rbac_object_permissions: typing.Optional[RBACObjectPermissionsAttribute] = None,
) -> None:
super().__init__()
self.action = action
if rbac_permissions:
self.rbac_permissions = rbac_permissions
if rbac_object_permissions:
self.rbac_object_permissions = rbac_object_permissions
class MockedAPIView(APIView):
def __init__(
self,
rbac_permissions: typing.Optional[RBACPermissionsAttribute] = None,
rbac_object_permissions: typing.Optional[RBACObjectPermissionsAttribute] = None,
) -> None:
super().__init__()
if rbac_permissions:
self.rbac_permissions = rbac_permissions
if rbac_object_permissions:
self.rbac_object_permissions = rbac_object_permissions
@pytest.mark.parametrize(
"user_permissions,required_permissions,org_has_rbac_enabled,expected_result",
[
(
[RBACPermission.Permissions.ALERT_GROUPS_READ],
[RBACPermission.Permissions.ALERT_GROUPS_READ],
True,
True,
),
(
[RBACPermission.Permissions.ALERT_GROUPS_READ],
[RBACPermission.Permissions.ALERT_GROUPS_READ],
False,
True,
),
(
[RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE],
[RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE],
True,
True,
),
(
[RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE],
[RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE],
False,
True,
),
(
[RBACPermission.Permissions.ALERT_GROUPS_WRITE],
[RBACPermission.Permissions.ALERT_GROUPS_READ],
True,
False,
),
(
[RBACPermission.Permissions.ALERT_GROUPS_WRITE],
[RBACPermission.Permissions.ALERT_GROUPS_READ],
False,
True,
),
(
[RBACPermission.Permissions.ALERT_GROUPS_READ],
[RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE],
False,
False,
),
(
[RBACPermission.Permissions.ALERT_GROUPS_READ],
[RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE],
True,
False,
),
],
)
def test_user_is_authorized(user_permissions, required_permissions, org_has_rbac_enabled, expected_result) -> None:
user = MockedUser(user_permissions, org_has_rbac_enabled=org_has_rbac_enabled)
assert user_is_authorized(user, required_permissions) == expected_result
@pytest.mark.parametrize(
"permissions,expected_role",
[
([RBACPermission.Permissions.ALERT_GROUPS_READ], RBACPermission.Permissions.ALERT_GROUPS_READ.fallback_role),
(
[RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE],
RBACPermission.Permissions.ALERT_GROUPS_WRITE.fallback_role,
),
(
[
RBACPermission.Permissions.USER_SETTINGS_READ,
RBACPermission.Permissions.USER_SETTINGS_WRITE,
RBACPermission.Permissions.USER_SETTINGS_ADMIN,
],
RBACPermission.Permissions.USER_SETTINGS_ADMIN.fallback_role,
),
],
)
def test_get_most_authorized_role(permissions, expected_role) -> None:
assert get_most_authorized_role(permissions) == expected_role
class TestRBACPermission:
def test_get_view_action(self) -> None:
viewset_action = "viewset_action"
viewset = MockedViewSet(viewset_action)
apiview = MockedAPIView()
method = "APIVIEW_ACTION"
request = MockedRequest(method=method)
assert RBACPermission._get_view_action(request, viewset) == viewset_action, "it works with a ViewSet"
assert RBACPermission._get_view_action(request, apiview) == method.lower(), "it works with an APIView"
def test_has_permission_works_on_a_viewset_view(self) -> None:
required_permission = RBACPermission.Permissions.ALERT_GROUPS_READ
action = "hello"
viewset = MockedViewSet(
action=action,
rbac_permissions={
action: [required_permission],
},
)
viewset_with_no_required_permissions = MockedViewSet(
action=action,
rbac_permissions={
action: [],
},
)
user_with_permission = MockedUser([required_permission])
user_without_permission = MockedUser([RBACPermission.Permissions.ALERT_GROUPS_WRITE])
assert (
RBACPermission().has_permission(MockedRequest(user_with_permission), viewset) is True
), "it works on a viewset when the user does have permission"
assert (
RBACPermission().has_permission(MockedRequest(user_without_permission), viewset) is False
), "it works on a viewset when the user does have permission"
assert (
RBACPermission().has_permission(
MockedRequest(user_without_permission), viewset_with_no_required_permissions
)
is True
), "it works on a viewset when the viewset action does not require permissions"
def test_has_permission_works_on_an_apiview_view(self) -> None:
required_permission = RBACPermission.Permissions.ALERT_GROUPS_READ
method = "hello"
apiview = MockedAPIView(
rbac_permissions={
method: [required_permission],
}
)
apiview_with_no_permissions = MockedAPIView(
rbac_permissions={
method: [],
}
)
user1 = MockedUser([required_permission])
user2 = MockedUser([RBACPermission.Permissions.ALERT_GROUPS_WRITE])
class Request(MockedRequest):
def __init__(self, user: typing.Optional[MockedUser] = None) -> None:
super().__init__(user, method)
assert (
RBACPermission().has_permission(Request(user1), apiview) is True
), "it works on an APIView when the user has permission"
assert (
RBACPermission().has_permission(Request(user2), apiview) is False
), "it works on an APIView when the user does not have permission"
assert (
RBACPermission().has_permission(Request(user2), apiview_with_no_permissions) is True
), "it works on a viewset when the viewset action does not require permissions"
def test_has_permission_throws_assertion_error_if_developer_forgets_to_specify_rbac_permissions(self) -> None:
action_slash_method = "hello"
error_msg = (
f"Must define a {RBAC_PERMISSIONS_ATTR} dict on the ViewSet that is consuming the RBACPermission class"
)
viewset = MockedViewSet(action_slash_method)
apiview = MockedAPIView()
with pytest.raises(AssertionError, match=error_msg):
RBACPermission().has_permission(MockedRequest(), viewset)
with pytest.raises(AssertionError, match=error_msg):
RBACPermission().has_permission(MockedRequest(method=action_slash_method), apiview)
def test_has_permission_throws_assertion_error_if_developer_forgets_to_specify_an_action_in_rbac_permissions(
self,
) -> None:
action_slash_method = "hello"
other_action_rbac_permissions = {"bonjour": []}
error_msg = f"""Each action must be defined within the {RBAC_PERMISSIONS_ATTR} dict on the ViewSet.
\nIf an action requires no permissions, its value should explicitly be set to an empty list"""
viewset = MockedViewSet(action_slash_method, other_action_rbac_permissions)
apiview = MockedAPIView(rbac_permissions=other_action_rbac_permissions)
with pytest.raises(AssertionError, match=error_msg):
RBACPermission().has_permission(MockedRequest(), viewset)
with pytest.raises(AssertionError, match=error_msg):
RBACPermission().has_permission(MockedRequest(method=action_slash_method), apiview)
def test_has_object_permission_returns_true_if_rbac_object_permissions_not_specified(self) -> None:
request = MockedRequest()
assert RBACPermission().has_object_permission(request, MockedAPIView(), None) is True
assert RBACPermission().has_object_permission(request, MockedViewSet("potato"), None) is True
def test_has_object_permission_works_if_no_permission_class_specified_for_action(self) -> None:
action = "hello"
request = MockedRequest(None, action)
apiview = MockedAPIView(rbac_object_permissions={})
viewset = MockedViewSet(action, rbac_object_permissions={})
assert RBACPermission().has_object_permission(request, apiview, None) is True
assert RBACPermission().has_object_permission(request, viewset, None) is True
def test_has_object_permission_works_when_permission_class_specified_for_action(self) -> None:
action = "hello"
mocked_permission_class_response = "asdfasdfasdf"
class MockedPermissionClass:
def has_object_permission(self, _req, _view, _obj) -> None:
return mocked_permission_class_response
rbac_object_permissions = {MockedPermissionClass(): (action,)}
request = MockedRequest(None, action)
apiview = MockedAPIView(rbac_object_permissions=rbac_object_permissions)
viewset = MockedViewSet(action, rbac_object_permissions=rbac_object_permissions)
assert RBACPermission().has_object_permission(request, apiview, None) == mocked_permission_class_response
assert RBACPermission().has_object_permission(request, viewset, None) == mocked_permission_class_response
class TestIsOwner:
def test_it_works_when_comparing_user_to_object(self) -> None:
user1 = MockedUser([])
user2 = MockedUser([])
request = MockedRequest(user1)
IsUser = IsOwner()
assert IsUser.has_object_permission(request, None, user1) is True
assert IsUser.has_object_permission(request, None, user2) is False
def test_it_works_when_comparing_user_to_ownership_field_object(self) -> None:
user1 = MockedUser([])
user2 = MockedUser([])
schedule = MockedSchedule(user1)
IsScheduleOwner = IsOwner("user")
assert IsScheduleOwner.has_object_permission(MockedRequest(user1), None, schedule) is True
assert IsScheduleOwner.has_object_permission(MockedRequest(user2), None, schedule) is False
def test_it_works_when_comparing_user_to_nested_ownership_field_object(self) -> None:
class Thingy:
def __init__(self, schedule: MockedSchedule) -> None:
self.schedule = schedule
user1 = MockedUser([])
user2 = MockedUser([])
schedule = MockedSchedule(user1)
thingy = Thingy(schedule)
IsScheduleOwner = IsOwner("schedule.user")
assert IsScheduleOwner.has_object_permission(MockedRequest(user1), None, thingy) is True
assert IsScheduleOwner.has_object_permission(MockedRequest(user2), None, thingy) is False
@pytest.mark.parametrize(
"user_permissions,required_permissions,expected_result",
[
(
[RBACPermission.Permissions.ALERT_GROUPS_READ],
[RBACPermission.Permissions.ALERT_GROUPS_READ],
True,
),
(
[RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE],
[RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE],
True,
),
(
[RBACPermission.Permissions.ALERT_GROUPS_WRITE],
[RBACPermission.Permissions.ALERT_GROUPS_READ],
False,
),
(
[RBACPermission.Permissions.ALERT_GROUPS_READ],
[RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE],
False,
),
],
)
def test_HasRBACPermission(user_permissions, required_permissions, expected_result) -> None:
request = MockedRequest(MockedUser(user_permissions))
assert HasRBACPermissions(required_permissions).has_object_permission(request, None, None) == expected_result
class TestIsOwnerOrHasRBACPermissions:
required_permission = RBACPermission.Permissions.SCHEDULES_READ
required_permissions = [required_permission]
def test_it_works_when_user_is_owner_and_does_not_have_permissions(self) -> None:
user1 = MockedUser([])
schedule = MockedSchedule(user1)
request = MockedRequest(user1)
PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions)
assert PermClass.has_object_permission(request, None, user1) is True
PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions, "user")
assert PermClass.has_object_permission(request, None, schedule) is True
def test_it_works_when_user_is_owner_and_has_permissions(self) -> None:
user1 = MockedUser(self.required_permissions)
schedule = MockedSchedule(user1)
request = MockedRequest(user1)
PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions)
assert PermClass.has_object_permission(request, None, user1) is True
PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions, "user")
assert PermClass.has_object_permission(request, None, schedule) is True
def test_it_works_when_user_is_not_owner_and_does_not_have_permissions(self) -> None:
user1 = MockedUser([])
user2 = MockedUser([])
schedule = MockedSchedule(user1)
request = MockedRequest(user2)
PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions)
assert PermClass.has_object_permission(request, None, user1) is False
PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions, "user")
assert PermClass.has_object_permission(request, None, schedule) is False
def test_it_works_when_user_is_not_owner_and_has_permissions(self) -> None:
user1 = MockedUser([])
user2 = MockedUser(self.required_permissions)
schedule = MockedSchedule(user1)
request = MockedRequest(user2)
PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions)
assert PermClass.has_object_permission(request, None, user1) is True
PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions, "user")
assert PermClass.has_object_permission(request, None, schedule) is True
class Thingy:
def __init__(self, schedule: MockedSchedule) -> None:
self.schedule = schedule
thingy = Thingy(schedule)
PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions, "schedule.user")
assert PermClass.has_object_permission(request, None, thingy) is True
assert PermClass.has_object_permission(MockedRequest(MockedUser([])), None, thingy) is False

View file

@ -132,7 +132,8 @@ class AlertGroupSerializer(AlertGroupListSerializer):
fields = AlertGroupListSerializer.Meta.fields + [
"alerts",
"render_after_resolve_report_json",
"slack_permalink",
"slack_permalink", # TODO: make plugin frontend use "permalinks" field to get Slack link
"permalinks",
"last_alert_at",
]

View file

@ -20,7 +20,8 @@ from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField, Writabl
from common.api_helpers.exceptions import BadRequest
from common.api_helpers.mixins import IMAGE_URL, TEMPLATE_NAMES_ONLY_WITH_NOTIFICATION_CHANNEL, EagerLoadingMixin
from common.api_helpers.utils import CurrentTeamDefault
from common.jinja_templater import jinja_template_env
from common.jinja_templater import apply_jinja_template, jinja_template_env
from common.jinja_templater.apply_jinja_template import JinjaTemplateWarning
from .integration_heartbeat import IntegrationHeartBeatSerializer
@ -28,9 +29,10 @@ from .integration_heartbeat import IntegrationHeartBeatSerializer
def valid_jinja_template_for_serializer_method_field(template):
for _, val in template.items():
try:
jinja_template_env.from_string(val)
except TemplateSyntaxError:
raise serializers.ValidationError("invalid template")
apply_jinja_template(val, payload={})
except JinjaTemplateWarning:
# Suppress warnings, template may be valid with payload
pass
class AlertReceiveChannelSerializer(EagerLoadingMixin, serializers.ModelSerializer):

View file

@ -1,15 +1,14 @@
import json
from collections import defaultdict
from django.core.validators import URLValidator, ValidationError
from jinja2 import TemplateError
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from apps.alerts.models import CustomButton
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
from common.api_helpers.utils import CurrentOrganizationDefault, CurrentTeamDefault
from common.jinja_templater import jinja_template_env
from common.jinja_templater import apply_jinja_template
from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning
class CustomButtonSerializer(serializers.ModelSerializer):
@ -53,32 +52,12 @@ class CustomButtonSerializer(serializers.ModelSerializer):
return None
try:
template = jinja_template_env.from_string(data)
except TemplateError:
raise serializers.ValidationError("Data has incorrect template")
try:
rendered = template.render(
{
# Validate that the template can be rendered with a JSON-ish alert payload.
# We don't know what the actual payload will be, so we use a defaultdict
# so that attribute access within a template will never fail
# (provided it's only one level deep - we won't accept templates that attempt
# to do nested attribute access).
# Every attribute access should return a string to ensure that users are
# correctly using `tojson` or wrapping fields in strings.
# If we instead used a `defaultdict(dict)` or `defaultdict(lambda: 1)` we
# would accidentally accept templates such as `{"name": {{ alert_payload.name }}}`
# which would then fail at the true render time due to the
# lack of explicit quotes around the template variable; this would render
# as `{"name": some_alert_name}` which is not valid JSON.
"alert_payload": defaultdict(str),
"alert_group_id": "abcd",
}
)
json.loads(rendered)
except ValueError:
raise serializers.ValidationError("Data has incorrect format")
apply_jinja_template(data, alert_payload=defaultdict(str), alert_group_id="abcd")
except JinjaTemplateError as e:
raise serializers.ValidationError(e.fallback_message)
except JinjaTemplateWarning:
# Suppress render exceptions since we do not have a representative payload to test with
pass
return data

View file

@ -1,15 +1,14 @@
from rest_framework import serializers
from apps.api.serializers.schedule_base import ScheduleBaseSerializer
from apps.schedules.models import OnCallScheduleCalendar
from apps.schedules.tasks import schedule_notify_about_empty_shifts_in_schedule, schedule_notify_about_gaps_in_schedule
from apps.slack.models import SlackChannel, SlackUserGroup
from common.api_helpers.custom_fields import OrganizationFilteredPrimaryKeyRelatedField
from common.api_helpers.utils import validate_ical_url
from common.timezones import TimeZoneField
class ScheduleCalendarSerializer(ScheduleBaseSerializer):
time_zone = serializers.CharField(required=False)
time_zone = TimeZoneField(required=False)
class Meta:
model = OnCallScheduleCalendar

View file

@ -1,14 +1,13 @@
from rest_framework import serializers
from apps.api.serializers.schedule_base import ScheduleBaseSerializer
from apps.schedules.models import OnCallScheduleWeb
from apps.schedules.tasks import schedule_notify_about_empty_shifts_in_schedule, schedule_notify_about_gaps_in_schedule
from apps.slack.models import SlackChannel, SlackUserGroup
from common.api_helpers.custom_fields import OrganizationFilteredPrimaryKeyRelatedField
from common.timezones import TimeZoneField
class ScheduleWebSerializer(ScheduleBaseSerializer):
time_zone = serializers.CharField(required=False)
time_zone = TimeZoneField(required=False)
class Meta:
model = OnCallScheduleWeb

View file

@ -1,12 +1,12 @@
import math
import time
import typing
import pytz
from django.conf import settings
from rest_framework import serializers
from apps.api.permissions import DONT_USE_LEGACY_PERMISSION_MAPPING
from apps.api.serializers.telegram import TelegramToUserConnectorSerializer
from apps.base.constants import ADMIN_PERMISSIONS, ALL_ROLES_PERMISSIONS, EDITOR_PERMISSIONS
from apps.base.messaging import get_messaging_backends
from apps.base.models import UserNotificationPolicy
from apps.base.utils import live_settings
@ -16,7 +16,7 @@ from apps.user_management.models import User
from apps.user_management.models.user import default_working_hours
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
from common.api_helpers.mixins import EagerLoadingMixin
from common.constants.role import Role
from common.timezones import TimeZoneField
from .custom_serializers import DynamicFieldsModelSerializer
from .organization import FastOrganizationSerializer
@ -34,9 +34,9 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
organization = FastOrganizationSerializer(read_only=True)
current_team = TeamPrimaryKeyRelatedField(allow_null=True, required=False)
timezone = serializers.CharField(allow_null=True, required=False)
timezone = TimeZoneField(allow_null=True, required=False)
avatar = serializers.URLField(source="avatar_url", read_only=True)
avatar_full = serializers.URLField(source="avatar_full_url", read_only=True)
permissions = serializers.SerializerMethodField()
notification_chain_verbal = serializers.SerializerMethodField()
cloud_connection_status = serializers.SerializerMethodField()
@ -51,8 +51,10 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
"current_team",
"email",
"username",
"role",
"name",
"role", # LEGACY.. this should get removed eventually
"avatar",
"avatar_full",
"timezone",
"working_hours",
"unverified_phone_number",
@ -60,7 +62,7 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
"slack_user_identity",
"telegram_configuration",
"messaging_backends",
"permissions",
"permissions", # LEGACY.. this should get removed eventually
"notification_chain_verbal",
"cloud_connection_status",
"hide_phone_number",
@ -68,21 +70,11 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
read_only_fields = [
"email",
"username",
"role",
"name",
"role", # LEGACY.. this should get removed eventually
"verified_phone_number",
]
def validate_timezone(self, tz):
if tz is None:
return tz
try:
pytz.timezone(tz)
except pytz.UnknownTimeZoneError:
raise serializers.ValidationError("not a valid timezone")
return tz
def validate_working_hours(self, working_hours):
if not isinstance(working_hours, dict):
raise serializers.ValidationError("must be dict")
@ -136,13 +128,8 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
serialized_data[backend_id] = backend.serialize_user(obj)
return serialized_data
def get_permissions(self, obj):
if obj.role == Role.ADMIN:
return ADMIN_PERMISSIONS
elif obj.role == Role.EDITOR:
return EDITOR_PERMISSIONS
else:
return ALL_ROLES_PERMISSIONS
def get_permissions(self, obj) -> typing.List[str]:
return DONT_USE_LEGACY_PERMISSION_MAPPING[obj.role]
def get_notification_chain_verbal(self, obj):
default, important = UserNotificationPolicy.get_short_verbals_for_user(user=obj)
@ -177,7 +164,7 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
class UserHiddenFieldsSerializer(UserSerializer):
available_for_all_roles_fields = [
fields_available_for_all_users = [
"pk",
"organization",
"current_team",
@ -193,7 +180,7 @@ class UserHiddenFieldsSerializer(UserSerializer):
ret = super(UserSerializer, self).to_representation(instance)
if instance.id != self.context["request"].user.id:
for field in ret:
if field not in self.available_for_all_roles_fields:
if field not in self.fields_available_for_all_users:
ret[field] = "******"
ret["hidden_fields"] = True
return ret

View file

@ -9,7 +9,7 @@ from rest_framework.response import Response
from rest_framework.test import APIClient
from apps.alerts.models import AlertGroup, AlertGroupLogRecord
from common.constants.role import Role
from apps.api.permissions import LegacyAccessControlRole
alert_raw_request_data = {
"evalMatches": [
@ -25,15 +25,6 @@ alert_raw_request_data = {
}
# # This function is for creating token and do not to change fixture alert_group_internal_api_setup return values.
# # To create token amixr team is needed but in most tests using fixture alert_group_internal_api_setup team is redundant
# # So it just extract amixr team form alert_groups.
# def create_token_from_initial_test_data(make_func, alert_groups, role):
# organization = alert_groups[0].channel.organization
# _, token_user_role = make_func(organization, role)
# return token_user_role
@pytest.fixture()
def alert_group_internal_api_setup(
make_organization_and_user_with_plugin_token,
@ -52,7 +43,7 @@ def alert_group_internal_api_setup(
@pytest.mark.django_db
def test_get_filter_started_at(alert_group_internal_api_setup, make_user_auth_headers):
user, token, alert_groups = alert_group_internal_api_setup
user, token, _ = alert_group_internal_api_setup
client = APIClient()
url = reverse("api-internal:alertgroup-list")
@ -69,7 +60,7 @@ def test_get_filter_started_at(alert_group_internal_api_setup, make_user_auth_he
@pytest.mark.django_db
def test_get_filter_resolved_at_alertgroup_empty_result(alert_group_internal_api_setup, make_user_auth_headers):
client = APIClient()
user, token, alert_groups = alert_group_internal_api_setup
user, token, _ = alert_group_internal_api_setup
url = reverse("api-internal:alertgroup-list")
response = client.get(
@ -84,7 +75,7 @@ def test_get_filter_resolved_at_alertgroup_empty_result(alert_group_internal_api
@pytest.mark.django_db
def test_get_filter_resolved_at_alertgroup_invalid_format(alert_group_internal_api_setup, make_user_auth_headers):
client = APIClient()
user, token, alert_groups = alert_group_internal_api_setup
user, token, _ = alert_group_internal_api_setup
url = reverse("api-internal:alertgroup-list")
response = client.get(
@ -660,19 +651,25 @@ def test_get_filter_with_resolution_note_after_delete_resolution_note(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_group_acknowledge_permissions(
make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status
alert_group_internal_api_setup,
make_user_for_organization,
make_user_auth_headers,
role,
expected_status,
):
client = APIClient()
_, token, alert_groups = alert_group_internal_api_setup
_, _, new_alert_group, _ = alert_groups
organization = new_alert_group.channel.organization
user = make_user_for_organization(organization, role)
client = APIClient()
url = reverse("api-internal:alertgroup-acknowledge", kwargs={"pk": new_alert_group.public_primary_key})
with patch(
@ -689,19 +686,24 @@ def test_alert_group_acknowledge_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_group_unacknowledge_permissions(
make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status
alert_group_internal_api_setup,
make_user_for_organization,
make_user_auth_headers,
role,
expected_status,
):
client = APIClient()
_, token, alert_groups = alert_group_internal_api_setup
_, _, new_alert_group, _ = alert_groups
organization = new_alert_group.channel.organization
user = make_user_for_organization(organization, role)
client = APIClient()
url = reverse("api-internal:alertgroup-unacknowledge", kwargs={"pk": new_alert_group.public_primary_key})
with patch(
@ -718,19 +720,24 @@ def test_alert_group_unacknowledge_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_group_resolve_permissions(
make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status
alert_group_internal_api_setup,
make_user_for_organization,
make_user_auth_headers,
role,
expected_status,
):
client = APIClient()
_, token, alert_groups = alert_group_internal_api_setup
_, _, new_alert_group, _ = alert_groups
organization = new_alert_group.channel.organization
user = make_user_for_organization(organization, role)
client = APIClient()
url = reverse("api-internal:alertgroup-resolve", kwargs={"pk": new_alert_group.public_primary_key})
with patch(
@ -747,19 +754,24 @@ def test_alert_group_resolve_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_group_unresolve_permissions(
make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status
alert_group_internal_api_setup,
make_user_for_organization,
make_user_auth_headers,
role,
expected_status,
):
client = APIClient()
_, token, alert_groups = alert_group_internal_api_setup
_, _, new_alert_group, _ = alert_groups
organization = new_alert_group.channel.organization
user = make_user_for_organization(organization, role)
client = APIClient()
url = reverse("api-internal:alertgroup-unresolve", kwargs={"pk": new_alert_group.public_primary_key})
with patch(
@ -776,19 +788,24 @@ def test_alert_group_unresolve_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_group_silence_permissions(
make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status
alert_group_internal_api_setup,
make_user_for_organization,
make_user_auth_headers,
role,
expected_status,
):
client = APIClient()
_, token, alert_groups = alert_group_internal_api_setup
_, _, new_alert_group, _ = alert_groups
organization = new_alert_group.channel.organization
user = make_user_for_organization(organization, role)
client = APIClient()
url = reverse("api-internal:alertgroup-silence", kwargs={"pk": new_alert_group.public_primary_key})
with patch(
@ -805,19 +822,24 @@ def test_alert_group_silence_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_group_unsilence_permissions(
make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status
alert_group_internal_api_setup,
make_user_for_organization,
make_user_auth_headers,
role,
expected_status,
):
client = APIClient()
_, token, alert_groups = alert_group_internal_api_setup
_, _, new_alert_group, _ = alert_groups
organization = new_alert_group.channel.organization
user = make_user_for_organization(organization, role)
client = APIClient()
url = reverse("api-internal:alertgroup-unsilence", kwargs={"pk": new_alert_group.public_primary_key})
with patch(
@ -834,19 +856,24 @@ def test_alert_group_unsilence_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_group_attach_permissions(
make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status
alert_group_internal_api_setup,
make_user_for_organization,
make_user_auth_headers,
role,
expected_status,
):
client = APIClient()
_, token, alert_groups = alert_group_internal_api_setup
_, _, new_alert_group, _ = alert_groups
organization = new_alert_group.channel.organization
user = make_user_for_organization(organization, role)
client = APIClient()
url = reverse("api-internal:alertgroup-attach", kwargs={"pk": new_alert_group.public_primary_key})
with patch(
@ -863,19 +890,24 @@ def test_alert_group_attach_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_group_unattach_permissions(
make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status
alert_group_internal_api_setup,
make_user_for_organization,
make_user_auth_headers,
role,
expected_status,
):
client = APIClient()
_, token, alert_groups = alert_group_internal_api_setup
_, _, new_alert_group, _ = alert_groups
organization = new_alert_group.channel.organization
user = make_user_for_organization(organization, role)
client = APIClient()
url = reverse("api-internal:alertgroup-unattach", kwargs={"pk": new_alert_group.public_primary_key})
with patch(
@ -892,19 +924,24 @@ def test_alert_group_unattach_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_alert_group_list_permissions(
make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status
alert_group_internal_api_setup,
make_user_for_organization,
make_user_auth_headers,
role,
expected_status,
):
client = APIClient()
_, token, alert_groups = alert_group_internal_api_setup
_, _, new_alert_group, _ = alert_groups
organization = new_alert_group.channel.organization
user = make_user_for_organization(organization, role)
client = APIClient()
url = reverse("api-internal:alertgroup-list")
with patch(
@ -921,19 +958,24 @@ def test_alert_group_list_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_alert_group_stats_permissions(
make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status
alert_group_internal_api_setup,
make_user_for_organization,
make_user_auth_headers,
role,
expected_status,
):
client = APIClient()
_, token, alert_groups = alert_group_internal_api_setup
_, _, new_alert_group, _ = alert_groups
organization = new_alert_group.channel.organization
user = make_user_for_organization(organization, role)
client = APIClient()
url = reverse("api-internal:alertgroup-stats")
with patch(
@ -950,19 +992,24 @@ def test_alert_group_stats_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_group_bulk_action_permissions(
make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status
alert_group_internal_api_setup,
make_user_for_organization,
make_user_auth_headers,
role,
expected_status,
):
client = APIClient()
_, token, alert_groups = alert_group_internal_api_setup
_, _, new_alert_group, _ = alert_groups
organization = new_alert_group.channel.organization
user = make_user_for_organization(organization, role)
client = APIClient()
url = reverse("api-internal:alertgroup-bulk-action")
with patch(
@ -977,19 +1024,24 @@ def test_alert_group_bulk_action_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_alert_group_filters_permissions(
make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status
alert_group_internal_api_setup,
make_user_for_organization,
make_user_auth_headers,
role,
expected_status,
):
client = APIClient()
_, token, alert_groups = alert_group_internal_api_setup
_, _, new_alert_group, _ = alert_groups
organization = new_alert_group.channel.organization
user = make_user_for_organization(organization, role)
client = APIClient()
url = reverse("api-internal:alertgroup-filters")
with patch(
@ -1006,19 +1058,24 @@ def test_alert_group_filters_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_alert_group_detail_permissions(
make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status
alert_group_internal_api_setup,
make_user_for_organization,
make_user_auth_headers,
role,
expected_status,
):
client = APIClient()
_, token, alert_groups = alert_group_internal_api_setup
_, _, new_alert_group, _ = alert_groups
organization = new_alert_group.channel.organization
user = make_user_for_organization(organization, role)
client = APIClient()
url = reverse("api-internal:alertgroup-detail", kwargs={"pk": new_alert_group.public_primary_key})
with patch(
@ -1032,10 +1089,7 @@ def test_alert_group_detail_permissions(
@pytest.mark.django_db
def test_silence(
alert_group_internal_api_setup,
make_user_auth_headers,
):
def test_silence(alert_group_internal_api_setup, make_user_auth_headers):
client = APIClient()
user, token, alert_groups = alert_group_internal_api_setup
_, _, new_alert_group, _ = alert_groups
@ -1396,9 +1450,9 @@ def test_alert_group_status_field(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_group_preview_template_permissions(
@ -1414,6 +1468,7 @@ def test_alert_group_preview_template_permissions(
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
make_alert(alert_group=alert_group, raw_request_data=alert_receive_channel.config.example_payload)
client = APIClient()
url = reverse("api-internal:alertgroup-preview-template", kwargs={"pk": alert_group.public_primary_key})
@ -1436,7 +1491,7 @@ def test_alert_group_preview_body_non_existent_template_var(
make_alert_group,
make_alert,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN)
organization, user, token = make_organization_and_user_with_plugin_token()
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
make_alert(alert_group=alert_group, raw_request_data=alert_receive_channel.config.example_payload)
@ -1446,8 +1501,9 @@ def test_alert_group_preview_body_non_existent_template_var(
data = {"template_name": "testonly_title_template", "template_body": "foobar: {{ foobar.does_not_exist }}"}
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
# Return errors as preview body instead of None
assert response.status_code == status.HTTP_200_OK
assert response.json()["preview"] is None
assert response.json()["preview"] == "Template Warning: &#x27;foobar&#x27; is undefined"
@pytest.mark.django_db
@ -1458,7 +1514,7 @@ def test_alert_group_preview_body_invalid_template_syntax(
make_alert_group,
make_alert,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN)
organization, user, token = make_organization_and_user_with_plugin_token()
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
make_alert(alert_group=alert_group, raw_request_data=alert_receive_channel.config.example_payload)
@ -1468,4 +1524,6 @@ def test_alert_group_preview_body_invalid_template_syntax(
data = {"template_name": "testonly_title_template", "template_body": "{{'' if foo is None else foo}}"}
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_400_BAD_REQUEST
# Errors now returned preview content
assert response.status_code == status.HTTP_200_OK
assert response.data["preview"] == "Template Error: No test named &#x27;None&#x27; found."

View file

@ -8,7 +8,7 @@ from rest_framework.response import Response
from rest_framework.test import APIClient
from apps.alerts.models import AlertReceiveChannel, EscalationPolicy
from common.constants.role import Role
from apps.api.permissions import LegacyAccessControlRole
@pytest.fixture()
@ -205,9 +205,9 @@ def test_integration_search(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_receive_channel_create_permissions(
@ -235,9 +235,9 @@ def test_alert_receive_channel_create_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_receive_channel_update_permissions(
@ -272,9 +272,9 @@ def test_alert_receive_channel_update_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_204_NO_CONTENT),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_receive_channel_delete_permissions(
@ -303,7 +303,11 @@ def test_alert_receive_channel_delete_permissions(
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)],
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_alert_receive_channel_list_permissions(
make_organization_and_user_with_plugin_token,
@ -311,7 +315,7 @@ def test_alert_receive_channel_list_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role)
_, user, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse("api-internal:alert_receive_channel-list")
@ -330,7 +334,11 @@ def test_alert_receive_channel_list_permissions(
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)],
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_alert_receive_channel_detail_permissions(
make_organization_and_user_with_plugin_token,
@ -360,9 +368,9 @@ def test_alert_receive_channel_detail_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_receive_channel_send_demo_alert_permissions(
@ -395,9 +403,9 @@ def test_alert_receive_channel_send_demo_alert_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_alert_receive_channel_integration_options_permissions(
@ -426,9 +434,9 @@ def test_alert_receive_channel_integration_options_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_receive_channel_preview_template_permissions(
@ -501,9 +509,9 @@ def test_alert_receive_channel_preview_template_require_notification_channel(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_receive_channel_change_team_permissions(
@ -597,9 +605,9 @@ def test_alert_receive_channel_change_team(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_alert_receive_channel_counters_permissions(
@ -608,7 +616,7 @@ def test_alert_receive_channel_counters_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role)
_, user, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse(
@ -630,9 +638,9 @@ def test_alert_receive_channel_counters_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_alert_receive_channel_counters_per_integration_permissions(

View file

@ -6,17 +6,17 @@ from rest_framework import status
from rest_framework.response import Response
from rest_framework.test import APIClient
from apps.api.permissions import LegacyAccessControlRole
from apps.base.messaging import BaseMessagingBackend
from common.constants.role import Role
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_receive_channel_template_update_permissions(
@ -48,9 +48,9 @@ def test_alert_receive_channel_template_update_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_alert_receive_channel_template_detail_permissions(
@ -83,7 +83,7 @@ def test_alert_receive_channel_template_include_additional_backend_templates(
make_user_auth_headers,
make_alert_receive_channel,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN)
organization, user, token = make_organization_and_user_with_plugin_token()
alert_receive_channel = make_alert_receive_channel(
organization,
messaging_backends_templates={"TESTONLY": {"title": "the-title", "message": "the-message", "image_url": "url"}},
@ -109,7 +109,7 @@ def test_alert_receive_channel_template_include_additional_backend_templates_usi
make_user_auth_headers,
make_alert_receive_channel,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN)
organization, user, token = make_organization_and_user_with_plugin_token()
alert_receive_channel = make_alert_receive_channel(organization, messaging_backends_templates=None)
client = APIClient()
@ -138,7 +138,7 @@ def test_update_alert_receive_channel_backend_template_invalid_template(
make_user_auth_headers,
make_alert_receive_channel,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN)
organization, user, token = make_organization_and_user_with_plugin_token()
alert_receive_channel = make_alert_receive_channel(organization, messaging_backends_templates=None)
client = APIClient()
@ -160,7 +160,7 @@ def test_update_alert_receive_channel_backend_template_invalid_url(
make_user_auth_headers,
make_alert_receive_channel,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN)
organization, user, token = make_organization_and_user_with_plugin_token()
alert_receive_channel = make_alert_receive_channel(organization, messaging_backends_templates=None)
client = APIClient()
@ -182,7 +182,7 @@ def test_update_alert_receive_channel_backend_template_empty_values_allowed(
make_user_auth_headers,
make_alert_receive_channel,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN)
organization, user, token = make_organization_and_user_with_plugin_token()
alert_receive_channel = make_alert_receive_channel(organization, messaging_backends_templates=None)
client = APIClient()
@ -208,7 +208,7 @@ def test_update_alert_receive_channel_backend_template_update_values(
make_user_auth_headers,
make_alert_receive_channel,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN)
organization, user, token = make_organization_and_user_with_plugin_token()
alert_receive_channel = make_alert_receive_channel(
organization,
messaging_backends_templates={
@ -249,7 +249,7 @@ def test_preview_alert_receive_channel_backend_templater(
make_alert_group,
make_alert,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN)
organization, user, token = make_organization_and_user_with_plugin_token()
alert_receive_channel = make_alert_receive_channel(organization)
default_channel_filter = make_channel_filter(alert_receive_channel, is_default=True)
alert_group = make_alert_group(alert_receive_channel, channel_filter=default_channel_filter)
@ -280,7 +280,7 @@ def test_update_alert_receive_channel_templates(
# set url here to pass *_url templates validation
return "https://grafana.com"
organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN)
organization, user, token = make_organization_and_user_with_plugin_token()
alert_receive_channel = make_alert_receive_channel(
organization,
messaging_backends_templates={"TESTONLY": {"title": "the-title", "message": "the-message", "image_url": "url"}},

View file

@ -6,21 +6,20 @@ from rest_framework import status
from rest_framework.response import Response
from rest_framework.test import APIClient
from common.constants.role import Role
from apps.api.permissions import LegacyAccessControlRole
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_channel_filter_create_permissions(
make_organization_and_user_with_plugin_token,
make_alert_receive_channel,
make_user_auth_headers,
role,
expected_status,
@ -45,9 +44,9 @@ def test_channel_filter_create_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_channel_filter_update_permissions(
@ -83,7 +82,11 @@ def test_channel_filter_update_permissions(
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)],
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_channel_filter_list_permissions(
make_organization_and_user_with_plugin_token,
@ -114,7 +117,11 @@ def test_channel_filter_list_permissions(
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)],
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_channel_filter_retrieve_permissions(
make_organization_and_user_with_plugin_token,
@ -146,9 +153,9 @@ def test_channel_filter_retrieve_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_204_NO_CONTENT),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_channel_filter_delete_permissions(
@ -181,9 +188,9 @@ def test_channel_filter_delete_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_channel_filter_move_to_position_permissions(
@ -216,9 +223,9 @@ def test_channel_filter_move_to_position_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_receive_channel_send_demo_alert_permissions(

View file

@ -8,7 +8,7 @@ from rest_framework.response import Response
from rest_framework.test import APIClient
from apps.alerts.models import CustomButton
from common.constants.role import Role
from apps.api.permissions import LegacyAccessControlRole
TEST_URL = "https://amixr.io"
@ -236,23 +236,7 @@ def test_create_invalid_data_custom_button(custom_button_internal_api_setup, mak
data = {
"name": "amixr_button_invalid_data",
"webhook": TEST_URL,
"data": "invalid_json",
}
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_400_BAD_REQUEST
@pytest.mark.django_db
def test_create_invalid_templated_data_custom_button(custom_button_internal_api_setup, make_user_auth_headers):
user, token, custom_button = custom_button_internal_api_setup
client = APIClient()
url = reverse("api-internal:custom_button-list")
data = {
"name": "amixr_button_invalid_data",
"webhook": TEST_URL,
# This would need a `| tojson` or some double quotes around it to pass validation.
"data": "{{ alert_payload.name }}",
"data": "{{%",
}
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_400_BAD_REQUEST
@ -291,14 +275,13 @@ def test_delete_custom_button(custom_button_internal_api_setup, make_user_auth_h
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_custom_button_create_permissions(
make_organization_and_user_with_plugin_token,
make_custom_action,
make_user_auth_headers,
role,
expected_status,
@ -323,9 +306,9 @@ def test_custom_button_create_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_custom_button_update_permissions(
@ -359,7 +342,11 @@ def test_custom_button_update_permissions(
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)],
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_custom_button_list_permissions(
make_organization_and_user_with_plugin_token,
@ -388,7 +375,11 @@ def test_custom_button_list_permissions(
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)],
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_custom_button_retrieve_permissions(
make_organization_and_user_with_plugin_token,
@ -418,9 +409,9 @@ def test_custom_button_retrieve_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_204_NO_CONTENT),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_custom_button_delete_permissions(

View file

@ -24,7 +24,7 @@ def test_delete_escalation_chain(escalation_chain_internal_api_setup, make_user_
@pytest.mark.django_db
def test_update_escalation_chain(escalation_chain_internal_api_setup, make_user_auth_headers, make_organization):
def test_update_escalation_chain(escalation_chain_internal_api_setup, make_user_auth_headers):
user, token, escalation_chain = escalation_chain_internal_api_setup
client = APIClient()
url = reverse("api-internal:escalation_chain-detail", kwargs={"pk": escalation_chain.public_primary_key})

View file

@ -9,7 +9,7 @@ from rest_framework.response import Response
from rest_framework.test import APIClient
from apps.alerts.models import EscalationPolicy
from common.constants.role import Role
from apps.api.permissions import LegacyAccessControlRole
@pytest.fixture()
@ -93,9 +93,9 @@ def test_move_to_position(escalation_policy_internal_api_setup, make_user_auth_h
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_escalation_policy_create_permissions(
@ -130,9 +130,9 @@ def test_escalation_policy_create_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_escalation_policy_update_permissions(
@ -171,9 +171,9 @@ def test_escalation_policy_update_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_escalation_policy_list_permissions(
@ -208,9 +208,9 @@ def test_escalation_policy_list_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_escalation_policy_retrieve_permissions(
@ -245,9 +245,9 @@ def test_escalation_policy_retrieve_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_204_NO_CONTENT),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_escalation_policy_delete_permissions(
@ -282,9 +282,9 @@ def test_escalation_policy_delete_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_escalation_policy_escalation_options_permissions(
@ -319,9 +319,9 @@ def test_escalation_policy_escalation_options_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_escalation_policy_delay_options_permissions(
@ -357,9 +357,9 @@ def test_escalation_policy_delay_options_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_escalation_policy_move_to_position_permissions(

View file

@ -3,16 +3,16 @@ from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from common.constants.role import Role
from apps.api.permissions import LegacyAccessControlRole
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_terraform_gitops_permissions(
@ -22,7 +22,7 @@ def test_terraform_gitops_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role)
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
make_escalation_chain(organization)
client = APIClient()
@ -38,15 +38,15 @@ def test_terraform_gitops_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_terraform_state_permissions(
make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status
):
_, user, token = make_organization_and_user_with_plugin_token(role)
_, user, token = make_organization_and_user_with_plugin_token(role=role)
client = APIClient()
url = reverse("api-internal:terraform_imports")

View file

@ -8,8 +8,8 @@ from rest_framework import status
from rest_framework.response import Response
from rest_framework.test import APIClient
from apps.api.permissions import LegacyAccessControlRole
from apps.heartbeat.models import IntegrationHeartBeat
from common.constants.role import Role
MOCK_LAST_HEARTBEAT_TIME_VERBAL = "a moment"
@ -151,7 +151,7 @@ def test_create_empty_alert_receive_channel_integration_heartbeat(
integration_heartbeat_internal_api_setup,
make_user_auth_headers,
):
user, token, alert_receive_channel, integration_heartbeat = integration_heartbeat_internal_api_setup
user, token, _, _ = integration_heartbeat_internal_api_setup
client = APIClient()
url = reverse("api-internal:integration_heartbeat-list")
@ -185,9 +185,39 @@ def test_update_integration_heartbeat(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_integration_heartbeat_create_permissions(
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
role,
expected_status,
):
_, user, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse("api-internal:integration_heartbeat-list")
with patch(
"apps.api.views.integration_heartbeat.IntegrationHeartBeatView.create",
return_value=Response(
status=status.HTTP_200_OK,
),
):
response = client.post(url, format="json", **make_user_auth_headers(user, token))
assert response.status_code == expected_status
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_integration_heartbeat_update_permissions(
@ -223,7 +253,11 @@ def test_integration_heartbeat_update_permissions(
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)],
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_integration_heartbeat_list_permissions(
make_organization_and_user_with_plugin_token,
@ -255,9 +289,40 @@ def test_integration_heartbeat_list_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_integration_heartbeat_timeout_options_permissions(
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
role,
expected_status,
):
_, user, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse("api-internal:integration_heartbeat-timeout-options")
with patch(
"apps.api.views.integration_heartbeat.IntegrationHeartBeatView.timeout_options",
return_value=Response(
status=status.HTTP_200_OK,
),
):
response = client.get(url, format="json", **make_user_auth_headers(user, token))
assert response.status_code == expected_status
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_integration_heartbeat_retrieve_permissions(

View file

@ -6,6 +6,8 @@ from rest_framework.test import APIClient
from apps.alerts.models import AlertReceiveChannel
from apps.user_management.models import Organization
# TODO: should probably modify these tests to take into account new rbac permissions
@pytest.fixture()
def maintenance_internal_api_setup(
@ -23,7 +25,7 @@ def maintenance_internal_api_setup(
def test_start_maintenance_integration(
maintenance_internal_api_setup, mock_start_disable_maintenance_task, make_user_auth_headers
):
token, organization, user, alert_receive_channel = maintenance_internal_api_setup
token, _, user, alert_receive_channel = maintenance_internal_api_setup
client = APIClient()
url = reverse("api-internal:start_maintenance")
@ -50,7 +52,7 @@ def test_stop_maintenance_integration(
mock_start_disable_maintenance_task,
make_user_auth_headers,
):
token, organization, user, alert_receive_channel = maintenance_internal_api_setup
token, _, user, alert_receive_channel = maintenance_internal_api_setup
client = APIClient()
mode = AlertReceiveChannel.MAINTENANCE
duration = AlertReceiveChannel.DURATION_ONE_HOUR.seconds
@ -161,7 +163,7 @@ def test_maintenances_list(
def test_empty_maintenances_list(
maintenance_internal_api_setup, mock_start_disable_maintenance_task, make_user_auth_headers
):
token, organization, user, alert_receive_channel = maintenance_internal_api_setup
token, _, user, alert_receive_channel = maintenance_internal_api_setup
client = APIClient()
url = reverse("api-internal:maintenance")
response = client.get(url, format="json", **make_user_auth_headers(user, token))

View file

@ -7,8 +7,8 @@ from rest_framework import status
from rest_framework.response import Response
from rest_framework.test import APIClient
from apps.api.permissions import LegacyAccessControlRole
from apps.schedules.models import CustomOnCallShift, OnCallSchedule, OnCallScheduleWeb
from common.constants.role import Role
@pytest.fixture()
@ -26,7 +26,7 @@ def on_call_shift_internal_api_setup(
@pytest.mark.django_db
def test_create_on_call_shift_rotation(on_call_shift_internal_api_setup, make_user_auth_headers):
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, user2, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
url = reverse("api-internal:oncall_shifts-list")
start_date = timezone.now().replace(microsecond=0, tzinfo=None)
@ -58,7 +58,7 @@ def test_create_on_call_shift_rotation(on_call_shift_internal_api_setup, make_us
@pytest.mark.django_db
def test_create_on_call_shift_override(on_call_shift_internal_api_setup, make_user_auth_headers):
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, user2, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
url = reverse("api-internal:oncall_shifts-list")
start_date = timezone.now().replace(microsecond=0, tzinfo=None)
@ -98,7 +98,7 @@ def test_get_on_call_shift(
make_on_call_shift,
make_user_auth_headers,
):
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, user2, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
start_date = timezone.now().replace(microsecond=0)
@ -144,7 +144,7 @@ def test_list_on_call_shift(
make_on_call_shift,
make_user_auth_headers,
):
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, user2, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
start_date = timezone.now().replace(microsecond=0)
@ -270,7 +270,7 @@ def test_update_future_on_call_shift(
make_user_auth_headers,
):
"""Test updating the shift that has not started (rotation_start > now)"""
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, _, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
start_date = (timezone.now() + timezone.timedelta(days=1)).replace(microsecond=0)
@ -337,7 +337,7 @@ def test_update_started_on_call_shift(
):
"""Test updating the shift that has started (rotation_start < now)"""
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, _, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
start_date = (timezone.now() - timezone.timedelta(hours=1)).replace(microsecond=0)
@ -409,7 +409,7 @@ def test_update_old_on_call_shift_with_future_version(
make_user_auth_headers,
):
"""Test updating the shift that has the newer version (updated_shift is not None)"""
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, _, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
now = timezone.now().replace(microsecond=0)
@ -498,7 +498,7 @@ def test_update_started_on_call_shift_title(
):
"""Test updating the title for the shift that has started (rotation_start < now)"""
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, _, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
start_date = (timezone.now() - timezone.timedelta(hours=1)).replace(microsecond=0)
@ -560,7 +560,7 @@ def test_delete_started_on_call_shift(
):
"""Test deleting the shift that has started (rotation_start < now)"""
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, _, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
start_date = (timezone.now() - timezone.timedelta(hours=1)).replace(microsecond=0)
@ -598,7 +598,7 @@ def test_delete_future_on_call_shift(
):
"""Test deleting the shift that has not started (rotation_start > now)"""
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, _, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
start_date = (timezone.now() + timezone.timedelta(days=1)).replace(microsecond=0)
@ -631,7 +631,7 @@ def test_create_on_call_shift_invalid_data_rotation_start(
on_call_shift_internal_api_setup,
make_user_auth_headers,
):
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, _, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
url = reverse("api-internal:oncall_shifts-list")
start_date = timezone.now().replace(microsecond=0, tzinfo=None)
@ -660,7 +660,7 @@ def test_create_on_call_shift_invalid_data_rotation_start(
@pytest.mark.django_db
def test_create_on_call_shift_invalid_data_until(on_call_shift_internal_api_setup, make_user_auth_headers):
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, user2, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
url = reverse("api-internal:oncall_shifts-list")
start_date = timezone.now().replace(microsecond=0, tzinfo=None)
@ -713,7 +713,7 @@ def test_create_on_call_shift_invalid_data_until(on_call_shift_internal_api_setu
@pytest.mark.django_db
def test_create_on_call_shift_invalid_data_by_day(on_call_shift_internal_api_setup, make_user_auth_headers):
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, _, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
url = reverse("api-internal:oncall_shifts-list")
start_date = timezone.now().replace(microsecond=0, tzinfo=None)
@ -763,7 +763,7 @@ def test_create_on_call_shift_invalid_data_by_day(on_call_shift_internal_api_set
@pytest.mark.django_db
def test_create_on_call_shift_invalid_data_interval(on_call_shift_internal_api_setup, make_user_auth_headers):
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, _, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
url = reverse("api-internal:oncall_shifts-list")
start_date = timezone.now().replace(microsecond=0, tzinfo=None)
@ -813,7 +813,7 @@ def test_create_on_call_shift_invalid_data_interval(on_call_shift_internal_api_s
@pytest.mark.django_db
def test_create_on_call_shift_invalid_data_shift_end(on_call_shift_internal_api_setup, make_user_auth_headers):
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, _, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
url = reverse("api-internal:oncall_shifts-list")
start_date = timezone.now().replace(microsecond=0, tzinfo=None)
@ -866,7 +866,7 @@ def test_create_on_call_shift_invalid_data_rolling_users(
on_call_shift_internal_api_setup,
make_user_auth_headers,
):
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, user2, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
url = reverse("api-internal:oncall_shifts-list")
start_date = timezone.now().replace(microsecond=0, tzinfo=None)
@ -894,7 +894,7 @@ def test_create_on_call_shift_invalid_data_rolling_users(
@pytest.mark.django_db
def test_create_on_call_shift_override_invalid_data(on_call_shift_internal_api_setup, make_user_auth_headers):
token, user1, user2, organization, schedule = on_call_shift_internal_api_setup
token, user1, _, _, schedule = on_call_shift_internal_api_setup
client = APIClient()
url = reverse("api-internal:oncall_shifts-list")
start_date = timezone.now().replace(microsecond=0, tzinfo=None)
@ -925,9 +925,9 @@ def test_create_on_call_shift_override_invalid_data(on_call_shift_internal_api_s
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_201_CREATED),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_201_CREATED),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_on_call_shift_create_permissions(
@ -936,7 +936,7 @@ def test_on_call_shift_create_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role)
_, user, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
@ -957,9 +957,9 @@ def test_on_call_shift_create_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_on_call_shift_update_permissions(
@ -1005,9 +1005,9 @@ def test_on_call_shift_update_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_on_call_shift_list_permissions(
@ -1016,7 +1016,7 @@ def test_on_call_shift_list_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role)
_, user, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse("api-internal:oncall_shifts-list")
@ -1036,9 +1036,9 @@ def test_on_call_shift_list_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_on_call_shift_retrieve_permissions(
@ -1079,9 +1079,9 @@ def test_on_call_shift_retrieve_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_204_NO_CONTENT),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_on_call_shift_delete_permissions(
@ -1122,9 +1122,9 @@ def test_on_call_shift_delete_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_on_call_shift_frequency_options_permissions(
@ -1153,9 +1153,9 @@ def test_on_call_shift_frequency_options_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_on_call_shift_days_options_permissions(
@ -1184,9 +1184,9 @@ def test_on_call_shift_days_options_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_on_call_shift_preview_permissions(

View file

@ -6,30 +6,25 @@ from rest_framework import status
from rest_framework.response import Response
from rest_framework.test import APIClient
from common.constants.role import Role
from apps.api.permissions import LegacyAccessControlRole
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_current_team_retrieve_permissions(
make_organization,
make_user_for_organization,
make_token_for_organization,
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
role,
expected_status,
):
org = make_organization()
tester = make_user_for_organization(org, role=role)
_, token = make_token_for_organization(org)
_, tester, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse("api-internal:api-current-team")
@ -48,23 +43,18 @@ def test_current_team_retrieve_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_current_team_update_permissions(
make_organization,
make_user_for_organization,
make_token_for_organization,
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
role,
expected_status,
):
org = make_organization()
tester = make_user_for_organization(org, role=role)
_, token = make_token_for_organization(org)
_, tester, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse("api-internal:api-current-team")
@ -84,9 +74,9 @@ def test_current_team_update_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_current_team_get_telegram_verification_code_permissions(
@ -95,8 +85,7 @@ def test_current_team_get_telegram_verification_code_permissions(
role,
expected_status,
):
organization, tester, token = make_organization_and_user_with_plugin_token(role)
_, tester, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse("api-internal:api-get-telegram-verification-code")
@ -109,9 +98,9 @@ def test_current_team_get_telegram_verification_code_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_current_team_get_channel_verification_code_permissions(
@ -120,8 +109,7 @@ def test_current_team_get_channel_verification_code_permissions(
role,
expected_status,
):
organization, tester, token = make_organization_and_user_with_plugin_token(role)
_, tester, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse("api-internal:api-get-channel-verification-code") + "?backend=TESTONLY"
@ -135,8 +123,7 @@ def test_current_team_get_channel_verification_code_ok(
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
):
organization, tester, token = make_organization_and_user_with_plugin_token(Role.ADMIN)
organization, tester, token = make_organization_and_user_with_plugin_token()
client = APIClient()
url = reverse("api-internal:api-get-channel-verification-code") + "?backend=TESTONLY"
@ -156,8 +143,7 @@ def test_current_team_get_channel_verification_code_invalid(
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
):
organization, tester, token = make_organization_and_user_with_plugin_token(Role.ADMIN)
_, tester, token = make_organization_and_user_with_plugin_token()
client = APIClient()
url = reverse("api-internal:api-get-channel-verification-code") + "?backend=INVALID"

View file

@ -7,7 +7,7 @@ from rest_framework.response import Response
from rest_framework.test import APIClient
from apps.alerts.models import ResolutionNote
from common.constants.role import Role
from apps.api.permissions import LegacyAccessControlRole
@pytest.mark.django_db
@ -212,9 +212,9 @@ def test_delete_resolution_note(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_resolution_note_create_permissions(
@ -224,7 +224,7 @@ def test_resolution_note_create_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
_, user, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse("api-internal:resolution_note-list")
@ -245,9 +245,9 @@ def test_resolution_note_create_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_resolution_note_update_permissions(
@ -260,7 +260,7 @@ def test_resolution_note_update_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
resolution_note = make_resolution_note(
@ -289,9 +289,9 @@ def test_resolution_note_update_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_204_NO_CONTENT),
(Role.EDITOR, status.HTTP_204_NO_CONTENT),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_resolution_note_delete_permissions(
@ -304,7 +304,7 @@ def test_resolution_note_delete_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
resolution_note = make_resolution_note(
@ -331,9 +331,9 @@ def test_resolution_note_delete_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_resolution_note_list_permissions(
@ -343,7 +343,7 @@ def test_resolution_note_list_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
_, user, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse("api-internal:resolution_note-list")
@ -363,9 +363,9 @@ def test_resolution_note_list_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_resolution_note_detail_permissions(
@ -378,7 +378,7 @@ def test_resolution_note_detail_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
resolution_note = make_resolution_note(

View file

@ -0,0 +1,115 @@
import pytest
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from apps.api.permissions import LegacyAccessControlRole
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_public_api_tokens_retrieve_permissions(
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
make_public_api_token,
role,
expected_status,
):
organization, user, plugin_token = make_organization_and_user_with_plugin_token(role)
api_token, _ = make_public_api_token(user, organization)
client = APIClient()
url = reverse("api-internal:api_token-detail", kwargs={"pk": api_token.id})
response = client.get(url, format="json", **make_user_auth_headers(user, plugin_token))
assert response.status_code == expected_status
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_public_api_tokens_list_permissions(
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
make_public_api_token,
role,
expected_status,
):
organization, user, plugin_token = make_organization_and_user_with_plugin_token(role)
make_public_api_token(user, organization)
client = APIClient()
url = reverse("api-internal:api_token-list")
response = client.get(url, format="json", **make_user_auth_headers(user, plugin_token))
assert response.status_code == expected_status
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_201_CREATED),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_public_api_tokens_create_permissions(
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
role,
expected_status,
):
_, user, plugin_token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse("api-internal:api_token-list")
response = client.post(
url,
data={
"name": "helloooo",
},
format="json",
**make_user_auth_headers(user, plugin_token),
)
assert response.status_code == expected_status
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_public_api_tokens_delete_permissions(
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
make_public_api_token,
role,
expected_status,
):
organization, user, plugin_token = make_organization_and_user_with_plugin_token(role)
api_token, _ = make_public_api_token(user, organization)
client = APIClient()
url = reverse("api-internal:api_token-detail", kwargs={"pk": api_token.id})
response = client.delete(url, format="json", **make_user_auth_headers(user, plugin_token))
assert response.status_code == expected_status

View file

@ -3,9 +3,9 @@ from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from apps.api.permissions import LegacyAccessControlRole
from apps.auth_token.models import ScheduleExportAuthToken
from apps.schedules.models import OnCallScheduleICal
from common.constants.role import Role
ICAL_URL = "https://calendar.google.com/calendar/ical/amixr.io_37gttuakhrtr75ano72p69rt78%40group.calendar.google.com/private-1d00a680ba5be7426c3eb3ef1616e26d/basic.ics" # noqa
@ -14,9 +14,9 @@ ICAL_URL = "https://calendar.google.com/calendar/ical/amixr.io_37gttuakhrtr75ano
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_get_schedule_export_token(
@ -26,8 +26,7 @@ def test_get_schedule_export_token(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleICal,
@ -50,9 +49,9 @@ def test_get_schedule_export_token(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_404_NOT_FOUND),
(Role.EDITOR, status.HTTP_404_NOT_FOUND),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_404_NOT_FOUND),
(LegacyAccessControlRole.EDITOR, status.HTTP_404_NOT_FOUND),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_schedule_export_token_not_found(
@ -62,8 +61,7 @@ def test_schedule_export_token_not_found(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleICal,
@ -84,9 +82,9 @@ def test_schedule_export_token_not_found(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_201_CREATED),
(Role.EDITOR, status.HTTP_201_CREATED),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_201_CREATED),
(LegacyAccessControlRole.EDITOR, status.HTTP_201_CREATED),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_schedule_create_export_token(
@ -96,8 +94,7 @@ def test_schedule_create_export_token(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleICal,
@ -118,9 +115,9 @@ def test_schedule_create_export_token(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_204_NO_CONTENT),
(Role.EDITOR, status.HTTP_204_NO_CONTENT),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_schedule_delete_export_token(
@ -130,8 +127,7 @@ def test_schedule_delete_export_token(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleICal,

View file

@ -10,6 +10,7 @@ from rest_framework.serializers import ValidationError
from rest_framework.test import APIClient
from apps.alerts.models import EscalationPolicy
from apps.api.permissions import LegacyAccessControlRole
from apps.schedules.ical_utils import memoized_users_in_ical
from apps.schedules.models import (
CustomOnCallShift,
@ -18,7 +19,6 @@ from apps.schedules.models import (
OnCallScheduleICal,
OnCallScheduleWeb,
)
from common.constants.role import Role
ICAL_URL = "https://calendar.google.com/calendar/ical/amixr.io_37gttuakhrtr75ano72p69rt78%40group.calendar.google.com/private-1d00a680ba5be7426c3eb3ef1616e26d/basic.ics"
@ -405,7 +405,7 @@ def test_create_web_schedule(schedule_internal_api_setup, make_user_auth_headers
@pytest.mark.django_db
def test_create_invalid_ical_schedule(schedule_internal_api_setup, make_user_auth_headers):
user, token, _, ical_schedule, _, _ = schedule_internal_api_setup
user, token, _, _, _, _ = schedule_internal_api_setup
client = APIClient()
url = reverse("api-internal:custom_button-list")
with patch(
@ -422,6 +422,33 @@ def test_create_invalid_ical_schedule(schedule_internal_api_setup, make_user_aut
assert response.status_code == status.HTTP_400_BAD_REQUEST
@pytest.mark.django_db
@pytest.mark.parametrize("calendar_type", [0, 2])
def test_create_schedule_invalid_time_zone(schedule_internal_api_setup, make_user_auth_headers, calendar_type):
user, token, _, _, _, _ = schedule_internal_api_setup
client = APIClient()
url = reverse("api-internal:schedule-list")
data = {
"name": "created_web_schedule",
"type": calendar_type,
"time_zone": "asdfasdfasdf",
"slack_channel_id": None,
"user_group": None,
"team": None,
"warnings": [],
"on_call_now": [],
"has_gaps": False,
"mention_oncall_next": False,
"mention_oncall_start": True,
"notify_empty_oncall": 0,
"notify_oncall_shift_freq": 1,
}
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json() == {"time_zone": ["Invalid timezone"]}
@pytest.mark.django_db
def test_update_calendar_schedule(schedule_internal_api_setup, make_user_auth_headers):
user, token, calendar_schedule, _, _, _ = schedule_internal_api_setup
@ -479,6 +506,24 @@ def test_update_web_schedule(schedule_internal_api_setup, make_user_auth_headers
assert updated_instance.name == "updated_web_schedule"
@pytest.mark.django_db
@pytest.mark.parametrize("calendar_type", [0, 2])
def test_update_schedule_invalid_time_zone(schedule_internal_api_setup, make_user_auth_headers, calendar_type):
user, token, *calendars, _ = schedule_internal_api_setup
schedule = calendars[calendar_type]
client = APIClient()
url = reverse("api-internal:schedule-detail", kwargs={"pk": schedule.public_primary_key})
data = {"name": "updated_web_schedule", "type": calendar_type, "time_zone": "asdfasdfasdf"}
response = client.put(
url, data=json.dumps(data), content_type="application/json", **make_user_auth_headers(user, token)
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json() == {"time_zone": ["Invalid timezone"]}
@pytest.mark.django_db
def test_delete_schedule(schedule_internal_api_setup, make_user_auth_headers):
user, token, calendar_schedule, ical_schedule, _, _ = schedule_internal_api_setup
@ -1062,7 +1107,7 @@ def test_merging_same_shift_events(
user_a = make_user_for_organization(organization)
user_b = make_user_for_organization(organization)
user_c = make_user_for_organization(organization, role=Role.VIEWER)
user_c = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER)
# clear users pks <-> organization cache (persisting between tests)
memoized_users_in_ical.cache_clear()
@ -1158,9 +1203,9 @@ def test_filter_events_invalid_type(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_schedule_create_permissions(
@ -1170,7 +1215,7 @@ def test_schedule_create_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
make_schedule(
organization,
schedule_class=OnCallScheduleICal,
@ -1196,9 +1241,9 @@ def test_schedule_create_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_schedule_update_permissions(
@ -1208,7 +1253,7 @@ def test_schedule_update_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleICal,
@ -1237,7 +1282,11 @@ def test_schedule_update_permissions(
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)],
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_schedule_list_permissions(
make_organization_and_user_with_plugin_token,
@ -1246,7 +1295,7 @@ def test_schedule_list_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
make_schedule(
organization,
schedule_class=OnCallScheduleICal,
@ -1271,7 +1320,11 @@ def test_schedule_list_permissions(
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)],
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_schedule_retrieve_permissions(
make_organization_and_user_with_plugin_token,
@ -1280,7 +1333,7 @@ def test_schedule_retrieve_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleICal,
@ -1306,9 +1359,9 @@ def test_schedule_retrieve_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_204_NO_CONTENT),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_schedule_delete_permissions(
@ -1318,7 +1371,7 @@ def test_schedule_delete_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleICal,
@ -1344,9 +1397,9 @@ def test_schedule_delete_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_events_permissions(
@ -1356,7 +1409,7 @@ def test_events_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleICal,
@ -1382,9 +1435,9 @@ def test_events_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_reload_ical_permissions(
@ -1394,7 +1447,7 @@ def test_reload_ical_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleICal,
@ -1420,9 +1473,9 @@ def test_reload_ical_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_schedule_notify_oncall_shift_freq_options_permissions(
@ -1432,7 +1485,7 @@ def test_schedule_notify_oncall_shift_freq_options_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
_, user, token = make_organization_and_user_with_plugin_token(role)
url = reverse("api-internal:schedule-notify-oncall-shift-freq-options")
client = APIClient()
response = client.get(url, format="json", **make_user_auth_headers(user, token))
@ -1444,9 +1497,9 @@ def test_schedule_notify_oncall_shift_freq_options_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_schedule_notify_empty_oncall_options_permissions(
@ -1456,7 +1509,7 @@ def test_schedule_notify_empty_oncall_options_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
_, user, token = make_organization_and_user_with_plugin_token(role)
url = reverse("api-internal:schedule-notify-empty-oncall-options")
client = APIClient()
response = client.get(url, format="json", **make_user_auth_headers(user, token))
@ -1468,9 +1521,9 @@ def test_schedule_notify_empty_oncall_options_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_schedule_mention_options_permissions(
@ -1480,7 +1533,7 @@ def test_schedule_mention_options_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
_, user, token = make_organization_and_user_with_plugin_token(role)
url = reverse("api-internal:schedule-mention-options")
client = APIClient()
response = client.get(url, format="json", **make_user_auth_headers(user, token))

View file

@ -6,7 +6,7 @@ from rest_framework import status
from rest_framework.response import Response
from rest_framework.test import APIClient
from common.constants.role import Role
from apps.api.permissions import LegacyAccessControlRole
# Testing permissions, not view itself. So mock is ok here
@ -14,13 +14,16 @@ from common.constants.role import Role
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_set_general_log_channel_permissions(
make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
role,
expected_status,
):
_, user, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()

View file

@ -6,20 +6,23 @@ from rest_framework import status
from rest_framework.response import Response
from rest_framework.test import APIClient
from common.constants.role import Role
from apps.api.permissions import LegacyAccessControlRole
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_slack_channels_list_permissions(
make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
role,
expected_status,
):
_, user, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
@ -40,13 +43,17 @@ def test_slack_channels_list_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_slack_channels_detail_permissions(
make_organization_and_user_with_plugin_token, make_user_auth_headers, role, make_slack_channel, expected_status
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
make_slack_channel,
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role)
slack_channel = make_slack_channel(organization.slack_team_identity)

View file

@ -6,16 +6,16 @@ from rest_framework import status
from rest_framework.response import Response
from rest_framework.test import APIClient
from common.constants.role import Role
from apps.api.permissions import LegacyAccessControlRole
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_get_slack_settings_permissions(
@ -24,7 +24,7 @@ def test_get_slack_settings_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
_, user, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse("api-internal:slack-settings")
@ -43,9 +43,9 @@ def test_get_slack_settings_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_update_slack_settings_permissions(
@ -54,7 +54,7 @@ def test_update_slack_settings_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
_, user, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse("api-internal:slack-settings")
@ -73,9 +73,9 @@ def test_update_slack_settings_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_get_acknowledge_remind_options_permissions(
@ -84,7 +84,7 @@ def test_get_acknowledge_remind_options_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
_, user, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse("api-internal:acknowledge-reminder-options")
@ -103,9 +103,9 @@ def test_get_acknowledge_remind_options_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_get_unacknowledge_timeout_options_permissions(
@ -114,7 +114,7 @@ def test_get_unacknowledge_timeout_options_permissions(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
_, user, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse("api-internal:unacknowledge-timeout-options")

View file

@ -6,16 +6,16 @@ from rest_framework import status
from rest_framework.response import Response
from rest_framework.test import APIClient
from common.constants.role import Role
from apps.api.permissions import LegacyAccessControlRole
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_subscription_retrieve_permissions(

View file

@ -3,9 +3,9 @@ from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from apps.api.permissions import LegacyAccessControlRole
from apps.schedules.models import OnCallScheduleCalendar
from apps.user_management.models import Team
from common.constants.role import Role
GENERAL_TEAM = Team(public_primary_key=None, name="General", email=None, avatar_url=None)
@ -64,28 +64,24 @@ def test_list_teams_for_non_member(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_list_teams_permissions(
make_organization,
make_token_for_organization,
make_user_for_organization,
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
role,
expected_status,
):
organization = make_organization()
_, token = make_token_for_organization(organization)
user = make_user_for_organization(organization, role=role)
_, user, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()
url = reverse("api-internal:team-list")
response = client.get(url, format="json", **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_200_OK
assert response.status_code == expected_status
@pytest.mark.django_db

View file

@ -3,14 +3,14 @@ from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from common.constants.role import Role
from apps.api.permissions import LegacyAccessControlRole
@pytest.mark.django_db
def test_not_authorized(make_organization_and_user_with_plugin_token, make_telegram_channel):
client = APIClient()
organization, user, _ = make_organization_and_user_with_plugin_token()
organization, _, _ = make_organization_and_user_with_plugin_token()
telegram_channel = make_telegram_channel(organization=organization)
url = reverse("api-internal:telegram_channel-list")
@ -34,9 +34,9 @@ def test_not_authorized(make_organization_and_user_with_plugin_token, make_teleg
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_list_telegram_channels_permissions(
@ -46,8 +46,7 @@ def test_list_telegram_channels_permissions(
expected_status,
):
client = APIClient()
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
_, user, token = make_organization_and_user_with_plugin_token(role)
url = reverse("api-internal:telegram_channel-list")
response = client.get(url, **make_user_auth_headers(user, token))
@ -59,9 +58,9 @@ def test_list_telegram_channels_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_get_telegram_channels_permissions(
@ -72,8 +71,7 @@ def test_get_telegram_channels_permissions(
expected_status,
):
client = APIClient()
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
telegram_channel = make_telegram_channel(organization=organization)
url = reverse("api-internal:telegram_channel-detail", kwargs={"pk": telegram_channel.public_primary_key})
@ -86,9 +84,9 @@ def test_get_telegram_channels_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_204_NO_CONTENT),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_delete_telegram_channels_permissions(
@ -100,7 +98,7 @@ def test_delete_telegram_channels_permissions(
):
client = APIClient()
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
telegram_channel = make_telegram_channel(organization=organization)
url = reverse("api-internal:telegram_channel-detail", kwargs={"pk": telegram_channel.public_primary_key})
@ -113,9 +111,9 @@ def test_delete_telegram_channels_permissions(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_403_FORBIDDEN),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_set_default_telegram_channels_permissions(
@ -127,8 +125,7 @@ def test_set_default_telegram_channels_permissions(
):
client = APIClient()
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
telegram_channel = make_telegram_channel(organization=organization)
url = reverse("api-internal:telegram_channel-set-default", kwargs={"pk": telegram_channel.public_primary_key})

View file

@ -18,7 +18,7 @@ def test_get_terraform_file(
@pytest.mark.django_db
def test_get_terraform_imports(make_organization_and_user_with_plugin_token, make_user_auth_headers):
organization, user, token = make_organization_and_user_with_plugin_token()
_, user, token = make_organization_and_user_with_plugin_token()
client = APIClient()
url = reverse("api-internal:terraform_imports")
response = client.get(url, format="text/plain", **make_user_auth_headers(user, token))

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from common.constants.role import Role
from apps.api.permissions import LegacyAccessControlRole
@pytest.mark.django_db
@ -52,13 +52,16 @@ def test_usergroup_list_without_slack_installed(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_200_OK),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_usergroup_permissions(
make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
role,
expected_status,
):
_, user, token = make_organization_and_user_with_plugin_token(role)
client = APIClient()

View file

@ -6,8 +6,8 @@ from django.utils import timezone
from rest_framework import status
from rest_framework.test import APIClient
from apps.api.permissions import LegacyAccessControlRole
from apps.base.models import UserNotificationPolicy
from common.constants.role import Role
DEFAULT_NOTIFICATION_CHANNEL = UserNotificationPolicy.NotificationChannel.SLACK
@ -17,7 +17,7 @@ def user_notification_policy_internal_api_setup(
make_organization_and_user_with_plugin_token, make_user_for_organization, make_user_notification_policy
):
organization, admin, token = make_organization_and_user_with_plugin_token()
user = make_user_for_organization(organization, Role.EDITOR)
user = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR)
wait_notification_step = make_user_notification_policy(
admin, UserNotificationPolicy.Step.WAIT, wait_delay=timezone.timedelta(minutes=15), important=False
@ -49,7 +49,7 @@ def user_notification_policy_internal_api_setup(
@pytest.mark.django_db
def test_create_notification_policy(user_notification_policy_internal_api_setup, make_user_auth_headers):
token, steps, users = user_notification_policy_internal_api_setup
token, _, users = user_notification_policy_internal_api_setup
admin, _ = users
client = APIClient()
url = reverse("api-internal:notification_policy-list")
@ -69,7 +69,7 @@ def test_create_notification_policy(user_notification_policy_internal_api_setup,
def test_admin_can_create_notification_policy_for_user(
user_notification_policy_internal_api_setup, make_user_auth_headers
):
token, steps, users = user_notification_policy_internal_api_setup
token, _, users = user_notification_policy_internal_api_setup
admin, user = users
client = APIClient()
url = reverse("api-internal:notification_policy-list")

View file

@ -3,8 +3,8 @@ from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from apps.api.permissions import LegacyAccessControlRole
from apps.auth_token.models import UserScheduleExportAuthToken
from common.constants.role import Role
ICAL_URL = "https://calendar.google.com/calendar/ical/amixr.io_37gttuakhrtr75ano72p69rt78%40group.calendar.google.com/private-1d00a680ba5be7426c3eb3ef1616e26d/basic.ics" # noqa
@ -13,9 +13,9 @@ ICAL_URL = "https://calendar.google.com/calendar/ical/amixr.io_37gttuakhrtr75ano
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_200_OK),
(Role.EDITOR, status.HTTP_200_OK),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_get_user_schedule_export_token(
@ -24,8 +24,7 @@ def test_get_user_schedule_export_token(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
UserScheduleExportAuthToken.create_auth_token(
user=user,
@ -45,9 +44,9 @@ def test_get_user_schedule_export_token(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_404_NOT_FOUND),
(Role.EDITOR, status.HTTP_404_NOT_FOUND),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_404_NOT_FOUND),
(LegacyAccessControlRole.EDITOR, status.HTTP_404_NOT_FOUND),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_user_schedule_export_token_not_found(
@ -56,8 +55,7 @@ def test_user_schedule_export_token_not_found(
role,
expected_status,
):
_, user, token = make_organization_and_user_with_plugin_token(role=role)
_, user, token = make_organization_and_user_with_plugin_token(role)
url = reverse("api-internal:user-export-token", kwargs={"pk": user.public_primary_key})
@ -72,9 +70,9 @@ def test_user_schedule_export_token_not_found(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_201_CREATED),
(Role.EDITOR, status.HTTP_201_CREATED),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_201_CREATED),
(LegacyAccessControlRole.EDITOR, status.HTTP_201_CREATED),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_user_schedule_create_export_token(
@ -83,8 +81,7 @@ def test_user_schedule_create_export_token(
role,
expected_status,
):
_, user, token = make_organization_and_user_with_plugin_token(role=role)
_, user, token = make_organization_and_user_with_plugin_token(role)
url = reverse("api-internal:user-export-token", kwargs={"pk": user.public_primary_key})
@ -99,9 +96,9 @@ def test_user_schedule_create_export_token(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_409_CONFLICT),
(Role.EDITOR, status.HTTP_409_CONFLICT),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_409_CONFLICT),
(LegacyAccessControlRole.EDITOR, status.HTTP_409_CONFLICT),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_user_schedule_create_multiple_export_tokens_fails(
@ -110,8 +107,7 @@ def test_user_schedule_create_multiple_export_tokens_fails(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
UserScheduleExportAuthToken.create_auth_token(
user=user,
@ -131,9 +127,9 @@ def test_user_schedule_create_multiple_export_tokens_fails(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_204_NO_CONTENT),
(Role.EDITOR, status.HTTP_204_NO_CONTENT),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_user_schedule_delete_export_token(
@ -142,8 +138,7 @@ def test_user_schedule_delete_export_token(
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
organization, user, token = make_organization_and_user_with_plugin_token(role)
instance, _ = UserScheduleExportAuthToken.create_auth_token(
user=user,
@ -168,9 +163,9 @@ def test_user_schedule_delete_export_token(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_404_NOT_FOUND),
(Role.EDITOR, status.HTTP_404_NOT_FOUND),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_404_NOT_FOUND),
(LegacyAccessControlRole.EDITOR, status.HTTP_404_NOT_FOUND),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_user_cannot_get_another_users_schedule_token(
@ -179,9 +174,8 @@ def test_user_cannot_get_another_users_schedule_token(
role,
expected_status,
):
organization1, user1, _ = make_organization_and_user_with_plugin_token(role=role)
_, user2, token2 = make_organization_and_user_with_plugin_token(role=role)
organization1, user1, _ = make_organization_and_user_with_plugin_token(role)
_, user2, token2 = make_organization_and_user_with_plugin_token(role)
UserScheduleExportAuthToken.create_auth_token(
user=user1,
@ -201,9 +195,9 @@ def test_user_cannot_get_another_users_schedule_token(
@pytest.mark.parametrize(
"role,expected_status",
[
(Role.ADMIN, status.HTTP_404_NOT_FOUND),
(Role.EDITOR, status.HTTP_404_NOT_FOUND),
(Role.VIEWER, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.ADMIN, status.HTTP_404_NOT_FOUND),
(LegacyAccessControlRole.EDITOR, status.HTTP_404_NOT_FOUND),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_user_cannot_delete_another_users_schedule_token(
@ -212,9 +206,8 @@ def test_user_cannot_delete_another_users_schedule_token(
role,
expected_status,
):
organization1, user1, _ = make_organization_and_user_with_plugin_token(role=role)
_, user2, token2 = make_organization_and_user_with_plugin_token(role=role)
organization1, user1, _ = make_organization_and_user_with_plugin_token(role)
_, user2, token2 = make_organization_and_user_with_plugin_token(role)
UserScheduleExportAuthToken.create_auth_token(
user=user1,

View file

@ -12,7 +12,7 @@ from rest_framework.response import Response
from apps.alerts.constants import ActionSource
from apps.alerts.models import Alert, AlertGroup, AlertReceiveChannel
from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdminOrEditor
from apps.api.permissions import RBACPermission
from apps.api.serializers.alert_group import AlertGroupListSerializer, AlertGroupSerializer
from apps.auth_token.auth import PluginAuthentication
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
@ -160,29 +160,29 @@ class AlertGroupView(
MobileAppAuthTokenAuthentication,
PluginAuthentication,
)
permission_classes = (IsAuthenticated, ActionPermission)
permission_classes = (IsAuthenticated, RBACPermission)
action_permissions = {
IsAdminOrEditor: (
*MODIFY_ACTIONS,
"acknowledge",
"unacknowledge",
"resolve",
"unresolve",
"attach",
"unattach",
"silence",
"unsilence",
"bulk_action",
"preview_template",
),
AnyRole: (
*READ_ACTIONS,
"stats",
"filters",
"silence_options",
"bulk_action_options",
),
rbac_permissions = {
"metadata": [RBACPermission.Permissions.ALERT_GROUPS_READ],
"list": [RBACPermission.Permissions.ALERT_GROUPS_READ],
"retrieve": [RBACPermission.Permissions.ALERT_GROUPS_READ],
"stats": [RBACPermission.Permissions.ALERT_GROUPS_READ],
"filters": [RBACPermission.Permissions.ALERT_GROUPS_READ],
"silence_options": [RBACPermission.Permissions.ALERT_GROUPS_READ],
"bulk_action_options": [RBACPermission.Permissions.ALERT_GROUPS_READ],
"create": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"update": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"destroy": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"acknowledge": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"unacknowledge": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"resolve": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"unresolve": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"attach": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"unattach": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"silence": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"unsilence": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"bulk_action": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"preview_template": [RBACPermission.Permissions.INTEGRATIONS_TEST],
}
http_method_names = ["get", "post"]

View file

@ -9,7 +9,7 @@ from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from apps.alerts.models import AlertReceiveChannel
from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin, IsAdminOrEditor
from apps.api.permissions import RBACPermission
from apps.api.serializers.alert_receive_channel import (
AlertReceiveChannelSerializer,
AlertReceiveChannelUpdateSerializer,
@ -66,19 +66,7 @@ class AlertReceiveChannelView(
ModelViewSet,
):
authentication_classes = (PluginAuthentication,)
permission_classes = (IsAuthenticated, ActionPermission)
action_permissions = {
IsAdmin: (*MODIFY_ACTIONS, "stop_maintenance", "start_maintenance", "change_team"),
IsAdminOrEditor: ("send_demo_alert", "preview_template"),
AnyRole: (
*READ_ACTIONS,
"integration_options",
"maintenance_duration_options",
"maintenance_mode_options",
"counters",
"counters_per_integration",
),
}
permission_classes = (IsAuthenticated, RBACPermission)
model = AlertReceiveChannel
serializer_class = AlertReceiveChannelSerializer
@ -90,6 +78,22 @@ class AlertReceiveChannelView(
filterset_class = AlertReceiveChannelFilter
rbac_permissions = {
"metadata": [RBACPermission.Permissions.INTEGRATIONS_READ],
"list": [RBACPermission.Permissions.INTEGRATIONS_READ],
"retrieve": [RBACPermission.Permissions.INTEGRATIONS_READ],
"integration_options": [RBACPermission.Permissions.INTEGRATIONS_READ],
"counters": [RBACPermission.Permissions.INTEGRATIONS_READ],
"counters_per_integration": [RBACPermission.Permissions.INTEGRATIONS_READ],
"send_demo_alert": [RBACPermission.Permissions.INTEGRATIONS_TEST],
"preview_template": [RBACPermission.Permissions.INTEGRATIONS_TEST],
"create": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"update": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"partial_update": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"destroy": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"change_team": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
}
def create(self, request, *args, **kwargs):
if request.data["integration"] is not None and (
request.data["integration"] in AlertReceiveChannel.WEB_INTEGRATION_CHOICES

Some files were not shown because too many files have changed in this diff Show more