mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
Merge branch 'master' into master
This commit is contained in:
commit
e2e4b1002b
22
.config/dotnet-tools.json
Normal file
22
.config/dotnet-tools.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"swashbuckle.aspnetcore.cli": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"commands": ["swagger"]
|
||||||
|
},
|
||||||
|
"coverlet.console": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"commands": ["coverlet"]
|
||||||
|
},
|
||||||
|
"dotnet-reportgenerator-globaltool": {
|
||||||
|
"version": "5.1.6",
|
||||||
|
"commands": ["reportgenerator"]
|
||||||
|
},
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "6.0.12",
|
||||||
|
"commands": ["dotnet-ef"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
**/bin
|
||||||
|
**/obj
|
||||||
|
**/node_modules
|
@ -5,6 +5,7 @@ root = true
|
|||||||
|
|
||||||
# Don't use tabs for indentation.
|
# Don't use tabs for indentation.
|
||||||
[*]
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
indent_style = space
|
indent_style = space
|
||||||
# (Please don't specify an indent_size here; that has too many unintended consequences.)
|
# (Please don't specify an indent_size here; that has too many unintended consequences.)
|
||||||
|
|
||||||
@ -72,6 +73,15 @@ dotnet_naming_style.end_in_async.required_suffix = Async
|
|||||||
dotnet_naming_style.end_in_async.capitalization = pascal_case
|
dotnet_naming_style.end_in_async.capitalization = pascal_case
|
||||||
dotnet_naming_style.end_in_async.word_separator =
|
dotnet_naming_style.end_in_async.word_separator =
|
||||||
|
|
||||||
|
# Obsolete warnings, this should be removed or changed to warning once we address some of the obsolete items.
|
||||||
|
dotnet_diagnostic.CS0618.severity = suggestion
|
||||||
|
|
||||||
|
# Obsolete warnings, this should be removed or changed to warning once we address some of the obsolete items.
|
||||||
|
dotnet_diagnostic.CS0612.severity = suggestion
|
||||||
|
|
||||||
|
# Remove unnecessary using directives https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0005
|
||||||
|
dotnet_diagnostic.IDE0005.severity = warning
|
||||||
|
|
||||||
# CSharp code style settings:
|
# CSharp code style settings:
|
||||||
[*.cs]
|
[*.cs]
|
||||||
# Prefer "var" everywhere
|
# Prefer "var" everywhere
|
||||||
@ -104,6 +114,13 @@ csharp_new_line_before_finally = true
|
|||||||
csharp_new_line_before_members_in_object_initializers = true
|
csharp_new_line_before_members_in_object_initializers = true
|
||||||
csharp_new_line_before_members_in_anonymous_types = true
|
csharp_new_line_before_members_in_anonymous_types = true
|
||||||
|
|
||||||
|
# Namespace settings
|
||||||
|
csharp_style_namespace_declarations = file_scoped:warning
|
||||||
|
|
||||||
|
# Switch expression
|
||||||
|
dotnet_diagnostic.CS8509.severity = error # missing switch case for named enum value
|
||||||
|
dotnet_diagnostic.CS8524.severity = none # missing switch case for unnamed enum value
|
||||||
|
|
||||||
# All files
|
# All files
|
||||||
[*]
|
[*]
|
||||||
guidelines = 120
|
guidelines = 120
|
||||||
|
11
.git-blame-ignore-revs
Normal file
11
.git-blame-ignore-revs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Apply .NET format https://github.com/bitwarden/server/pull/1764
|
||||||
|
23b0a1f9df25058ab29785ecad9a233113c10889
|
||||||
|
|
||||||
|
# Turn on file scoped namespaces (gets reverted) https://github.com/bitwarden/server/pull/2225
|
||||||
|
34fb4cca2aa78deb84d4cbc359992a7c6bba7ea5
|
||||||
|
|
||||||
|
# Revert filescoped https://github.com/bitwarden/server/pull/2227
|
||||||
|
bae03feffecbef488cb52f5f5bc133dfdbbaa316
|
||||||
|
|
||||||
|
# Run formatting for file scoped namespaces https://github.com/bitwarden/server/pull/2230
|
||||||
|
7f5f010e1eea400300c47f776604ecf46c4b4f2d
|
8
.git-hooks/pre-commit
Executable file
8
.git-hooks/pre-commit
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
FILES=$(git diff --cached --name-only --diff-filter=ACM "*.cs")
|
||||||
|
if [ -n "$FILES" ]
|
||||||
|
then
|
||||||
|
dotnet format ./bitwarden-server.sln --no-restore --include $FILES
|
||||||
|
echo "$FILES" | xargs git add
|
||||||
|
fi
|
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1,3 +1,4 @@
|
|||||||
*.sh eol=lf
|
*.sh eol=lf
|
||||||
|
*.cs eol=lf
|
||||||
.dockerignore eol=lf
|
.dockerignore eol=lf
|
||||||
dockerfile eol=lf
|
dockerfile eol=lf
|
5
.github/CODEOWNERS
vendored
Normal file
5
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Please sort lines alphabetically, this will ensure we don't accidentally add duplicates.
|
||||||
|
#
|
||||||
|
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||||
|
|
||||||
|
**/SecretsManager @bitwarden/pod-sm-dev
|
10
.github/ISSUE_TEMPLATE/bug.yml
vendored
10
.github/ISSUE_TEMPLATE/bug.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: Bug Report
|
name: Server Bug Report
|
||||||
description: File a bug report
|
description: File a bug report
|
||||||
labels: [bug]
|
labels: [bug]
|
||||||
body:
|
body:
|
||||||
@ -71,3 +71,11 @@ body:
|
|||||||
- Operating system: [e.g. Windows 10, Mac OS Catalina]
|
- Operating system: [e.g. Windows 10, Mac OS Catalina]
|
||||||
- Environment: [e.g. Docker, EKS, ECS, K8S]
|
- Environment: [e.g. Docker, EKS, ECS, K8S]
|
||||||
- Hardware: [e.g. Intel 6-core, 8GB RAM]
|
- Hardware: [e.g. Intel 6-core, 8GB RAM]
|
||||||
|
- type: checkboxes
|
||||||
|
id: issue-tracking-info
|
||||||
|
attributes:
|
||||||
|
label: Issue Tracking Info
|
||||||
|
description: |
|
||||||
|
Issue tracking information
|
||||||
|
options:
|
||||||
|
- label: I understand that work is tracked outside of Github. A PR will be linked to this issue should one be opened to address it, but Bitwarden doesn't use fields like "assigned", "milestone", or "project" to track progress.
|
||||||
|
90
.github/ISSUE_TEMPLATE/bw-unified.yml
vendored
Normal file
90
.github/ISSUE_TEMPLATE/bw-unified.yml
vendored
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
name: Bitwarden Unified Bug Report
|
||||||
|
name: Bitwarden Unified Deployment Bug Report
|
||||||
|
description: File a bug report
|
||||||
|
labels: [bug, bw-unified-deploy]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
|
||||||
|
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
|
||||||
|
- type: textarea
|
||||||
|
id: reproduce
|
||||||
|
attributes:
|
||||||
|
label: Steps To Reproduce
|
||||||
|
description: How can we reproduce the behavior.
|
||||||
|
value: |
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. Click on '...'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: Expected Result
|
||||||
|
description: A clear and concise description of what you expected to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: actual
|
||||||
|
attributes:
|
||||||
|
label: Actual Result
|
||||||
|
description: A clear and concise description of what is happening.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: screenshots
|
||||||
|
attributes:
|
||||||
|
label: Screenshots or Videos
|
||||||
|
description: If applicable, add screenshots and/or a short video to help explain your problem.
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
description: Add any other context about the problem here.
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Githash Version
|
||||||
|
description: Please go to https://{your-bitwarden-domain}/api/config and copy the gitHash version
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: environment-details
|
||||||
|
attributes:
|
||||||
|
label: Environment Details
|
||||||
|
description: If Self-Hosted please provide some additional environment details.
|
||||||
|
placeholder: |
|
||||||
|
- Operating system: [e.g. Windows 10, Mac OS Catalina]
|
||||||
|
- Environment: [e.g. Docker, EKS, ECS, K8S]
|
||||||
|
- Hardware: [e.g. Intel 6-core, 8GB RAM]
|
||||||
|
- type: textarea
|
||||||
|
id: database-image
|
||||||
|
attributes:
|
||||||
|
label: Database Image
|
||||||
|
description: Please include the image and version of your database
|
||||||
|
placeholder: |
|
||||||
|
# MariaDB Example
|
||||||
|
mariadb:10
|
||||||
|
# Postgres Example
|
||||||
|
postgres:14
|
||||||
|
- type: textarea
|
||||||
|
id: epic-label
|
||||||
|
attributes:
|
||||||
|
label: Issue-Link
|
||||||
|
description: Link to our pinned issue, tracking all Bitwarden Unified
|
||||||
|
value: |
|
||||||
|
https://github.com/bitwarden/server/issues/2480
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
id: issue-tracking-info
|
||||||
|
attributes:
|
||||||
|
label: Issue Tracking Info
|
||||||
|
description: |
|
||||||
|
Issue tracking information
|
||||||
|
options:
|
||||||
|
- label: I understand that work is tracked outside of Github. A PR will be linked to this issue should one be opened to address it, but Bitwarden doesn't use fields like "assigned", "milestone", or "project" to track progress.
|
20
.github/PULL_REQUEST_TEMPLATE.md
vendored
20
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,9 +1,14 @@
|
|||||||
## Type of change
|
## Type of change
|
||||||
|
|
||||||
|
<!-- (mark with an `X`) -->
|
||||||
|
|
||||||
|
```
|
||||||
- [ ] Bug fix
|
- [ ] Bug fix
|
||||||
- [ ] New feature development
|
- [ ] New feature development
|
||||||
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
|
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
|
||||||
- [ ] Build/deploy pipeline (DevOps)
|
- [ ] Build/deploy pipeline (DevOps)
|
||||||
- [ ] Other
|
- [ ] Other
|
||||||
|
```
|
||||||
|
|
||||||
## Objective
|
## Objective
|
||||||
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
|
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
|
||||||
@ -16,13 +21,10 @@
|
|||||||
|
|
||||||
* **file.ext:** Description of what was changed and why
|
* **file.ext:** Description of what was changed and why
|
||||||
|
|
||||||
## Testing requirements
|
|
||||||
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Before you submit
|
## Before you submit
|
||||||
- [ ] If making database changes - I have also updated Entity Framework queries and/or migrations
|
|
||||||
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required)
|
- Please check for formatting errors (`dotnet format --verify-no-changes`) (required)
|
||||||
- [ ] This change requires a **documentation update** (notify the documentation team)
|
- If making database changes - make sure you also update Entity Framework queries and/or migrations
|
||||||
- [ ] This change has particular **deployment requirements** (notify the DevOps team)
|
- Please add **unit tests** where it makes sense to do so (encouraged but not required)
|
||||||
|
- If this change requires a **documentation update** - notify the documentation team
|
||||||
|
- If this change has particular **deployment requirements** - notify the DevOps team
|
||||||
|
25
.github/renovate.json
vendored
Normal file
25
.github/renovate.json
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:base",
|
||||||
|
"schedule:monthly",
|
||||||
|
":maintainLockFilesMonthly",
|
||||||
|
":preserveSemverRanges",
|
||||||
|
":rebaseStalePrs",
|
||||||
|
":disableMajorUpdates"
|
||||||
|
],
|
||||||
|
"enabledManagers": [
|
||||||
|
"nuget"
|
||||||
|
],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchManagers": ["nuget"],
|
||||||
|
"groupName": "Nuget updates",
|
||||||
|
"groupSlug": "nuget",
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"minor",
|
||||||
|
"patch"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
64
.github/workflows/automatic-issue-responses.yml
vendored
Normal file
64
.github/workflows/automatic-issue-responses.yml
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
name: Automatic responses
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
jobs:
|
||||||
|
close-issue:
|
||||||
|
name: 'Close issue with automatic response'
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
# Feature request
|
||||||
|
- if: github.event.label.name == 'feature-request'
|
||||||
|
name: Feature request
|
||||||
|
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||||
|
with:
|
||||||
|
comment: |
|
||||||
|
We use GitHub issues as a place to track bugs and other development related issues. The [Bitwarden Community Forums](https://community.bitwarden.com/) has a [Feature Requests](https://community.bitwarden.com/c/feature-requests) section for submitting, voting for, and discussing requests like this one.
|
||||||
|
|
||||||
|
Please [sign up on our forums](https://community.bitwarden.com/signup) and search to see if this request already exists. If so, you can vote for it and contribute to any discussions about it. If not, you can re-create the request there so that it can be properly tracked.
|
||||||
|
|
||||||
|
This issue will now be closed. Thanks!
|
||||||
|
# Intended behavior
|
||||||
|
- if: github.event.label.name == 'intended-behavior'
|
||||||
|
name: Intended behaviour
|
||||||
|
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||||
|
with:
|
||||||
|
comment: |
|
||||||
|
Your issue appears to be describing the intended behavior of the software. If you want this to be changed, it would be a feature request.
|
||||||
|
|
||||||
|
We use GitHub issues as a place to track bugs and other development related issues. The [Bitwarden Community Forums](https://community.bitwarden.com/) has a [Feature Requests](https://community.bitwarden.com/c/feature-requests) section for submitting, voting for, and discussing requests like this one.
|
||||||
|
|
||||||
|
Please [sign up on our forums](https://community.bitwarden.com/signup) and search to see if this request already exists. If so, you can vote for it and contribute to any discussions about it. If not, you can re-create the request there so that it can be properly tracked.
|
||||||
|
|
||||||
|
This issue will now be closed. Thanks!
|
||||||
|
# Customer support request
|
||||||
|
- if: github.event.label.name == 'customer-support'
|
||||||
|
name: Customer Support request
|
||||||
|
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||||
|
with:
|
||||||
|
comment: |
|
||||||
|
We use GitHub issues as a place to track bugs and other development related issues. Your issue appears to be a support request, or would otherwise be better handled by our dedicated Customer Success team.
|
||||||
|
|
||||||
|
Please contact us using our [Contact page](https://bitwarden.com/contact). You can include a link to this issue in the message content.
|
||||||
|
|
||||||
|
Alternatively, you can also search for an answer in our [help documentation](https://bitwarden.com/help/) or get help from other Bitwarden users on our [community forums](https://community.bitwarden.com/c/support/). The issue here will be closed.
|
||||||
|
# Resolved
|
||||||
|
- if: github.event.label.name == 'resolved'
|
||||||
|
name: Resolved
|
||||||
|
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||||
|
with:
|
||||||
|
comment: |
|
||||||
|
We’ve closed this issue, as it appears the original problem has been resolved. If this happens again or continues to be an problem, please respond to this issue with any additional detail to assist with reproduction and root cause analysis.
|
||||||
|
# Stale
|
||||||
|
- if: github.event.label.name == 'stale'
|
||||||
|
name: Stale
|
||||||
|
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||||
|
with:
|
||||||
|
comment: |
|
||||||
|
As we haven’t heard from you about this problem in some time, this issue will now be closed.
|
||||||
|
|
||||||
|
If this happens again or continues to be an problem, please respond to this issue with any additional detail to assist with reproduction and root cause analysis.
|
166
.github/workflows/build-self-host.yml
vendored
Normal file
166
.github/workflows/build-self-host.yml
vendored
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
---
|
||||||
|
name: Build Self-Host
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- "l10n_master"
|
||||||
|
- "gh-pages"
|
||||||
|
paths-ignore:
|
||||||
|
- ".github/workflows/**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-docker:
|
||||||
|
name: Build Docker image
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||||
|
|
||||||
|
- name: Check Branch to Publish
|
||||||
|
env:
|
||||||
|
PUBLISH_BRANCHES: "master,rc,hotfix-rc"
|
||||||
|
id: publish-branch-check
|
||||||
|
run: |
|
||||||
|
IFS="," read -a publish_branches <<< $PUBLISH_BRANCHES
|
||||||
|
|
||||||
|
if [[ " ${publish_branches[*]} " =~ " ${GITHUB_REF:11} " ]]; then
|
||||||
|
echo "is_publish_branch=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "is_publish_branch=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
########## Set up Docker ##########
|
||||||
|
- name: Set up QEMU emulators
|
||||||
|
uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@8c0edbc76e98fa90f69d9a2c020dcb50019dc325
|
||||||
|
|
||||||
|
########## Login to Docker registries ##########
|
||||||
|
- name: Login to Azure - QA Subscription
|
||||||
|
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Login to Azure ACR
|
||||||
|
run: az acr login -n bitwardenqa
|
||||||
|
|
||||||
|
- name: Login to Azure - Prod Subscription
|
||||||
|
if: ${{ env.is_publish_branch == 'true' }}
|
||||||
|
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Retrieve secrets
|
||||||
|
if: ${{ env.is_publish_branch == 'true' }}
|
||||||
|
id: retrieve-secrets
|
||||||
|
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
||||||
|
with:
|
||||||
|
keyvault: "bitwarden-prod-kv"
|
||||||
|
secrets: "docker-password,
|
||||||
|
docker-username,
|
||||||
|
dct-delegate-2-repo-passphrase,
|
||||||
|
dct-delegate-2-key"
|
||||||
|
|
||||||
|
- name: Log into Docker
|
||||||
|
if: ${{ env.is_publish_branch == 'true' }}
|
||||||
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ steps.retrieve-secrets.outputs.docker-username }}
|
||||||
|
DOCKER_PASSWORD: ${{ steps.retrieve-secrets.outputs.docker-password }}
|
||||||
|
run: echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||||
|
|
||||||
|
- name: Setup Docker Trust
|
||||||
|
if: ${{ env.is_publish_branch == 'true' }}
|
||||||
|
env:
|
||||||
|
DCT_DELEGATION_KEY_ID: "c9bde8ec820701516491e5e03d3a6354e7bd66d05fa3df2b0062f68b116dc59c"
|
||||||
|
DCT_DELEGATE_KEY: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-key }}
|
||||||
|
DCT_REPO_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.docker/trust/private
|
||||||
|
echo "$DCT_DELEGATE_KEY" > ~/.docker/trust/private/$DCT_DELEGATION_KEY_ID.key
|
||||||
|
echo "DOCKER_CONTENT_TRUST=1" >> $GITHUB_ENV
|
||||||
|
echo "DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE=$DCT_REPO_PASSPHRASE" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
########## Generate image tag and build Docker image ##########
|
||||||
|
- name: Generate Docker image tag
|
||||||
|
id: tag
|
||||||
|
run: |
|
||||||
|
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name
|
||||||
|
if [[ "$IMAGE_TAG" == "master" ]]; then
|
||||||
|
IMAGE_TAG=dev
|
||||||
|
elif [[ "$IMAGE_TAG" == "rc" ]] || [[ "$IMAGE_TAG" == "hotfix-rc" ]]; then
|
||||||
|
IMAGE_TAG=beta
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Generate tag list
|
||||||
|
id: tag-list
|
||||||
|
env:
|
||||||
|
IMAGE_TAG: ${{ steps.tag.outputs.image_tag }}
|
||||||
|
run: |
|
||||||
|
if [ "$IMAGE_TAG" = "dev" ] || [ "$IMAGE_TAG" = "beta" ]; then
|
||||||
|
echo "tags=bitwardenqa.azurecr.io/self-host:${IMAGE_TAG},bitwarden/self-host:${IMAGE_TAG}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "tags=bitwardenqa.azurecr.io/self-host:${IMAGE_TAG}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
uses: docker/build-push-action@c56af957549030174b10d6867f20e78cfd7debc5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker-unified/Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64,
|
||||||
|
linux/arm/v7,
|
||||||
|
linux/arm64/v8
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.tag-list.outputs.tags }}
|
||||||
|
|
||||||
|
- name: Log out of Docker and disable Docker Notary
|
||||||
|
if: ${{ env.is_publish_branch == 'true' }}
|
||||||
|
run: |
|
||||||
|
docker logout
|
||||||
|
echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
check-failures:
|
||||||
|
name: Check for failures
|
||||||
|
if: always()
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs: build-docker
|
||||||
|
steps:
|
||||||
|
- name: Check if any job failed
|
||||||
|
if: |
|
||||||
|
github.ref == 'refs/heads/master'
|
||||||
|
|| github.ref == 'refs/heads/rc'
|
||||||
|
|| github.ref == 'refs/heads/hotfix-rc'
|
||||||
|
env:
|
||||||
|
BUILD_DOCKER_STATUS: ${{ needs.build-docker.result }}
|
||||||
|
run: |
|
||||||
|
if [ "$BUILD_DOCKER_STATUS" = "failure" ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Login to Azure - Prod Subscription
|
||||||
|
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Retrieve secrets
|
||||||
|
id: retrieve-secrets
|
||||||
|
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
keyvault: "bitwarden-prod-kv"
|
||||||
|
secrets: "devops-alerts-slack-webhook-url"
|
||||||
|
|
||||||
|
- name: Notify Slack on failure
|
||||||
|
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
||||||
|
with:
|
||||||
|
status: ${{ job.status }}
|
506
.github/workflows/build.yml
vendored
506
.github/workflows/build.yml
vendored
@ -4,18 +4,19 @@ name: Build
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- 'l10n_master'
|
- "l10n_master"
|
||||||
- 'gh-pages'
|
- "gh-pages"
|
||||||
|
paths-ignore:
|
||||||
|
- ".github/workflows/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cloc:
|
cloc:
|
||||||
name: CLOC
|
name: CLOC
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
|
||||||
|
|
||||||
- name: Install cloc
|
- name: Install cloc
|
||||||
run: |
|
run: |
|
||||||
@ -25,43 +26,46 @@ jobs:
|
|||||||
- name: Print lines of code
|
- name: Print lines of code
|
||||||
run: cloc --include-lang C#,SQL,Razor,"Bourne Shell",PowerShell,HTML,CSS,Sass,JavaScript,TypeScript --vcs git
|
run: cloc --include-lang C#,SQL,Razor,"Bourne Shell",PowerShell,HTML,CSS,Sass,JavaScript,TypeScript --vcs git
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||||
|
|
||||||
|
- name: Verify Format
|
||||||
|
run: dotnet format --verify-no-changes
|
||||||
|
|
||||||
testing:
|
testing:
|
||||||
name: Testing
|
name: Testing
|
||||||
runs-on: windows-2019
|
runs-on: windows-2022
|
||||||
|
env:
|
||||||
|
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||||
steps:
|
steps:
|
||||||
- name: Set up NuGet
|
- name: Set up dotnet
|
||||||
uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1
|
uses: actions/setup-dotnet@9211491ffb35dd6a6657ca4f45d43dfe6e97c829
|
||||||
with:
|
with:
|
||||||
nuget-version: '5'
|
dotnet-version: "6.0.x"
|
||||||
|
|
||||||
- name: Set up MSBuild
|
- name: Set up MSBuild
|
||||||
uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d
|
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
||||||
|
|
||||||
- name: Set up Node
|
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
|
||||||
with:
|
|
||||||
node-version: '14'
|
|
||||||
|
|
||||||
- name: Update NPM
|
|
||||||
run: |
|
|
||||||
npm install -g npm@7
|
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
nuget help | grep Version
|
|
||||||
msbuild -version
|
|
||||||
dotnet --info
|
dotnet --info
|
||||||
node --version
|
msbuild -version
|
||||||
npm --version
|
nuget help | grep Version
|
||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||||
|
|
||||||
- name: Restore
|
- name: Restore
|
||||||
run: msbuild /t:restore
|
run: dotnet restore --locked-mode
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Build OSS solution
|
||||||
|
run: msbuild bitwarden-server.sln /p:Configuration=Debug /p:DefineConstants="OSS" /verbosity:minimal
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Build solution
|
- name: Build solution
|
||||||
@ -69,58 +73,69 @@ jobs:
|
|||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Test OSS solution
|
- name: Test OSS solution
|
||||||
run: dotnet test ./test --configuration Debug --no-build
|
run: dotnet test ./test --configuration Debug --no-build --logger "trx;LogFileName=oss-test-results.trx" || true
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Test Bitwarden solution
|
- name: Test Bitwarden solution
|
||||||
run: dotnet test ./bitwarden_license/test/CmmCore.Test --configuration Debug --no-build
|
run: dotnet test ./bitwarden_license/test --configuration Debug --no-build --logger "trx;LogFileName=bw-test-results.trx" || true
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Report test results
|
||||||
|
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: Test Results
|
||||||
|
path: "**/*-test-results.trx"
|
||||||
|
reporter: dotnet-trx
|
||||||
|
fail-on-error: true
|
||||||
|
|
||||||
build-artifacts:
|
build-artifacts:
|
||||||
name: Build artifacts
|
name: Build artifacts
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
needs: testing
|
needs:
|
||||||
|
- testing
|
||||||
|
- lint
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- service_name: Admin
|
- project_name: Admin
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
gulp: true
|
node: true
|
||||||
- service_name: Api
|
- project_name: Api
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
- service_name: Billing
|
- project_name: Billing
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
- service_name: Events
|
- project_name: Events
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
- service_name: EventsProcessor
|
- project_name: EventsProcessor
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
- service_name: Icons
|
- project_name: Icons
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
- service_name: Identity
|
- project_name: Identity
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
- service_name: Notifications
|
- project_name: Notifications
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
- service_name: Server
|
- project_name: Server
|
||||||
base_path: ./util
|
base_path: ./util
|
||||||
- service_name: Setup
|
- project_name: Setup
|
||||||
base_path: ./util
|
base_path: ./util
|
||||||
- service_name: Sso
|
- project_name: Sso
|
||||||
base_path: ./bitwarden_license/src
|
base_path: ./bitwarden_license/src
|
||||||
gulp: true
|
node: true
|
||||||
|
- project_name: Scim
|
||||||
|
base_path: ./bitwarden_license/src
|
||||||
|
dotnet: true
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
cache: "npm"
|
||||||
|
cache-dependency-path: "**/package-lock.json"
|
||||||
- name: Update NPM
|
node-version: "16"
|
||||||
run: |
|
|
||||||
npm install -g npm@7
|
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@ -128,288 +143,273 @@ jobs:
|
|||||||
dotnet --info
|
dotnet --info
|
||||||
node --version
|
node --version
|
||||||
npm --version
|
npm --version
|
||||||
gulp --version
|
|
||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Set up Gulp
|
- name: Restore/Clean project
|
||||||
if: ${{ matrix.gulp }}
|
working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
||||||
working-directory: ${{ matrix.base_path }}/${{ matrix.service_name }}
|
|
||||||
run: |
|
|
||||||
npm install -g gulp
|
|
||||||
|
|
||||||
- name: Restore/Clean service
|
|
||||||
working-directory: ${{ matrix.base_path }}/${{ matrix.service_name }}
|
|
||||||
run: |
|
run: |
|
||||||
echo "Restore"
|
echo "Restore"
|
||||||
dotnet restore
|
dotnet restore
|
||||||
echo "Clean"
|
echo "Clean"
|
||||||
dotnet clean -c "Release" -o obj/build-output/publish
|
dotnet clean -c "Release" -o obj/build-output/publish
|
||||||
|
|
||||||
- name: Execute Gulp
|
- name: Build node
|
||||||
if: ${{ matrix.gulp }}
|
if: ${{ matrix.node }}
|
||||||
working-directory: ${{ matrix.base_path }}/${{ matrix.service_name }}
|
working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
||||||
run: |
|
run: |
|
||||||
npm install
|
npm ci
|
||||||
gulp --gulpfile gulpfile.js build
|
npm run build
|
||||||
|
|
||||||
- name: Publish service
|
- name: Publish project
|
||||||
working-directory: ${{ matrix.base_path }}/${{ matrix.service_name }}
|
working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
||||||
run: |
|
run: |
|
||||||
echo "Publish"
|
echo "Publish"
|
||||||
dotnet publish -c "Release" -o obj/build-output/publish
|
dotnet publish -c "Release" -o obj/build-output/publish
|
||||||
|
|
||||||
cd obj/build-output/publish
|
cd obj/build-output/publish
|
||||||
zip -r ${{ matrix.service_name }}.zip .
|
zip -r ${{ matrix.project_name }}.zip .
|
||||||
mv ${{ matrix.service_name }}.zip ../../../
|
mv ${{ matrix.project_name }}.zip ../../../
|
||||||
|
|
||||||
pwd
|
pwd
|
||||||
ls -atlh ../../../
|
ls -atlh ../../../
|
||||||
|
|
||||||
- name: Upload service artifact
|
- name: Upload project artifact
|
||||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
|
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.service_name }}.zip
|
name: ${{ matrix.project_name }}.zip
|
||||||
path: ${{ matrix.base_path }}/${{ matrix.service_name }}/${{ matrix.service_name }}.zip
|
path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
build-docker:
|
build-docker:
|
||||||
name: Build Docker images
|
name: Build Docker images
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
needs: build-artifacts
|
needs: build-artifacts
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- service_name: Admin
|
- project_name: Admin
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
docker_repo: bitwarden
|
docker_repos: [bitwarden, bitwardenqa.azurecr.io]
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- service_name: Api
|
- project_name: Api
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
docker_repo: bitwarden
|
docker_repos: [bitwarden, bitwardenqa.azurecr.io]
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- service_name: Attachments
|
- project_name: Attachments
|
||||||
base_path: ./util
|
base_path: ./util
|
||||||
docker_repo: bitwarden
|
docker_repos: [bitwarden, bitwardenqa.azurecr.io]
|
||||||
- service_name: Events
|
- project_name: Events
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
docker_repo: bitwarden
|
docker_repos: [bitwarden, bitwardenqa.azurecr.io]
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- service_name: EventsProcessor
|
- project_name: EventsProcessor
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
docker_repo: bitwardenqa.azurecr.io
|
docker_repos: [bitwardenqa.azurecr.io]
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- service_name: Icons
|
- project_name: Icons
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
docker_repo: bitwarden
|
docker_repos: [bitwarden, bitwardenqa.azurecr.io]
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- service_name: Identity
|
- project_name: Identity
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
docker_repo: bitwarden
|
docker_repos: [bitwarden, bitwardenqa.azurecr.io]
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- service_name: K8S-Proxy
|
- project_name: MsSql
|
||||||
base_path: ./util
|
base_path: ./util
|
||||||
docker_repo: bitwarden
|
docker_repos: [bitwarden, bitwardenqa.azurecr.io]
|
||||||
- service_name: MsSql
|
- project_name: Nginx
|
||||||
base_path: ./util
|
base_path: ./util
|
||||||
docker_repo: bitwarden
|
docker_repos: [bitwarden, bitwardenqa.azurecr.io]
|
||||||
- service_name: Nginx
|
- project_name: Notifications
|
||||||
base_path: ./util
|
|
||||||
docker_repo: bitwarden
|
|
||||||
- service_name: Notifications
|
|
||||||
base_path: ./src
|
base_path: ./src
|
||||||
docker_repo: bitwarden
|
docker_repos: [bitwarden, bitwardenqa.azurecr.io]
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- service_name: Server
|
- project_name: Server
|
||||||
base_path: ./util
|
base_path: ./util
|
||||||
docker_repo: bitwarden
|
docker_repos: [bitwarden, bitwardenqa.azurecr.io]
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- service_name: Setup
|
- project_name: Setup
|
||||||
base_path: ./util
|
base_path: ./util
|
||||||
docker_repo: bitwarden
|
docker_repos: [bitwarden, bitwardenqa.azurecr.io]
|
||||||
dotnet: true
|
dotnet: true
|
||||||
- service_name: Sso
|
- project_name: Sso
|
||||||
base_path: ./bitwarden_license/src
|
base_path: ./bitwarden_license/src
|
||||||
docker_repo: bitwarden
|
docker_repos: [bitwarden, bitwardenqa.azurecr.io]
|
||||||
|
dotnet: true
|
||||||
|
- project_name: Scim
|
||||||
|
base_path: ./bitwarden_license/src
|
||||||
|
docker_repos: [bitwarden, bitwardenqa.azurecr.io]
|
||||||
|
dotnet: true
|
||||||
|
- project_name: Billing
|
||||||
|
base_path: ./src
|
||||||
|
docker_repos: [bitwardenqa.azurecr.io]
|
||||||
dotnet: true
|
dotnet: true
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||||
|
|
||||||
- name: Login to Azure - Prod Subscription
|
########## Build Docker Image ##########
|
||||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
- name: Setup project name
|
||||||
with:
|
|
||||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
|
||||||
|
|
||||||
- name: Retrieve secrets
|
|
||||||
id: retrieve-secrets
|
|
||||||
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
|
|
||||||
with:
|
|
||||||
keyvault: "bitwarden-prod-kv"
|
|
||||||
secrets: "aws-ecr-access-key-id,
|
|
||||||
aws-ecr-secret-access-key,
|
|
||||||
docker-password,
|
|
||||||
docker-username,
|
|
||||||
dct-delegate-2-repo-passphrase,
|
|
||||||
dct-delegate-2-key"
|
|
||||||
|
|
||||||
- name: Login to Azure - QA Subscription
|
|
||||||
if: ${{ matrix.service_name }} == "EventsProcessor"
|
|
||||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
|
||||||
with:
|
|
||||||
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
|
|
||||||
|
|
||||||
- name: Log into Docker
|
|
||||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
|
|
||||||
env:
|
|
||||||
DOCKER_USERNAME: ${{ steps.retrieve-secrets.outputs.docker-username }}
|
|
||||||
DOCKER_PASSWORD: ${{ steps.retrieve-secrets.outputs.docker-password }}
|
|
||||||
run: |
|
|
||||||
if [[ "${{ matrix.docker_repo }}" == "bitwardenqa.azurecr.io" ]]; then
|
|
||||||
az acr login -n bitwardenqa
|
|
||||||
else
|
|
||||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Setup Docker Trust
|
|
||||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
|
|
||||||
env:
|
|
||||||
DCT_DELEGATION_KEY_ID: "c9bde8ec820701516491e5e03d3a6354e7bd66d05fa3df2b0062f68b116dc59c"
|
|
||||||
DCT_DELEGATE_KEY: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-key }}
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.docker/trust/private
|
|
||||||
echo "$DCT_DELEGATE_KEY" > ~/.docker/trust/private/$DCT_DELEGATION_KEY_ID.key
|
|
||||||
|
|
||||||
- name: Setup service name
|
|
||||||
id: setup
|
id: setup
|
||||||
run: |
|
run: |
|
||||||
SERVICE_NAME=$(echo "${{ matrix.service_name }}" | awk '{print tolower($0)}')
|
PROJECT_NAME=$(echo "${{ matrix.project_name }}" | awk '{print tolower($0)}')
|
||||||
echo "Matrix name: ${{ matrix.service_name }}"
|
echo "Matrix name: ${{ matrix.project_name }}"
|
||||||
echo "SERVICE_NAME: $SERVICE_NAME"
|
echo "PROJECT_NAME: $PROJECT_NAME"
|
||||||
echo "::set-output name=service_name::$SERVICE_NAME"
|
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Get build artifact
|
- name: Get build artifact
|
||||||
if: ${{ matrix.dotnet }}
|
if: ${{ matrix.dotnet }}
|
||||||
uses: actions/download-artifact@3be87be14a055c47b01d3bd88f8fe02320a9bb60 # v2.0.10
|
uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.service_name }}.zip
|
name: ${{ matrix.project_name }}.zip
|
||||||
|
|
||||||
- name: Setup build artifact
|
- name: Setup build artifact
|
||||||
if: ${{ matrix.dotnet }}
|
if: ${{ matrix.dotnet }}
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ${{ matrix.base_path}}/${{ matrix.service_name }}/obj/build-output/publish
|
mkdir -p ${{ matrix.base_path}}/${{ matrix.project_name }}/obj/build-output/publish
|
||||||
unzip ${{ matrix.service_name }}.zip \
|
unzip ${{ matrix.project_name }}.zip \
|
||||||
-d ${{ matrix.base_path }}/${{ matrix.service_name }}/obj/build-output/publish
|
-d ${{ matrix.base_path }}/${{ matrix.project_name }}/obj/build-output/publish
|
||||||
|
|
||||||
- name: Build Docker images
|
- name: Build Docker image
|
||||||
|
env:
|
||||||
|
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||||
|
run: docker build -t $PROJECT_NAME ${{ matrix.base_path }}/${{ matrix.project_name }}
|
||||||
|
|
||||||
|
########## ACR ##########
|
||||||
|
- name: Login to Azure - QA Subscription
|
||||||
|
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Login to Azure ACR
|
||||||
|
run: az acr login -n bitwardenqa
|
||||||
|
|
||||||
|
- name: Tag and Push image to Azure ACR QA registry
|
||||||
|
env:
|
||||||
|
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||||
|
REGISTRY: bitwardenqa.azurecr.io
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ matrix.service_name }}" = "K8S-Proxy" ]; then
|
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name
|
||||||
docker build -f ${{ matrix.base_path }}/Nginx/Dockerfile-k8s \
|
if [[ "$IMAGE_TAG" == "master" ]]; then
|
||||||
-t ${{ steps.setup.outputs.service_name }} ${{ matrix.base_path }}/Nginx
|
IMAGE_TAG=dev
|
||||||
else
|
|
||||||
docker build -t ${{ steps.setup.outputs.service_name }} \
|
|
||||||
${{ matrix.base_path }}/${{ matrix.service_name }}
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Docker Trust setup
|
docker tag $PROJECT_NAME \
|
||||||
|
$REGISTRY/$PROJECT_NAME:$IMAGE_TAG
|
||||||
|
docker push $REGISTRY/$PROJECT_NAME:$IMAGE_TAG
|
||||||
|
|
||||||
|
- name: Log out of Docker
|
||||||
|
run: docker logout
|
||||||
|
|
||||||
|
########## DockerHub ##########
|
||||||
|
- name: Login to Azure - Prod Subscription
|
||||||
if: |
|
if: |
|
||||||
matrix.docker_repo == 'bitwarden'
|
contains(matrix.docker_repos, 'bitwarden')
|
||||||
&& (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix')
|
&& (github.ref == 'refs/heads/master' ||
|
||||||
|
github.ref == 'refs/heads/rc' ||
|
||||||
|
github.ref == 'refs/heads/hotfix-rc')
|
||||||
|
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Retrieve secrets
|
||||||
|
if: |
|
||||||
|
contains(matrix.docker_repos, 'bitwarden')
|
||||||
|
&& (github.ref == 'refs/heads/master' ||
|
||||||
|
github.ref == 'refs/heads/rc' ||
|
||||||
|
github.ref == 'refs/heads/hotfix-rc')
|
||||||
|
id: retrieve-secrets
|
||||||
|
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
||||||
|
with:
|
||||||
|
keyvault: "bitwarden-prod-kv"
|
||||||
|
secrets: "docker-password,
|
||||||
|
docker-username,
|
||||||
|
dct-delegate-2-repo-passphrase,
|
||||||
|
dct-delegate-2-key"
|
||||||
|
|
||||||
|
- name: Log into Docker
|
||||||
|
if: |
|
||||||
|
contains(matrix.docker_repos, 'bitwarden')
|
||||||
|
&& (github.ref == 'refs/heads/master' ||
|
||||||
|
github.ref == 'refs/heads/rc' ||
|
||||||
|
github.ref == 'refs/heads/hotfix-rc')
|
||||||
env:
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ steps.retrieve-secrets.outputs.docker-username }}
|
||||||
|
DOCKER_PASSWORD: ${{ steps.retrieve-secrets.outputs.docker-password }}
|
||||||
|
run: echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||||
|
|
||||||
|
- name: Setup Docker Trust
|
||||||
|
if: |
|
||||||
|
contains(matrix.docker_repos, 'bitwarden')
|
||||||
|
&& (github.ref == 'refs/heads/master' ||
|
||||||
|
github.ref == 'refs/heads/rc' ||
|
||||||
|
github.ref == 'refs/heads/hotfix-rc')
|
||||||
|
env:
|
||||||
|
DCT_DELEGATION_KEY_ID: "c9bde8ec820701516491e5e03d3a6354e7bd66d05fa3df2b0062f68b116dc59c"
|
||||||
|
DCT_DELEGATE_KEY: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-key }}
|
||||||
DCT_REPO_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
|
DCT_REPO_PASSPHRASE: ${{ steps.retrieve-secrets.outputs.dct-delegate-2-repo-passphrase }}
|
||||||
run: |
|
run: |
|
||||||
|
mkdir -p ~/.docker/trust/private
|
||||||
|
echo "$DCT_DELEGATE_KEY" > ~/.docker/trust/private/$DCT_DELEGATION_KEY_ID.key
|
||||||
echo "DOCKER_CONTENT_TRUST=1" >> $GITHUB_ENV
|
echo "DOCKER_CONTENT_TRUST=1" >> $GITHUB_ENV
|
||||||
echo "DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE=$DCT_REPO_PASSPHRASE" >> $GITHUB_ENV
|
echo "DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE=$DCT_REPO_PASSPHRASE" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Tag and Push RC to Docker Hub
|
- name: Tag and Push RC to Docker Hub
|
||||||
if: github.ref == 'refs/heads/rc'
|
if: |
|
||||||
|
contains(matrix.docker_repos, 'bitwarden')
|
||||||
|
&& (github.ref == 'refs/heads/master' ||
|
||||||
|
github.ref == 'refs/heads/rc' ||
|
||||||
|
github.ref == 'refs/heads/hotfix-rc')
|
||||||
|
env:
|
||||||
|
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||||
|
REGISTRY: bitwarden
|
||||||
run: |
|
run: |
|
||||||
docker tag ${{ steps.setup.outputs.service_name }} \
|
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name
|
||||||
${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }}:rc
|
if [[ "$IMAGE_TAG" == "master" ]]; then
|
||||||
docker push ${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }}:rc
|
IMAGE_TAG=dev
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Tag and Push Hotfix to Docker Hub
|
docker tag $PROJECT_NAME \
|
||||||
if: github.ref == 'refs/heads/hotfix'
|
$REGISTRY/$PROJECT_NAME:$IMAGE_TAG
|
||||||
run: |
|
docker push $REGISTRY/$PROJECT_NAME:$IMAGE_TAG
|
||||||
docker tag ${{ steps.setup.outputs.service_name }} \
|
|
||||||
${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }}:hotfix
|
|
||||||
docker push ${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }}:hotfix
|
|
||||||
|
|
||||||
- name: Tag and Push Dev to Docker Hub
|
|
||||||
if: github.ref == 'refs/heads/master'
|
|
||||||
run: |
|
|
||||||
docker tag ${{ steps.setup.outputs.service_name }} \
|
|
||||||
${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }}:dev
|
|
||||||
docker push ${{ matrix.docker_repo }}/${{ steps.setup.outputs.service_name }}:dev
|
|
||||||
|
|
||||||
- name: Log out of Docker and disable Docker Notary
|
- name: Log out of Docker and disable Docker Notary
|
||||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
|
if: |
|
||||||
|
contains(matrix.docker_repos, 'bitwarden')
|
||||||
|
&& (github.ref == 'refs/heads/master' ||
|
||||||
|
github.ref == 'refs/heads/rc' ||
|
||||||
|
github.ref == 'refs/heads/hotfix-rc')
|
||||||
run: |
|
run: |
|
||||||
docker logout
|
docker logout
|
||||||
echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV
|
echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@0d9a5be0dceea74e09396820e1e522ba4a110d2f # v1
|
|
||||||
with:
|
|
||||||
aws-access-key-id: ${{ steps.retrieve-secrets.outputs.aws-ecr-access-key-id }}
|
|
||||||
aws-secret-access-key: ${{ steps.retrieve-secrets.outputs.aws-ecr-secret-access-key }}
|
|
||||||
aws-region: us-east-1
|
|
||||||
|
|
||||||
- name: Login to Amazon ECR
|
|
||||||
id: login-ecr
|
|
||||||
uses: aws-actions/amazon-ecr-login@aaf69d68aa3fb14c1d5a6be9ac61fe15b48453a2 # v1
|
|
||||||
|
|
||||||
- name: Tag and Push RC to AWS ECR nonprod registry
|
|
||||||
if: github.ref == 'refs/heads/rc'
|
|
||||||
env:
|
|
||||||
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
|
||||||
IMAGE_TAG: ${{ github.sha }}
|
|
||||||
run: |
|
|
||||||
docker tag ${{ steps.setup.outputs.service_name }} \
|
|
||||||
$ECR_REGISTRY/nonprod/${{ steps.setup.outputs.service_name }}:rc-${IMAGE_TAG:(-8)}
|
|
||||||
docker push $ECR_REGISTRY/nonprod/${{ steps.setup.outputs.service_name }}:rc-${IMAGE_TAG:(-8)}
|
|
||||||
|
|
||||||
- name: Tag and Push Hotfix to AWS ECR nonprod registry
|
|
||||||
if: github.ref == 'refs/heads/hotfix'
|
|
||||||
env:
|
|
||||||
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
|
||||||
IMAGE_TAG: ${{ github.sha }}
|
|
||||||
run: |
|
|
||||||
docker tag ${{ steps.setup.outputs.service_name }} \
|
|
||||||
$ECR_REGISTRY/nonprod/${{ steps.setup.outputs.service_name }}:hotfix-${IMAGE_TAG:(-8)}
|
|
||||||
docker push $ECR_REGISTRY/nonprod/${{ steps.setup.outputs.service_name }}:hotfix-${IMAGE_TAG:(-8)}
|
|
||||||
|
|
||||||
- name: Tag and Push Dev to AWS ECR nonprod registry
|
|
||||||
if: github.ref == 'refs/heads/master'
|
|
||||||
env:
|
|
||||||
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
|
||||||
IMAGE_TAG: ${{ github.sha }}
|
|
||||||
run: |
|
|
||||||
docker tag ${{ steps.setup.outputs.service_name }} \
|
|
||||||
$ECR_REGISTRY/nonprod/${{ steps.setup.outputs.service_name }}:dev-${IMAGE_TAG:(-8)}
|
|
||||||
docker push $ECR_REGISTRY/nonprod/${{ steps.setup.outputs.service_name }}:dev-${IMAGE_TAG:(-8)}
|
|
||||||
|
|
||||||
|
|
||||||
upload:
|
upload:
|
||||||
name: Upload
|
name: Upload
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
needs: build-docker
|
needs: build-docker
|
||||||
steps:
|
steps:
|
||||||
|
- name: Set up dotnet
|
||||||
|
uses: actions/setup-dotnet@9211491ffb35dd6a6657ca4f45d43dfe6e97c829
|
||||||
|
with:
|
||||||
|
dotnet-version: "6.0.x"
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||||
|
|
||||||
- name: Restore
|
- name: Restore
|
||||||
run: dotnet tool restore
|
run: dotnet tool restore
|
||||||
|
|
||||||
- name: Make Docker stub
|
- name: Make Docker stub
|
||||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
|
if: github.ref == 'refs/heads/master' ||
|
||||||
|
github.ref == 'refs/heads/rc' ||
|
||||||
|
github.ref == 'refs/heads/hotfix-rc'
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.ref }}" == "rc" ]]; then
|
if [[ "${{ github.ref }}" == "rc" ]]; then
|
||||||
SETUP_IMAGE="bitwarden/setup:rc"
|
SETUP_IMAGE="bitwarden/setup:rc"
|
||||||
elif [[ "${{ github.ref }}" == "hotfix" ]]; then
|
elif [[ "${{ github.ref }}" == "hotfix-rc" ]]; then
|
||||||
SETUP_IMAGE="bitwarden/setup:hotfix"
|
SETUP_IMAGE="bitwarden/setup:hotfix-rc"
|
||||||
else
|
else
|
||||||
SETUP_IMAGE="bitwarden/setup:dev"
|
SETUP_IMAGE="bitwarden/setup:dev"
|
||||||
fi
|
fi
|
||||||
@ -423,12 +423,24 @@ jobs:
|
|||||||
touch $STUB_OUTPUT/env/uid.env
|
touch $STUB_OUTPUT/env/uid.env
|
||||||
cd docker-stub; zip -r ../docker-stub.zip *; cd ..
|
cd docker-stub; zip -r ../docker-stub.zip *; cd ..
|
||||||
|
|
||||||
|
- name: Make Docker stub checksum
|
||||||
|
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||||
|
run: sha256sum docker-stub.zip > docker-stub-sha256.txt
|
||||||
|
|
||||||
- name: Upload Docker stub artifact
|
- name: Upload Docker stub artifact
|
||||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
|
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
|
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||||
with:
|
with:
|
||||||
name: docker-stub.zip
|
name: docker-stub.zip
|
||||||
path: ./docker-stub.zip
|
path: docker-stub.zip
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload Docker stub checksum artifact
|
||||||
|
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
|
||||||
|
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||||
|
with:
|
||||||
|
name: docker-stub-sha256.txt
|
||||||
|
path: docker-stub-sha256.txt
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Build Swagger
|
- name: Build Swagger
|
||||||
@ -446,21 +458,24 @@ jobs:
|
|||||||
cd ../..
|
cd ../..
|
||||||
env:
|
env:
|
||||||
ASPNETCORE_ENVIRONMENT: Production
|
ASPNETCORE_ENVIRONMENT: Production
|
||||||
swaggerGen: 'True'
|
swaggerGen: "True"
|
||||||
|
DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX: 2
|
||||||
|
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
|
||||||
|
|
||||||
- name: Upload Swagger artifact
|
- name: Upload Swagger artifact
|
||||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
|
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||||
with:
|
with:
|
||||||
name: swagger.json
|
name: swagger.json
|
||||||
path: ./swagger.json
|
path: swagger.json
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
check-failures:
|
check-failures:
|
||||||
name: Check for failures
|
name: Check for failures
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- cloc
|
- cloc
|
||||||
|
- lint
|
||||||
- testing
|
- testing
|
||||||
- build-artifacts
|
- build-artifacts
|
||||||
- build-docker
|
- build-docker
|
||||||
@ -470,9 +485,10 @@ jobs:
|
|||||||
if: |
|
if: |
|
||||||
github.ref == 'refs/heads/master'
|
github.ref == 'refs/heads/master'
|
||||||
|| github.ref == 'refs/heads/rc'
|
|| github.ref == 'refs/heads/rc'
|
||||||
|| github.ref == 'refs/heads/hotfix'
|
|| github.ref == 'refs/heads/hotfix-rc'
|
||||||
env:
|
env:
|
||||||
CLOC_STATUS: ${{ needs.cloc.result }}
|
CLOC_STATUS: ${{ needs.cloc.result }}
|
||||||
|
LINT_STATUS: ${{ needs.lint.result }}
|
||||||
TESTING_STATUS: ${{ needs.testing.result }}
|
TESTING_STATUS: ${{ needs.testing.result }}
|
||||||
BUILD_ARTIFACTS_STATUS: ${{ needs.build-artifacts.result }}
|
BUILD_ARTIFACTS_STATUS: ${{ needs.build-artifacts.result }}
|
||||||
BUILD_DOCKER_STATUS: ${{ needs.build-docker.result }}
|
BUILD_DOCKER_STATUS: ${{ needs.build-docker.result }}
|
||||||
@ -480,6 +496,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
if [ "$CLOC_STATUS" = "failure" ]; then
|
if [ "$CLOC_STATUS" = "failure" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
|
elif [ "$LINT_STATUS" = "failure" ]; then
|
||||||
|
exit 1
|
||||||
elif [ "$TESTING_STATUS" = "failure" ]; then
|
elif [ "$TESTING_STATUS" = "failure" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
elif [ "$BUILD_ARTIFACTS_STATUS" = "failure" ]; then
|
elif [ "$BUILD_ARTIFACTS_STATUS" = "failure" ]; then
|
||||||
@ -491,21 +509,21 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Login to Azure - Prod Subscription
|
- name: Login to Azure - Prod Subscription
|
||||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
|
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
keyvault: "bitwarden-prod-kv"
|
keyvault: "bitwarden-prod-kv"
|
||||||
secrets: "devops-alerts-slack-webhook-url"
|
secrets: "devops-alerts-slack-webhook-url"
|
||||||
|
|
||||||
- name: Notify Slack on failure
|
- name: Notify Slack on failure
|
||||||
uses: act10ns/slack@e4e71685b9b239384b0f676a63c32367f59c2522 # v1.2.2
|
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
|
||||||
if: failure()
|
if: failure()
|
||||||
env:
|
env:
|
||||||
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
||||||
|
66
.github/workflows/cleanup-after-pr.yml
vendored
Normal file
66
.github/workflows/cleanup-after-pr.yml
vendored
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
name: Clean After PR
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-docker:
|
||||||
|
name: Remove feature branch docker images
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||||
|
|
||||||
|
########## ACR ##########
|
||||||
|
- name: Login to Azure - QA Subscription
|
||||||
|
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Login to Azure ACR
|
||||||
|
run: az acr login -n bitwardenqa
|
||||||
|
|
||||||
|
########## Remove Docker images ##########
|
||||||
|
- name: Remove the docker image from ACR
|
||||||
|
env:
|
||||||
|
REGISTRY_NAME: bitwardenqa
|
||||||
|
SERVICES: |
|
||||||
|
services:
|
||||||
|
- Admin
|
||||||
|
- Api
|
||||||
|
- Attachments
|
||||||
|
- Events
|
||||||
|
- EventsProcessor
|
||||||
|
- Icons
|
||||||
|
- Identity
|
||||||
|
- K8S-Proxy
|
||||||
|
- MsSql
|
||||||
|
- Nginx
|
||||||
|
- Notifications
|
||||||
|
- Server
|
||||||
|
- Setup
|
||||||
|
- Sso
|
||||||
|
run: |
|
||||||
|
for SERVICE in $(echo "${{ env.SERVICES }}" | yq e ".services[]" - )
|
||||||
|
do
|
||||||
|
SERVICE_NAME=$(echo $SERVICE | awk '{print tolower($0)}')
|
||||||
|
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name
|
||||||
|
|
||||||
|
echo "[*] Checking if remote exists: $REGISTRY_NAME.azurecr.io/$SERVICE_NAME:$IMAGE_TAG"
|
||||||
|
TAG_EXISTS=$(
|
||||||
|
az acr repository show-tags --name $REGISTRY_NAME --repository $SERVICE_NAME \
|
||||||
|
| jq --arg $TAG "$IMAGE_TAG" -e '. | any(. == "$TAG")'
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ "$TAG_EXISTS" == "true" ]]; then
|
||||||
|
echo "[*] Tag exists. Removing tag"
|
||||||
|
az acr repository delete --name $REGISTRY_NAME --image $SERVICE_NAME:$IMAGE_TAG --yes
|
||||||
|
else
|
||||||
|
echo "[*] Tag does not exist. No action needed"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Log out of Docker
|
||||||
|
run: docker logout
|
121
.github/workflows/container-registry-purge.yml
vendored
Normal file
121
.github/workflows/container-registry-purge.yml
vendored
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
---
|
||||||
|
name: Container Registry Purge
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * SUN'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
purge:
|
||||||
|
name: Purge old images
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: bitwardenqa
|
||||||
|
- name: bitwardenprod
|
||||||
|
steps:
|
||||||
|
- name: Login to Azure
|
||||||
|
if: matrix.name == 'bitwardenprod'
|
||||||
|
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Login to Azure
|
||||||
|
if: matrix.name == 'bitwardenqa'
|
||||||
|
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Purge images
|
||||||
|
env:
|
||||||
|
REGISTRY: ${{ matrix.name }}
|
||||||
|
AGO_DUR_VER: "180d"
|
||||||
|
AGO_DUR: "30d"
|
||||||
|
run: |
|
||||||
|
REPO_LIST=$(az acr repository list -n $REGISTRY -o tsv)
|
||||||
|
for REPO in $REPO_LIST
|
||||||
|
do
|
||||||
|
|
||||||
|
PURGE_LATEST=""
|
||||||
|
PURGE_VERSION=""
|
||||||
|
PURGE_ELSE=""
|
||||||
|
|
||||||
|
TAG_LIST=$(az acr repository show-tags -n $REGISTRY --repository $REPO -o tsv)
|
||||||
|
for TAG in $TAG_LIST
|
||||||
|
do
|
||||||
|
if [ $TAG = "latest" ] || [ $TAG = "dev" ]; then
|
||||||
|
PURGE_LATEST+="--filter '$REPO:$TAG' "
|
||||||
|
elif [[ $TAG =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||||
|
PURGE_VERSION+="--filter '$REPO:$TAG' "
|
||||||
|
else
|
||||||
|
PURGE_ELSE+="--filter '$REPO:$TAG' "
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ! -z "$PURGE_LATEST" ]
|
||||||
|
then
|
||||||
|
PURGE_LATEST_CMD="acr purge $PURGE_LATEST --ago $AGO_DUR_VER --untagged --keep 1"
|
||||||
|
az acr run --cmd "$PURGE_LATEST_CMD" --registry $REGISTRY /dev/null &
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$PURGE_VERSION" ]
|
||||||
|
then
|
||||||
|
PURGE_VERSION_CMD="acr purge $PURGE_VERSION --ago $AGO_DUR_VER --untagged"
|
||||||
|
az acr run --cmd "$PURGE_VERSION_CMD" --registry $REGISTRY /dev/null &
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "$PURGE_ELSE" ]
|
||||||
|
then
|
||||||
|
PURGE_ELSE_CMD="acr purge $PURGE_ELSE --ago $AGO_DUR --untagged"
|
||||||
|
az acr run --cmd "$PURGE_ELSE_CMD" --registry $REGISTRY /dev/null &
|
||||||
|
fi
|
||||||
|
|
||||||
|
wait
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
check-failures:
|
||||||
|
name: Check for failures
|
||||||
|
if: always()
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
needs:
|
||||||
|
- purge
|
||||||
|
steps:
|
||||||
|
- name: Check if any job failed
|
||||||
|
if: |
|
||||||
|
github.ref == 'refs/heads/master'
|
||||||
|
|| github.ref == 'refs/heads/rc'
|
||||||
|
|| github.ref == 'refs/heads/hotfix-rc'
|
||||||
|
env:
|
||||||
|
PURGE_STATUS: ${{ needs.purge.result }}
|
||||||
|
run: |
|
||||||
|
if [ "$PURGE_STATUS" = "failure" ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Login to Azure - Prod Subscription
|
||||||
|
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Retrieve secrets
|
||||||
|
id: retrieve-secrets
|
||||||
|
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
keyvault: "bitwarden-prod-kv"
|
||||||
|
secrets: "devops-alerts-slack-webhook-url"
|
||||||
|
|
||||||
|
- name: Notify Slack on failure
|
||||||
|
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
||||||
|
with:
|
||||||
|
status: ${{ job.status }}
|
107
.github/workflows/database.yml
vendored
Normal file
107
.github/workflows/database.yml
vendored
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
name: Validate Database
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches-ignore:
|
||||||
|
- 'l10n_master'
|
||||||
|
- 'gh-pages'
|
||||||
|
paths:
|
||||||
|
- 'src/Sql/**'
|
||||||
|
- 'util/Migrator/**'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
- 'rc'
|
||||||
|
paths:
|
||||||
|
- 'src/Sql/**'
|
||||||
|
- 'util/Migrator/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build DACPAC
|
||||||
|
runs-on: windows-2022
|
||||||
|
env:
|
||||||
|
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||||
|
steps:
|
||||||
|
- name: Set up dotnet
|
||||||
|
uses: actions/setup-dotnet@9211491ffb35dd6a6657ca4f45d43dfe6e97c829
|
||||||
|
with:
|
||||||
|
dotnet-version: '6.0.x'
|
||||||
|
- name: Set up MSBuild
|
||||||
|
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
||||||
|
|
||||||
|
- name: Print environment
|
||||||
|
run: |
|
||||||
|
dotnet --info
|
||||||
|
msbuild -version
|
||||||
|
nuget help | grep Version
|
||||||
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||||
|
|
||||||
|
- name: Build DACPAC
|
||||||
|
run: msbuild src/Sql/Sql.sqlproj /p:Configuration=Release /verbosity:minimal
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Upload DACPAC
|
||||||
|
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||||
|
with:
|
||||||
|
name: sql.dacpac
|
||||||
|
path: src/Sql/bin/Release/Sql.dacpac
|
||||||
|
|
||||||
|
validate:
|
||||||
|
name: Validate
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||||
|
|
||||||
|
- name: Download dacpac
|
||||||
|
uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741
|
||||||
|
with:
|
||||||
|
name: sql.dacpac
|
||||||
|
|
||||||
|
- name: Docker Compose up
|
||||||
|
working-directory: "dev"
|
||||||
|
run: |
|
||||||
|
cp .env.example .env
|
||||||
|
docker compose --profile mssql up -d
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Migrate
|
||||||
|
working-directory: "dev"
|
||||||
|
run: "pwsh ./migrate.ps1"
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Diff sqlproj to migrations
|
||||||
|
run: /usr/local/sqlpackage/sqlpackage /action:DeployReport /SourceFile:"Sql.dacpac" /TargetConnectionString:"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;" /OutputPath:"report.xml" /p:IgnoreColumnOrder=True /p:IgnoreComments=True
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Upload Report
|
||||||
|
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||||
|
with:
|
||||||
|
name: report.xml
|
||||||
|
path: report.xml
|
||||||
|
|
||||||
|
- name: Validate XML
|
||||||
|
run: |
|
||||||
|
if grep -q "<Operations>" "report.xml"; then
|
||||||
|
echo
|
||||||
|
echo "Migrations are out of sync with sqlproj!"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Report looks good"
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Docker compose down
|
||||||
|
if: ${{ always() }}
|
||||||
|
working-directory: "dev"
|
||||||
|
run: docker compose down
|
||||||
|
shell: pwsh
|
16
.github/workflows/enforce-labels.yml
vendored
Normal file
16
.github/workflows/enforce-labels.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
name: Enforce PR labels
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [labeled, unlabeled, opened, edited, synchronize]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
enforce-label:
|
||||||
|
name: EnforceLabel
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- name: Enforce Label
|
||||||
|
uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024
|
||||||
|
with:
|
||||||
|
BANNED_LABELS: "hold,DB-migrations-changed,needs-qa"
|
27
.github/workflows/linter.yml
vendored
27
.github/workflows/linter.yml
vendored
@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
name: Workflow Linter
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: add-workflow-linter
|
|
||||||
# branches-ignore:
|
|
||||||
# - 'l10n_master'
|
|
||||||
# - 'gh-pages'
|
|
||||||
# workflow_dispatch:
|
|
||||||
# inputs: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cloc:
|
|
||||||
name: CLOC
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
|
||||||
|
|
||||||
- name: Install cloc
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get -y install cloc
|
|
||||||
|
|
||||||
- name: Print lines of code
|
|
||||||
run: cloc --include-lang C#,SQL,Razor,"Bourne Shell",PowerShell,HTML,CSS,Sass,JavaScript,TypeScript --vcs git
|
|
55
.github/workflows/protect-files.yml
vendored
Normal file
55
.github/workflows/protect-files.yml
vendored
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Runs if there are changes to the paths: list.
|
||||||
|
# Starts a matrix job to check for modified files, then sets output based on the results.
|
||||||
|
# The input decides if the label job is ran, adding a label to the PR.
|
||||||
|
---
|
||||||
|
|
||||||
|
name: Protect Files
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- synchronize
|
||||||
|
- unlabeled
|
||||||
|
paths:
|
||||||
|
- "util/Migrator/DbScripts/**.sql"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
changed-files:
|
||||||
|
name: Check for file changes
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
outputs:
|
||||||
|
changes: ${{steps.check-changes.outputs.changes_detected}}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: Database Scripts
|
||||||
|
path: util/Migrator/DbScripts
|
||||||
|
label: "DB-migrations-changed"
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Check for file changes
|
||||||
|
id: check-changes
|
||||||
|
run: |
|
||||||
|
MODIFIED_FILES=$(git diff --name-only --diff-filter=M HEAD~1)
|
||||||
|
|
||||||
|
for file in $MODIFIED_FILES
|
||||||
|
do
|
||||||
|
if [[ $file == *"${{ matrix.path }}"* ]]; then
|
||||||
|
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||||
|
break
|
||||||
|
else echo "changes_detected=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Add label to pull request
|
||||||
|
if: contains(steps.check-changes.outputs.changes_detected, true)
|
||||||
|
uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90
|
||||||
|
with:
|
||||||
|
add-labels: ${{ matrix.label }}
|
137
.github/workflows/qa-deploy.yml
vendored
137
.github/workflows/qa-deploy.yml
vendored
@ -1,137 +0,0 @@
|
|||||||
---
|
|
||||||
name: QA Deploy
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
migrateDb:
|
|
||||||
required: true
|
|
||||||
default: "true"
|
|
||||||
resetDb:
|
|
||||||
required: true
|
|
||||||
default: "false"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
reset-db:
|
|
||||||
name: Reset Database
|
|
||||||
if: ${{ github.event.inputs.resetDb == 'true' }}
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
steps:
|
|
||||||
- name: Reset Test Data - Stub
|
|
||||||
run: |
|
|
||||||
echo "placeholder for cleaning DB"
|
|
||||||
echo "placeholder for loading test dataset"
|
|
||||||
|
|
||||||
|
|
||||||
update-db:
|
|
||||||
name: Update Database
|
|
||||||
if: ${{ github.event.inputs.migrateDb == 'true' }}
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo
|
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
|
||||||
|
|
||||||
- name: Login to Azure
|
|
||||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
|
||||||
with:
|
|
||||||
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
|
|
||||||
|
|
||||||
- name: Retrieve secrets
|
|
||||||
id: retrieve-secrets
|
|
||||||
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
|
|
||||||
with:
|
|
||||||
keyvault: "bitwarden-qa-kv"
|
|
||||||
secrets: "mssql-server-host,
|
|
||||||
mssql-admin-login,
|
|
||||||
mssql-admin-login-password"
|
|
||||||
|
|
||||||
- name: Migrate database
|
|
||||||
env:
|
|
||||||
MSSQL_HOST: ${{ steps.retrieve-secrets.outputs.mssql-server-host }}
|
|
||||||
MSSQL_USER: ${{ steps.retrieve-secrets.outputs.mssql-admin-login }}
|
|
||||||
MSSQL_PASS: ${{ steps.retrieve-secrets.outputs.mssql-admin-login-password }}
|
|
||||||
working-directory: ./util/Migrator/DbScripts
|
|
||||||
run: |
|
|
||||||
echo "Running database migrations..."
|
|
||||||
for f in `ls -v ./*.sql`; do
|
|
||||||
echo "Executing file: ${f}..."
|
|
||||||
sqlcmd -S $MSSQL_HOST -d vault -U $MSSQL_USER -P $MSSQL_PASS -I -i $f
|
|
||||||
done;
|
|
||||||
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
name: Deploy
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
if: always()
|
|
||||||
needs:
|
|
||||||
- reset-db
|
|
||||||
- update-db
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- name: Api
|
|
||||||
- name: Admin
|
|
||||||
- name: Billing
|
|
||||||
- name: Events
|
|
||||||
- name: Sso
|
|
||||||
- name: Identity
|
|
||||||
steps:
|
|
||||||
- name: Setup
|
|
||||||
id: setup
|
|
||||||
run: |
|
|
||||||
NAME_LOWER=$(echo "${{ matrix.name }}" | awk '{print tolower($0)}')
|
|
||||||
echo "Matrix name: ${{ matrix.name }}"
|
|
||||||
echo "NAME_LOWER: $NAME_LOWER"
|
|
||||||
echo "::set-output name=name_lower::$NAME_LOWER"
|
|
||||||
|
|
||||||
BRANCH_NAME=$(echo "$GITHUB_REF" | sed "s#refs/heads/##g")
|
|
||||||
echo "GITHUB_REF: $GITHUB_REF"
|
|
||||||
echo "BRANCH_NAME: $BRANCH_NAME"
|
|
||||||
echo "::set-output name=branch_name::$BRANCH_NAME"
|
|
||||||
|
|
||||||
mkdir publish
|
|
||||||
|
|
||||||
- name: Download latest ${{ matrix.name }} asset from ${{ env.branch_name }}
|
|
||||||
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
|
|
||||||
env:
|
|
||||||
branch_name: ${{ steps.setup.outputs.branch_name }}
|
|
||||||
with:
|
|
||||||
workflow: build.yml
|
|
||||||
workflow_conclusion: success
|
|
||||||
branch: ${{ env.branch_name }}
|
|
||||||
artifacts: ${{ matrix.name }}.zip
|
|
||||||
|
|
||||||
- name: Login to Azure
|
|
||||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
|
||||||
with:
|
|
||||||
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
|
|
||||||
|
|
||||||
- name: Retrieve secrets
|
|
||||||
id: retrieve-secrets
|
|
||||||
env:
|
|
||||||
VAULT_NAME: "bitwarden-qa-kv"
|
|
||||||
run: |
|
|
||||||
webapp_name=$(
|
|
||||||
az keyvault secret show --vault-name $VAULT_NAME \
|
|
||||||
--name appservices-${{ steps.setup.outputs.name_lower }}-webapp-name \
|
|
||||||
--query value --output tsv
|
|
||||||
)
|
|
||||||
echo "::add-mask::$webapp_name"
|
|
||||||
echo "::set-output name=webapp-name::$webapp_name"
|
|
||||||
|
|
||||||
- name: Stop App Service
|
|
||||||
env:
|
|
||||||
AZURE_RESOURCE_GROUP: "bw-qa-env"
|
|
||||||
run: az webapp stop --name ${{ steps.retrieve-secrets.outputs.webapp-name }} --resource-group $AZURE_RESOURCE_GROUP
|
|
||||||
|
|
||||||
- name: Deploy App
|
|
||||||
uses: azure/webapps-deploy@798e43877120eda6a2a690a4f212c545e586ae31
|
|
||||||
with:
|
|
||||||
app-name: ${{ steps.retrieve-secrets.outputs.webapp-name }}
|
|
||||||
package: ./${{ matrix.name }}.zip
|
|
||||||
|
|
||||||
- name: Start App Service
|
|
||||||
env:
|
|
||||||
AZURE_RESOURCE_GROUP: "bw-qa-env"
|
|
||||||
run: az webapp start --name ${{ steps.retrieve-secrets.outputs.webapp-name }} --resource-group $AZURE_RESOURCE_GROUP
|
|
323
.github/workflows/release.yml
vendored
323
.github/workflows/release.yml
vendored
@ -1,56 +1,58 @@
|
|||||||
---
|
---
|
||||||
name: Release
|
name: Release
|
||||||
|
run-name: Release ${{ inputs.release_type }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs: {}
|
inputs:
|
||||||
|
release_type:
|
||||||
|
description: "Release Options"
|
||||||
|
required: true
|
||||||
|
default: "Initial Release"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- Initial Release
|
||||||
|
- Redeploy
|
||||||
|
- Dry Run
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup:
|
setup:
|
||||||
name: Setup
|
name: Setup
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
outputs:
|
outputs:
|
||||||
release_version: ${{ steps.version.outputs.package }}
|
release_version: ${{ steps.version.outputs.version }}
|
||||||
branch-name: ${{ steps.branch.outputs.branch-name }}
|
branch-name: ${{ steps.branch.outputs.branch-name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Branch check
|
- name: Branch check
|
||||||
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
run: |
|
run: |
|
||||||
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix" ]]; then
|
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
|
||||||
echo "==================================="
|
echo "==================================="
|
||||||
echo "[!] Can only release from the 'rc' or 'hotfix' branches"
|
echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches"
|
||||||
echo "==================================="
|
echo "==================================="
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||||
|
|
||||||
- name: Check Release Version
|
- name: Check Release Version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
uses: bitwarden/gh-actions/release-version-check@4cf17a5ff15a995a2daf2b60ba371e5c9907c068
|
||||||
version=$( grep -o "<Version>.*</Version>" Directory.Build.props | grep -o "[0-9]*\.[0-9]*\.[0-9]*")
|
with:
|
||||||
previous_release_tag_version=$(
|
release-type: ${{ github.event.inputs.release_type }}
|
||||||
curl -sL https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest | jq -r ".tag_name"
|
project-type: dotnet
|
||||||
)
|
file: Directory.Build.props
|
||||||
|
|
||||||
if [ "v$version" == "$previous_release_tag_version" ]; then
|
|
||||||
echo "[!] Already released v$version. Please bump version to continue"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "::set-output name=package::$version"
|
|
||||||
|
|
||||||
- name: Get branch name
|
- name: Get branch name
|
||||||
id: branch
|
id: branch
|
||||||
run: |
|
run: |
|
||||||
BRANCH_NAME=$(basename ${{ github.ref }})
|
BRANCH_NAME=$(basename ${{ github.ref }})
|
||||||
echo "::set-output name=branch-name::$BRANCH_NAME"
|
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy
|
name: Deploy
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- setup
|
- setup
|
||||||
strategy:
|
strategy:
|
||||||
@ -70,18 +72,39 @@ jobs:
|
|||||||
NAME_LOWER=$(echo "${{ matrix.name }}" | awk '{print tolower($0)}')
|
NAME_LOWER=$(echo "${{ matrix.name }}" | awk '{print tolower($0)}')
|
||||||
echo "Matrix name: ${{ matrix.name }}"
|
echo "Matrix name: ${{ matrix.name }}"
|
||||||
echo "NAME_LOWER: $NAME_LOWER"
|
echo "NAME_LOWER: $NAME_LOWER"
|
||||||
echo "::set-output name=name_lower::$NAME_LOWER"
|
echo "name_lower=$NAME_LOWER" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Create GitHub deployment for ${{ matrix.name }}
|
||||||
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
|
uses: chrnorm/deployment-action@1b599fe41a0ef1f95191e7f2eec4743f2d7dfc48
|
||||||
|
id: deployment
|
||||||
|
with:
|
||||||
|
token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
initial-status: "in_progress"
|
||||||
|
environment: "Production Cloud"
|
||||||
|
task: "deploy"
|
||||||
|
description: "Deploy from ${{ needs.setup.outputs.branch-name }} branch"
|
||||||
|
|
||||||
- name: Download latest Release ${{ matrix.name }} asset
|
- name: Download latest Release ${{ matrix.name }} asset
|
||||||
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
|
uses: bitwarden/gh-actions/download-artifacts@850faad0cf6c02a8c0dc46eddde2363fbd6c373a
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: ${{ needs.setup.outputs.branch-name }}
|
branch: ${{ needs.setup.outputs.branch-name }}
|
||||||
artifacts: ${{ matrix.name }}.zip
|
artifacts: ${{ matrix.name }}.zip
|
||||||
|
|
||||||
|
- name: Download latest Release ${{ matrix.name }} asset
|
||||||
|
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||||
|
uses: bitwarden/gh-actions/download-artifacts@850faad0cf6c02a8c0dc46eddde2363fbd6c373a
|
||||||
|
with:
|
||||||
|
workflow: build.yml
|
||||||
|
workflow_conclusion: success
|
||||||
|
branch: master
|
||||||
|
artifacts: ${{ matrix.name }}.zip
|
||||||
|
|
||||||
- name: Login to Azure
|
- name: Login to Azure
|
||||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
|
|
||||||
@ -101,22 +124,50 @@ jobs:
|
|||||||
--query value --output tsv
|
--query value --output tsv
|
||||||
)
|
)
|
||||||
echo "::add-mask::$webapp_name"
|
echo "::add-mask::$webapp_name"
|
||||||
echo "::set-output name=webapp-name::$webapp_name"
|
echo "webapp-name=$webapp_name" >> $GITHUB_OUTPUT
|
||||||
echo "::add-mask::$publish_profile"
|
echo "::add-mask::$publish_profile"
|
||||||
echo "::set-output name=publish-profile::$publish_profile"
|
echo "publish-profile=$publish_profile" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Deploy App
|
- name: Deploy App
|
||||||
uses: azure/webapps-deploy@798e43877120eda6a2a690a4f212c545e586ae31
|
uses: azure/webapps-deploy@0b651ed7546ecfc75024011f76944cb9b381ef1e
|
||||||
with:
|
with:
|
||||||
app-name: ${{ steps.retrieve-secrets.outputs.webapp-name }}
|
app-name: ${{ steps.retrieve-secrets.outputs.webapp-name }}
|
||||||
publish-profile: ${{ steps.retrieve-secrets.outputs.publish-profile }}
|
publish-profile: ${{ steps.retrieve-secrets.outputs.publish-profile }}
|
||||||
package: ./${{ matrix.name }}.zip
|
package: ./${{ matrix.name }}.zip
|
||||||
slot-name: "staging"
|
slot-name: "staging"
|
||||||
|
|
||||||
|
- name: Start staging slot
|
||||||
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
|
env:
|
||||||
|
SERVICE: ${{ matrix.name }}
|
||||||
|
WEBAPP_NAME: ${{ steps.retrieve-secrets.outputs.webapp-name }}
|
||||||
|
run: |
|
||||||
|
if [[ "$SERVICE" = "Api" ]] || [[ "$SERVICE" = "Identity" ]]; then
|
||||||
|
RESOURCE_GROUP=bitwardenappservices
|
||||||
|
else
|
||||||
|
RESOURCE_GROUP=bitwarden
|
||||||
|
fi
|
||||||
|
az webapp start -n $WEBAPP_NAME -g $RESOURCE_GROUP -s staging
|
||||||
|
|
||||||
|
- name: Update ${{ matrix.name }} deployment status to Success
|
||||||
|
if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }}
|
||||||
|
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
|
||||||
|
with:
|
||||||
|
token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
state: "success"
|
||||||
|
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||||
|
|
||||||
|
- name: Update ${{ matrix.name }} deployment status to Failure
|
||||||
|
if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }}
|
||||||
|
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
|
||||||
|
with:
|
||||||
|
token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
state: "failure"
|
||||||
|
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||||
|
|
||||||
release-docker:
|
release-docker:
|
||||||
name: Build Docker images
|
name: Build Docker images
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- setup
|
- setup
|
||||||
env:
|
env:
|
||||||
@ -126,94 +177,226 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- service_name: Admin
|
- project_name: Admin
|
||||||
- service_name: Api
|
origin_docker_repo: bitwarden
|
||||||
- service_name: Attachments
|
- project_name: Api
|
||||||
- service_name: Events
|
origin_docker_repo: bitwarden
|
||||||
- service_name: Icons
|
- project_name: Attachments
|
||||||
- service_name: Identity
|
origin_docker_repo: bitwarden
|
||||||
- service_name: K8S-Proxy
|
- project_name: Events
|
||||||
- service_name: MsSql
|
prod_acr: true
|
||||||
- service_name: Nginx
|
origin_docker_repo: bitwarden
|
||||||
- service_name: Notifications
|
- project_name: EventsProcessor
|
||||||
- service_name: Server
|
prod_acr: true
|
||||||
- service_name: Setup
|
origin_docker_repo: bitwardenqa.azurecr.io
|
||||||
- service_name: Sso
|
- project_name: Icons
|
||||||
|
origin_docker_repo: bitwarden
|
||||||
|
prod_acr: true
|
||||||
|
- project_name: Identity
|
||||||
|
origin_docker_repo: bitwarden
|
||||||
|
- project_name: MsSql
|
||||||
|
origin_docker_repo: bitwarden
|
||||||
|
- project_name: Nginx
|
||||||
|
origin_docker_repo: bitwarden
|
||||||
|
- project_name: Notifications
|
||||||
|
origin_docker_repo: bitwarden
|
||||||
|
- project_name: Server
|
||||||
|
origin_docker_repo: bitwarden
|
||||||
|
- project_name: Setup
|
||||||
|
origin_docker_repo: bitwarden
|
||||||
|
- project_name: Sso
|
||||||
|
origin_docker_repo: bitwarden
|
||||||
|
- project_name: Scim
|
||||||
|
origin_docker_repo: bitwarden
|
||||||
|
- project_name: Billing
|
||||||
|
origin_docker_repo: bitwardenqa.azurecr.io
|
||||||
steps:
|
steps:
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
|
env:
|
||||||
|
RELEASE_OPTION: ${{ github.event.inputs.release_type }}
|
||||||
run: |
|
run: |
|
||||||
whoami
|
whoami
|
||||||
docker --version
|
docker --version
|
||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
echo "Github Release Option: $RELEASE_OPTION"
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||||
|
|
||||||
|
- name: Setup project name
|
||||||
|
id: setup
|
||||||
|
run: |
|
||||||
|
PROJECT_NAME=$(echo "${{ matrix.project_name }}" | awk '{print tolower($0)}')
|
||||||
|
echo "Matrix name: ${{ matrix.project_name }}"
|
||||||
|
echo "PROJECT_NAME: $PROJECT_NAME"
|
||||||
|
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
########## DockerHub ##########
|
||||||
- name: Setup DCT
|
- name: Setup DCT
|
||||||
id: setup-dct
|
id: setup-dct
|
||||||
|
if: matrix.origin_docker_repo == 'bitwarden'
|
||||||
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
|
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
|
||||||
with:
|
with:
|
||||||
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
azure-keyvault-name: "bitwarden-prod-kv"
|
azure-keyvault-name: "bitwarden-prod-kv"
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Pull latest project image
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
if: matrix.origin_docker_repo == 'bitwarden'
|
||||||
|
|
||||||
- name: Setup service name
|
|
||||||
id: setup
|
|
||||||
run: |
|
|
||||||
SERVICE_NAME=$(echo "${{ matrix.service_name }}" | awk '{print tolower($0)}')
|
|
||||||
echo "Matrix name: ${{ matrix.service_name }}"
|
|
||||||
echo "SERVICE_NAME: $SERVICE_NAME"
|
|
||||||
echo "::set-output name=service_name::$SERVICE_NAME"
|
|
||||||
|
|
||||||
- name: Pull latest selfhost image
|
|
||||||
env:
|
env:
|
||||||
SERVICE_NAME: ${{ steps.setup.outputs.service_name }}
|
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||||
run: docker pull bitwarden/$SERVICE_NAME:$_BRANCH_NAME
|
run: |
|
||||||
|
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
||||||
|
docker pull bitwarden/$PROJECT_NAME:latest
|
||||||
|
else
|
||||||
|
docker pull bitwarden/$PROJECT_NAME:$_BRANCH_NAME
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Tag version and latest
|
- name: Tag version and latest
|
||||||
|
if: matrix.origin_docker_repo == 'bitwarden'
|
||||||
env:
|
env:
|
||||||
SERVICE_NAME: ${{ steps.setup.outputs.service_name }}
|
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||||
run: |
|
run: |
|
||||||
docker tag bitwarden/$SERVICE_NAME:$_BRANCH_NAME bitwarden/$SERVICE_NAME:$_RELEASE_VERSION
|
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
||||||
docker tag bitwarden/$SERVICE_NAME:$_BRANCH_NAME bitwarden/$SERVICE_NAME:latest
|
docker tag bitwarden/$PROJECT_NAME:latest bitwarden/$PROJECT_NAME:dryrun
|
||||||
|
else
|
||||||
- name: List Docker images
|
docker tag bitwarden/$PROJECT_NAME:$_BRANCH_NAME bitwarden/$PROJECT_NAME:$_RELEASE_VERSION
|
||||||
run: docker images
|
fi
|
||||||
|
|
||||||
- name: Push version and latest image
|
- name: Push version and latest image
|
||||||
|
if: ${{ github.event.inputs.release_type != 'Dry Run' && matrix.origin_docker_repo == 'bitwarden' }}
|
||||||
env:
|
env:
|
||||||
DOCKER_CONTENT_TRUST: 1
|
DOCKER_CONTENT_TRUST: 1
|
||||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
||||||
SERVICE_NAME: ${{ steps.setup.outputs.service_name }}
|
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||||
|
run: docker push bitwarden/$PROJECT_NAME:$_RELEASE_VERSION
|
||||||
|
|
||||||
|
- name: Log out of Docker and disable Docker Notary
|
||||||
|
if: matrix.origin_docker_repo == 'bitwarden'
|
||||||
run: |
|
run: |
|
||||||
docker push bitwarden/$SERVICE_NAME:$_RELEASE_VERSION
|
docker logout
|
||||||
docker push bitwarden/$SERVICE_NAME:latest
|
echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
########## ACR QA ##########
|
||||||
|
- name: Login to Azure - QA Subscription
|
||||||
|
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Login to Azure ACR
|
||||||
|
run: az acr login -n bitwardenqa
|
||||||
|
|
||||||
|
- name: Pull latest project image
|
||||||
|
if: matrix.origin_docker_repo == 'bitwardenqa.azurecr.io'
|
||||||
|
env:
|
||||||
|
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||||
|
REGISTRY: bitwardenqa.azurecr.io
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
||||||
|
docker pull $REGISTRY/$PROJECT_NAME:latest
|
||||||
|
else
|
||||||
|
docker pull $REGISTRY/$PROJECT_NAME:$_BRANCH_NAME
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Tag version and latest
|
||||||
|
env:
|
||||||
|
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||||
|
REGISTRY: bitwardenqa.azurecr.io
|
||||||
|
ORIGIN_REGISTRY: ${{ matrix.origin_docker_repo }}
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
||||||
|
docker tag $ORIGIN_REGISTRY/$PROJECT_NAME:latest $REGISTRY/$PROJECT_NAME:dryrun
|
||||||
|
else
|
||||||
|
docker tag $ORIGIN_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION
|
||||||
|
docker tag $ORIGIN_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $REGISTRY/$PROJECT_NAME:latest
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Push version and latest image
|
||||||
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
|
env:
|
||||||
|
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||||
|
REGISTRY: bitwardenqa.azurecr.io
|
||||||
|
run: |
|
||||||
|
docker push $REGISTRY/$PROJECT_NAME:latest
|
||||||
|
docker push $REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION
|
||||||
|
|
||||||
- name: Log out of Docker
|
- name: Log out of Docker
|
||||||
run: docker logout
|
run: docker logout
|
||||||
|
|
||||||
|
########## ACR PROD ##########
|
||||||
|
- name: Login to Azure - PROD Subscription
|
||||||
|
if: matrix.prod_acr == true
|
||||||
|
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Login to Azure ACR
|
||||||
|
if: matrix.prod_acr == true
|
||||||
|
run: az acr login -n bitwardenprod
|
||||||
|
|
||||||
|
- name: Tag version and latest
|
||||||
|
if: matrix.prod_acr == true
|
||||||
|
env:
|
||||||
|
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||||
|
REGISTRY: bitwardenprod.azurecr.io
|
||||||
|
ORIGIN_REGISTRY: ${{ matrix.origin_docker_repo }}
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
||||||
|
docker tag $ORIGIN_REGISTRY/$PROJECT_NAME:latest $REGISTRY/$PROJECT_NAME:dryrun
|
||||||
|
else
|
||||||
|
docker tag $ORIGIN_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION
|
||||||
|
docker tag $ORIGIN_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $REGISTRY/$PROJECT_NAME:latest
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Push version and latest image
|
||||||
|
if: ${{ github.event.inputs.release_type != 'Dry Run' && matrix.prod_acr == true }}
|
||||||
|
env:
|
||||||
|
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||||
|
REGISTRY: bitwardenprod.azurecr.io
|
||||||
|
run: |
|
||||||
|
docker push $REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION
|
||||||
|
docker push $REGISTRY/$PROJECT_NAME:latest
|
||||||
|
|
||||||
|
- name: Log out of Docker
|
||||||
|
if: matrix.prod_acr == true
|
||||||
|
run: docker logout
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Create GitHub Release
|
name: Create GitHub Release
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- setup
|
- setup
|
||||||
- deploy
|
- deploy
|
||||||
steps:
|
steps:
|
||||||
- name: Download latest Release docker-stub
|
- name: Download latest Release docker-stub
|
||||||
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
|
uses: bitwarden/gh-actions/download-artifacts@850faad0cf6c02a8c0dc46eddde2363fbd6c373a
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: ${{ needs.setup.outputs.branch-name }}
|
branch: ${{ needs.setup.outputs.branch-name }}
|
||||||
artifacts: "docker-stub.zip,
|
artifacts: "docker-stub.zip,
|
||||||
|
docker-stub-sha256.txt,
|
||||||
|
swagger.json"
|
||||||
|
|
||||||
|
- name: Download latest Release docker-stub
|
||||||
|
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||||
|
uses: bitwarden/gh-actions/download-artifacts@850faad0cf6c02a8c0dc46eddde2363fbd6c373a
|
||||||
|
with:
|
||||||
|
workflow: build.yml
|
||||||
|
workflow_conclusion: success
|
||||||
|
branch: master
|
||||||
|
artifacts: "docker-stub.zip,
|
||||||
|
docker-stub-sha256.txt,
|
||||||
swagger.json"
|
swagger.json"
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: ncipollo/release-action@95215a3cb6e6a1908b3c44e00b4fdb15548b1e09
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
|
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01
|
||||||
with:
|
with:
|
||||||
artifacts: 'docker-stub.zip,
|
artifacts: "docker-stub.zip,
|
||||||
swagger.json'
|
docker-stub-sha256.txt,
|
||||||
|
swagger.json"
|
||||||
commit: ${{ github.sha }}
|
commit: ${{ github.sha }}
|
||||||
tag: "v${{ needs.setup.outputs.release_version }}"
|
tag: "v${{ needs.setup.outputs.release_version }}"
|
||||||
name: "Version ${{ needs.setup.outputs.release_version }}"
|
name: "Version ${{ needs.setup.outputs.release_version }}"
|
||||||
|
30
.github/workflows/stale-bot.yml
vendored
Normal file
30
.github/workflows/stale-bot.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
name: 'Close stale issues and PRs'
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule: # Run once a day at 5.23am (arbitrary but should avoid peak loads on the hour)
|
||||||
|
- cron: '23 5 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
name: 'Check for stale issues and PRs'
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- name: 'Run stale action'
|
||||||
|
uses: actions/stale@3cc123766321e9f15a6676375c154ccffb12a358 # v5.0.0
|
||||||
|
with:
|
||||||
|
stale-issue-label: 'needs-reply'
|
||||||
|
stale-pr-label: 'needs-changes'
|
||||||
|
days-before-stale: -1 # Do not apply the stale labels automatically, this is a manual process
|
||||||
|
days-before-issue-close: 14 # Close issue if no further activity after X days
|
||||||
|
days-before-pr-close: 21 # Close PR if no further activity after X days
|
||||||
|
close-issue-message: |
|
||||||
|
We need more information before we can help you with your problem. As we haven’t heard from you recently, this issue will be closed.
|
||||||
|
|
||||||
|
If this happens again or continues to be an problem, please respond to this issue with the information we’ve requested and anything else relevant.
|
||||||
|
close-pr-message: |
|
||||||
|
We can’t merge your pull request until you make the changes we’ve requested. As we haven’t heard from you recently, this pull request will be closed.
|
||||||
|
|
||||||
|
If you’re still working on this, please respond here after you’ve made the changes we’ve requested and our team will re-open it for further review.
|
||||||
|
|
||||||
|
Please make sure to resolve any conflicts with the master branch before requesting another review.
|
60
.github/workflows/stop-staging-slots.yml
vendored
Normal file
60
.github/workflows/stop-staging-slots.yml
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
name: Stop Staging Slots
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs: {}
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stop-slots:
|
||||||
|
name: Stop Slots
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: Api
|
||||||
|
- name: Admin
|
||||||
|
- name: Billing
|
||||||
|
- name: Events
|
||||||
|
- name: Sso
|
||||||
|
- name: Identity
|
||||||
|
steps:
|
||||||
|
- name: Setup
|
||||||
|
id: setup
|
||||||
|
run: |
|
||||||
|
NAME_LOWER=$(echo "${{ matrix.name }}" | awk '{print tolower($0)}')
|
||||||
|
echo "Matrix name: ${{ matrix.name }}"
|
||||||
|
echo "NAME_LOWER: $NAME_LOWER"
|
||||||
|
echo "name_lower=$NAME_LOWER" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Login to Azure
|
||||||
|
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Retrieve secrets
|
||||||
|
id: retrieve-secrets
|
||||||
|
env:
|
||||||
|
VAULT_NAME: "bitwarden-prod-kv"
|
||||||
|
run: |
|
||||||
|
webapp_name=$(
|
||||||
|
az keyvault secret show --vault-name $VAULT_NAME \
|
||||||
|
--name appservices-${{ steps.setup.outputs.name_lower }}-webapp-name \
|
||||||
|
--query value --output tsv
|
||||||
|
)
|
||||||
|
echo "::add-mask::$webapp_name"
|
||||||
|
echo "webapp-name=$webapp_name" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Stop staging slot
|
||||||
|
env:
|
||||||
|
SERVICE: ${{ matrix.name }}
|
||||||
|
WEBAPP_NAME: ${{ steps.retrieve-secrets.outputs.webapp-name }}
|
||||||
|
run: |
|
||||||
|
if [[ "$SERVICE" = "Api" ]] || [[ "$SERVICE" = "Identity" ]]; then
|
||||||
|
RESOURCE_GROUP=bitwardenappservices
|
||||||
|
else
|
||||||
|
RESOURCE_GROUP=bitwarden
|
||||||
|
fi
|
||||||
|
az webapp stop -n $WEBAPP_NAME -g $RESOURCE_GROUP -s staging
|
93
.github/workflows/version-bump.yml
vendored
Normal file
93
.github/workflows/version-bump.yml
vendored
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
name: Version Bump
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version_number:
|
||||||
|
description: "New Version"
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bump_props_version:
|
||||||
|
name: "Create version_bump_${{ github.event.inputs.version_number }} branch"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout Branch
|
||||||
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||||
|
|
||||||
|
- name: Login to Azure - Prod Subscription
|
||||||
|
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Retrieve secrets
|
||||||
|
id: retrieve-secrets
|
||||||
|
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
||||||
|
with:
|
||||||
|
keyvault: "bitwarden-prod-kv"
|
||||||
|
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||||
|
|
||||||
|
- name: Import GPG key
|
||||||
|
uses: crazy-max/ghaction-import-gpg@c8bb57c57e8df1be8c73ff3d59deab1dbc00e0d1
|
||||||
|
with:
|
||||||
|
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
||||||
|
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
||||||
|
git_user_signingkey: true
|
||||||
|
git_commit_gpgsign: true
|
||||||
|
|
||||||
|
- name: Create Version Branch
|
||||||
|
run: git switch -c version_bump_${{ github.event.inputs.version_number }}
|
||||||
|
|
||||||
|
- name: Bump Version - Props
|
||||||
|
uses: bitwarden/gh-actions/version-bump@6a42772f8849107fd457cf47cd9c7e224be44e55
|
||||||
|
with:
|
||||||
|
version: ${{ github.event.inputs.version_number }}
|
||||||
|
file_path: "Directory.Build.props"
|
||||||
|
|
||||||
|
- name: Setup git
|
||||||
|
run: |
|
||||||
|
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||||
|
git config --local user.name "bitwarden-devops-bot"
|
||||||
|
|
||||||
|
- name: Check if version changed
|
||||||
|
id: version-changed
|
||||||
|
run: |
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
echo "changes_to_commit=TRUE" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "changes_to_commit=FALSE" >> $GITHUB_OUTPUT
|
||||||
|
echo "No changes to commit!";
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit files
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
|
run: git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
|
||||||
|
|
||||||
|
- name: Push changes
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
|
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||||
|
|
||||||
|
- name: Create Version PR
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
|
env:
|
||||||
|
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
|
||||||
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
BASE_BRANCH: master
|
||||||
|
TITLE: "Bump version to ${{ github.event.inputs.version_number }}"
|
||||||
|
run: |
|
||||||
|
gh pr create --title "$TITLE" \
|
||||||
|
--base "$BASE" \
|
||||||
|
--head "$PR_BRANCH" \
|
||||||
|
--label "version update" \
|
||||||
|
--label "automated pr" \
|
||||||
|
--body "
|
||||||
|
## Type of change
|
||||||
|
- [ ] Bug fix
|
||||||
|
- [ ] New feature development
|
||||||
|
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
|
||||||
|
- [ ] Build/deploy pipeline (DevOps)
|
||||||
|
- [X] Other
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Automated version bump to ${{ github.event.inputs.version_number }}"
|
11
.github/workflows/workflow-linter.yml
vendored
Normal file
11
.github/workflows/workflow-linter.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
name: Workflow Linter
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/**
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
call-workflow:
|
||||||
|
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@master
|
418
.vscode/launch.json
vendored
Normal file
418
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,418 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "Min Server",
|
||||||
|
"configurations": [
|
||||||
|
"Identity",
|
||||||
|
"API"
|
||||||
|
],
|
||||||
|
"presentation": {
|
||||||
|
"hidden": false,
|
||||||
|
"group": "AA_compounds",
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
"stopAll": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Admin, API, Identity",
|
||||||
|
"configurations": [
|
||||||
|
"Admin",
|
||||||
|
"API",
|
||||||
|
"Identity"
|
||||||
|
],
|
||||||
|
"presentation": {
|
||||||
|
"hidden": false,
|
||||||
|
"group": "AA_compounds",
|
||||||
|
"order": 3
|
||||||
|
},
|
||||||
|
"stopAll": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Full Server",
|
||||||
|
"configurations": [
|
||||||
|
"Admin",
|
||||||
|
"API",
|
||||||
|
"EventsProcessor",
|
||||||
|
"Identity",
|
||||||
|
"Sso",
|
||||||
|
"Icons",
|
||||||
|
"Billing",
|
||||||
|
"Notifications"
|
||||||
|
],
|
||||||
|
"presentation": {
|
||||||
|
"hidden": false,
|
||||||
|
"group": "AA_compounds",
|
||||||
|
"order": 4
|
||||||
|
},
|
||||||
|
"stopAll": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Self Host: Bit",
|
||||||
|
"configurations": [
|
||||||
|
"Admin-SelfHost",
|
||||||
|
"API-SelfHost",
|
||||||
|
"EventsProcessor-SelfHost",
|
||||||
|
"Identity-SelfHost",
|
||||||
|
"Sso-SelfHost",
|
||||||
|
"Notifications-SelfHost"
|
||||||
|
],
|
||||||
|
"presentation": {
|
||||||
|
"hidden": false,
|
||||||
|
"group": "AA_compounds",
|
||||||
|
"order": 2
|
||||||
|
},
|
||||||
|
"stopAll": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Self Host: OSS",
|
||||||
|
"configurations": [
|
||||||
|
"Admin-SelfHost",
|
||||||
|
"API-SelfHost",
|
||||||
|
"EventsProcessor-SelfHost",
|
||||||
|
"Identity-SelfHost",
|
||||||
|
],
|
||||||
|
"presentation": {
|
||||||
|
"hidden": false,
|
||||||
|
"group": "AA_compounds",
|
||||||
|
"order": 99
|
||||||
|
},
|
||||||
|
"stopAll": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Identity",
|
||||||
|
"presentation": {
|
||||||
|
"hidden": false,
|
||||||
|
"group": "cloud",
|
||||||
|
"order": 10
|
||||||
|
},
|
||||||
|
"requireExactSource": true,
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "buildIdentity",
|
||||||
|
"program": "${workspaceFolder}/src/Identity/bin/Debug/net6.0/Identity.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src/Identity",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "API",
|
||||||
|
"presentation": {
|
||||||
|
"hidden": false,
|
||||||
|
"group": "cloud",
|
||||||
|
"order": 10
|
||||||
|
},
|
||||||
|
"requireExactSource": true,
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "buildAPI",
|
||||||
|
"program": "${workspaceFolder}/src/Api/bin/Debug/net6.0/Api.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src/Api",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Billing",
|
||||||
|
"presentation": {
|
||||||
|
"hidden": false,
|
||||||
|
"group": "cloud",
|
||||||
|
"order": 10
|
||||||
|
},
|
||||||
|
"requireExactSource": true,
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "buildBilling",
|
||||||
|
"program": "${workspaceFolder}/src/Billing/bin/Debug/net6.0/Billing.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src/Billing",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Admin",
|
||||||
|
"presentation": {
|
||||||
|
"hidden": false,
|
||||||
|
"group": "cloud",
|
||||||
|
"order": 20
|
||||||
|
},
|
||||||
|
"requireExactSource": true,
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "buildAdmin",
|
||||||
|
"OS-COMMENT4": "If you have changed target frameworks, make sure to update the program path.",
|
||||||
|
"program": "${workspaceFolder}/src/Admin/bin/Debug/net6.0/Admin.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src/Admin",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"OS-COMMENT5": "Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser",
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sso",
|
||||||
|
"presentation": {
|
||||||
|
"hidden": false,
|
||||||
|
"group": "cloud",
|
||||||
|
"order": 50
|
||||||
|
},
|
||||||
|
"requireExactSource": true,
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "buildSso",
|
||||||
|
"program": "${workspaceFolder}/bitwarden_license/src/Sso/bin/Debug/net6.0/Sso.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/bitwarden_license/src/Sso",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "EventsProcessor",
|
||||||
|
"presentation": {
|
||||||
|
"hidden": false,
|
||||||
|
"group": "cloud",
|
||||||
|
"order": 90
|
||||||
|
},
|
||||||
|
"requireExactSource": true,
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "buildEventsProcessor",
|
||||||
|
"program": "${workspaceFolder}/src/EventsProcessor/bin/Debug/net6.0/EventsProcessor.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src/EventsProcessor",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Icons",
|
||||||
|
"presentation": {
|
||||||
|
"hidden": false,
|
||||||
|
"group": "cloud",
|
||||||
|
"order": 90
|
||||||
|
},
|
||||||
|
"requireExactSource": true,
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "buildIcons",
|
||||||
|
"program": "${workspaceFolder}/src/Icons/bin/Debug/net6.0/Icons.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src/Icons",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Notifications",
|
||||||
|
"presentation": {
|
||||||
|
"hidden": true,
|
||||||
|
"group": "cloud",
|
||||||
|
"order": 100
|
||||||
|
},
|
||||||
|
"requireExactSource": true,
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "buildNotifications",
|
||||||
|
"program": "${workspaceFolder}/src/Notifications/bin/Debug/net6.0/Notifications.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src/Notifications",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Identity-SelfHost",
|
||||||
|
"presentation": {
|
||||||
|
"hidden": true,
|
||||||
|
"group": "self-host",
|
||||||
|
"order": 999
|
||||||
|
},
|
||||||
|
"requireExactSource": true,
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "buildIdentity",
|
||||||
|
"program": "${workspaceFolder}/src/Identity/bin/Debug/net6.0/Identity.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src/Identity",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"ASPNETCORE_URLS": "http://localhost:33657",
|
||||||
|
"developSelfHosted": "true"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "API-SelfHost",
|
||||||
|
"presentation": {
|
||||||
|
"hidden": true,
|
||||||
|
"group": "self-host",
|
||||||
|
"order": 999
|
||||||
|
},
|
||||||
|
"requireExactSource": true,
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "buildAPI",
|
||||||
|
"program": "${workspaceFolder}/src/Api/bin/Debug/net6.0/Api.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src/Api",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"ASPNETCORE_URLS": "http://localhost:4001",
|
||||||
|
"developSelfHosted": "true"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Admin-SelfHost",
|
||||||
|
"presentation": {
|
||||||
|
"hidden": true,
|
||||||
|
"group": "self-host",
|
||||||
|
"order": 999
|
||||||
|
},
|
||||||
|
"requireExactSource": true,
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "buildAdmin",
|
||||||
|
"OS-COMMENT4": "If you have changed target frameworks, make sure to update the program path.",
|
||||||
|
"program": "${workspaceFolder}/src/Admin/bin/Debug/net6.0/Admin.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src/Admin",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"OS-COMMENT5": "Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser",
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"ASPNETCORE_URLS": "http://localhost:62912",
|
||||||
|
"developSelfHosted": "true"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sso-SelfHost",
|
||||||
|
"presentation": {
|
||||||
|
"hidden": true,
|
||||||
|
"group": "self-host",
|
||||||
|
"order": 999
|
||||||
|
},
|
||||||
|
"requireExactSource": true,
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "buildSso",
|
||||||
|
"program": "${workspaceFolder}/bitwarden_license/src/Sso/bin/Debug/net6.0/Sso.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/bitwarden_license/src/Sso",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"ASPNETCORE_URLS": "http://localhost:51822",
|
||||||
|
"developSelfHosted": "true"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Notifications-SelfHost",
|
||||||
|
"presentation": {
|
||||||
|
"hidden": true,
|
||||||
|
"group": "self-host",
|
||||||
|
"order": 999
|
||||||
|
},
|
||||||
|
"requireExactSource": true,
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "buildNotifications",
|
||||||
|
"program": "${workspaceFolder}/src/Notifications/bin/Debug/net6.0/Notifications.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src/Notifications",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"ASPNETCORE_URLS": "http://localhost:61841",
|
||||||
|
"developSelfHosted": "true"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "EventsProcessor-SelfHost",
|
||||||
|
"presentation": {
|
||||||
|
"hidden": true,
|
||||||
|
"group": "self-host",
|
||||||
|
"order": 999
|
||||||
|
},
|
||||||
|
"requireExactSource": true,
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "buildEventsProcessor",
|
||||||
|
"program": "${workspaceFolder}/src/EventsProcessor/bin/Debug/net6.0/EventsProcessor.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/src/EventsProcessor",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"ASPNETCORE_URLS": "http://localhost:46274",
|
||||||
|
"developSelfHosted": "true"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ".NET Core Attach",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach",
|
||||||
|
"processId": "${command:pickProcess}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
157
.vscode/tasks.json
vendored
Normal file
157
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "buildIcons",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/src/Icons/Icons.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "buildPortal",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/bitwarden_license/src/Portal/Portal.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "buildSso",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/bitwarden_license/src/Sso/Sso.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "buildEventsProcessor",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/src/EventsProcessor/EventsProcessor.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "buildAdmin",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/src/Admin/Admin.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "buildIdentity",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/src/Identity/Identity.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "buildAPI",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/src/Api/Api.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile",
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "buildNotifications",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/src/Notifications/Notifications.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile",
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "buildBilling",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/src/Billing/Billing.csproj",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile",
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "clean",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "dotnet clean",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": true,
|
||||||
|
"clear": false
|
||||||
|
},
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "test",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "dotnet test",
|
||||||
|
"group": {
|
||||||
|
"kind": "test",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": true,
|
||||||
|
"clear": false
|
||||||
|
},
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -1,31 +1,3 @@
|
|||||||
# How to Contribute
|
# How to Contribute
|
||||||
|
|
||||||
Contributions of all kinds are welcome!
|
Our [Contributing Guidelines](https://contributing.bitwarden.com/contributing/) are located in our [Contributing Documentation](https://contributing.bitwarden.com/). The documentation also includes recommended tooling, code style tips, and lots of other great information to get you started.
|
||||||
|
|
||||||
Please visit our [Community Forums](https://community.bitwarden.com/) for general community discussion and the development roadmap.
|
|
||||||
|
|
||||||
Here is how you can get involved:
|
|
||||||
|
|
||||||
* **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
|
|
||||||
|
|
||||||
* **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
|
|
||||||
|
|
||||||
* **Report a bug or submit a bugfix:** Use Github issues and pull requests
|
|
||||||
|
|
||||||
* **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
|
|
||||||
|
|
||||||
* **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
|
|
||||||
|
|
||||||
## Contributor Agreement
|
|
||||||
|
|
||||||
Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/server) if you intend on contributing to any Github repository. Pull requests cannot be accepted and merged unless the author has signed the Contributor Agreement.
|
|
||||||
|
|
||||||
## Pull Request Guidelines
|
|
||||||
|
|
||||||
* commit any pull requests against the `master` branch
|
|
||||||
* include a link to your Community Forums post
|
|
||||||
* please do **not** submit version number updates/bumps
|
|
||||||
|
|
||||||
# Setup guide
|
|
||||||
|
|
||||||
Please read the [Setup guide](https://github.com/bitwarden/server/blob/master/SETUP.md) for a step-by-step guide to set up your own local development server.
|
|
||||||
|
@ -1,9 +1,71 @@
|
|||||||
<Project>
|
<Project>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<Version>1.44.1</Version>
|
<!--2022.6.2-->
|
||||||
|
<Version>2023.1.0</Version>
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This section is for packages that we use multiple times throughout the solution
|
||||||
|
It gives us a single place to manage the version to ensure we are using the same version
|
||||||
|
across the solution.
|
||||||
|
-->
|
||||||
|
<PropertyGroup>
|
||||||
|
<!--
|
||||||
|
NuGet: https://www.nuget.org/packages/Microsoft.NET.Test.Sdk
|
||||||
|
-->
|
||||||
|
<MicrosoftNetTestSdkVersion>17.1.0</MicrosoftNetTestSdkVersion>
|
||||||
|
<!--
|
||||||
|
NuGet: https://www.nuget.org/packages/xunit
|
||||||
|
-->
|
||||||
|
<XUnitVersion>2.4.1</XUnitVersion>
|
||||||
|
<!--
|
||||||
|
NuGet: https://www.nuget.org/packages/xunit
|
||||||
|
-->
|
||||||
|
<XUnitRunnerVisualStudioVersion>2.4.3</XUnitRunnerVisualStudioVersion>
|
||||||
|
<!--
|
||||||
|
NuGet: https://www.nuget.org/packages/coverlet.collector/
|
||||||
|
-->
|
||||||
|
<CoverletCollectorVersion>3.1.2</CoverletCollectorVersion>
|
||||||
|
<!--
|
||||||
|
NuGet: https://www.nuget.org/packages/Swashbuckle.AspNetCore/
|
||||||
|
-->
|
||||||
|
<MicrosoftVisualStudioWebCodeGenerationDesignVersion>6.0.3</MicrosoftVisualStudioWebCodeGenerationDesignVersion>
|
||||||
|
<!--
|
||||||
|
NuGet: https://www.nuget.org/packages/NSubstitute/
|
||||||
|
-->
|
||||||
|
<NSubstitueVersion>4.3.0</NSubstitueVersion>
|
||||||
|
<!--
|
||||||
|
NuGet: https://www.nuget.org/packages/AutoFixture.Xunit2/
|
||||||
|
-->
|
||||||
|
<AutoFixtureXUnit2Version>4.17.0</AutoFixtureXUnit2Version>
|
||||||
|
<!--
|
||||||
|
NuGet: https://www.nuget.org/packages/AutoFixture.AutoNSubstitute/
|
||||||
|
-->
|
||||||
|
<AutoFixtureAutoNSubstituteVersion>4.17.0</AutoFixtureAutoNSubstituteVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This section is for getting & setting the gitHash value, which can easily be accessed
|
||||||
|
via the Core.Utilities.AssemblyHelpers class.
|
||||||
|
-->
|
||||||
|
<Target Name="SetSourceRevisionId" BeforeTargets="CoreGenerateAssemblyInfo">
|
||||||
|
<Exec Command="git describe --long --always --dirty --exclude=* --abbrev=8" ConsoleToMSBuild="True" IgnoreExitCode="False">
|
||||||
|
<Output PropertyName="SourceRevisionId" TaskParameter="ConsoleOutput"/>
|
||||||
|
</Exec>
|
||||||
|
</Target>
|
||||||
|
<Target Name="WriteRevision" AfterTargets="SetSourceRevisionId">
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||||
|
<_Parameter1>GitHash</_Parameter1>
|
||||||
|
<_Parameter2>$(SourceRevisionId)</_Parameter2>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
</ItemGroup>
|
||||||
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
@ -16,12 +16,12 @@ Our current software products have the following licenses:
|
|||||||
|
|
||||||
*Bitwarden server:* The main Bitwarden server code is licensed under the AGPL 3.0 license.
|
*Bitwarden server:* The main Bitwarden server code is licensed under the AGPL 3.0 license.
|
||||||
|
|
||||||
*CommCore and SSO integration:* Code for certain new modules that are designed and developed for use by larger
|
*Commercial.Core and SSO integration:* Code for certain new modules that are designed and developed for use by larger
|
||||||
organizations and enterprise environments is released under the Bitwarden License, a "source available" license. The
|
organizations and enterprise environments is released under the Bitwarden License, a "source available" license. The
|
||||||
Bitwarden License provides users access to product source code for non-production purposes such as development and
|
Bitwarden License provides users access to product source code for non-production purposes such as development and
|
||||||
testing, but requires a paid subscription for production use of the product, and environments supporting production.
|
testing, but requires a paid subscription for production use of the product, and environments supporting production.
|
||||||
Additionally the Api module by default includes CommCore which is under the Bitwarden License, however this can be
|
Additionally the Api module by default includes Commercial.Core which is under the Bitwarden License, however this can
|
||||||
disabled by using `/p:DefineConstants="OSS"` as an argument to `dotnet` while building the module.
|
be disabled by using `/p:DefineConstants="OSS"` as an argument to `dotnet` while building the module.
|
||||||
|
|
||||||
# Frequently Asked Questions
|
# Frequently Asked Questions
|
||||||
|
|
||||||
|
77
README.md
77
README.md
@ -13,51 +13,15 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
-------------------
|
---
|
||||||
|
|
||||||
The Bitwarden Server project contains the APIs, database, and other core infrastructure items needed for the "backend" of all bitwarden client applications.
|
The Bitwarden Server project contains the APIs, database, and other core infrastructure items needed for the "backend" of all bitwarden client applications.
|
||||||
|
|
||||||
The server project is written in C# using .NET Core with ASP.NET Core. The database is written in T-SQL/SQL Server. The codebase can be developed, built, run, and deployed cross-platform on Windows, macOS, and Linux distributions.
|
The server project is written in C# using .NET Core with ASP.NET Core. The database is written in T-SQL/SQL Server. The codebase can be developed, built, run, and deployed cross-platform on Windows, macOS, and Linux distributions.
|
||||||
|
|
||||||
## Build/Run
|
## Developer Documentation
|
||||||
|
|
||||||
Please read the [Setup guide](https://github.com/bitwarden/server/blob/master/SETUP.md) for a step-by-step guide to set up your own local development server.
|
Please refer to the [Server Setup Guide](https://contributing.bitwarden.com/getting-started/server/guide) in the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.
|
||||||
|
|
||||||
### Requirements
|
|
||||||
|
|
||||||
- [.NET 5.0 SDK](https://dotnet.microsoft.com/download)
|
|
||||||
- [SQL Server 2017](https://docs.microsoft.com/en-us/sql/index)
|
|
||||||
|
|
||||||
*These dependencies are free to use.*
|
|
||||||
|
|
||||||
### Recommended Development Tooling
|
|
||||||
|
|
||||||
- [Visual Studio](https://www.visualstudio.com/vs/) (Windows and macOS)
|
|
||||||
- [Visual Studio Code](https://code.visualstudio.com/) (other)
|
|
||||||
|
|
||||||
*These tools are free to use.*
|
|
||||||
|
|
||||||
### API
|
|
||||||
|
|
||||||
```
|
|
||||||
cd src/Api
|
|
||||||
dotnet restore
|
|
||||||
dotnet build
|
|
||||||
dotnet run
|
|
||||||
```
|
|
||||||
|
|
||||||
visit http://localhost:4000/alive
|
|
||||||
|
|
||||||
### Identity
|
|
||||||
|
|
||||||
```
|
|
||||||
cd src/Identity
|
|
||||||
dotnet restore
|
|
||||||
dotnet build
|
|
||||||
dotnet run
|
|
||||||
```
|
|
||||||
|
|
||||||
visit http://localhost:33657/.well-known/openid-configuration
|
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
@ -76,13 +40,13 @@ Full documentation for deploying Bitwarden with Docker can be found in our help
|
|||||||
- [Docker](https://www.docker.com/community-edition#/download)
|
- [Docker](https://www.docker.com/community-edition#/download)
|
||||||
- [Docker Compose](https://docs.docker.com/compose/install/) (already included with some Docker installations)
|
- [Docker Compose](https://docs.docker.com/compose/install/) (already included with some Docker installations)
|
||||||
|
|
||||||
*These dependencies are free to use.*
|
_These dependencies are free to use._
|
||||||
|
|
||||||
### Linux & macOS
|
### Linux & macOS
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -s -o bitwarden.sh \
|
curl -s -L -o bitwarden.sh \
|
||||||
https://raw.githubusercontent.com/bitwarden/server/master/scripts/bitwarden.sh \
|
"https://func.bitwarden.com/api/dl/?app=self-host&platform=linux" \
|
||||||
&& chmod +x bitwarden.sh
|
&& chmod +x bitwarden.sh
|
||||||
./bitwarden.sh install
|
./bitwarden.sh install
|
||||||
./bitwarden.sh start
|
./bitwarden.sh start
|
||||||
@ -92,15 +56,40 @@ curl -s -o bitwarden.sh \
|
|||||||
|
|
||||||
```
|
```
|
||||||
Invoke-RestMethod -OutFile bitwarden.ps1 `
|
Invoke-RestMethod -OutFile bitwarden.ps1 `
|
||||||
-Uri https://raw.githubusercontent.com/bitwarden/server/master/scripts/bitwarden.ps1
|
-Uri "https://func.bitwarden.com/api/dl/?app=self-host&platform=windows"
|
||||||
.\bitwarden.ps1 -install
|
.\bitwarden.ps1 -install
|
||||||
.\bitwarden.ps1 -start
|
.\bitwarden.ps1 -start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## We're Hiring!
|
||||||
|
|
||||||
|
Interested in contributing in a big way? Consider joining our team! We're hiring for many positions. Please take a look at our [Careers page](https://bitwarden.com/careers/) to see what opportunities are currently open as well as what it's like to work at Bitwarden.
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
Code contributions are welcome! Visual Studio or VS Code is highly recommended if you are working on this project. Please commit any pull requests against the `master` branch. Please see [`CONTRIBUTING.md`](CONTRIBUTING.md) for more info (and feel free to contribute to that guide as well).
|
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [Contributing Guidelines](https://contributing.bitwarden.com/contributing/). Check out the [Contributing Documentation](https://contributing.bitwarden.com/) for how to get started with your first contribution.
|
||||||
|
|
||||||
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file. We also run a program on [HackerOne](https://hackerone.com/bitwarden).
|
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file. We also run a program on [HackerOne](https://hackerone.com/bitwarden).
|
||||||
|
|
||||||
No grant of any rights in the trademarks, service marks, or logos of Bitwarden is made (except as may be necessary to comply with the notice requirements as applicable), and use of any Bitwarden trademarks must comply with [Bitwarden Trademark Guidelines](https://github.com/bitwarden/server/blob/master/TRADEMARK_GUIDELINES.md).
|
No grant of any rights in the trademarks, service marks, or logos of Bitwarden is made (except as may be necessary to comply with the notice requirements as applicable), and use of any Bitwarden trademarks must comply with [Bitwarden Trademark Guidelines](https://github.com/bitwarden/server/blob/master/TRADEMARK_GUIDELINES.md).
|
||||||
|
|
||||||
|
### Dotnet-format
|
||||||
|
|
||||||
|
Consider installing our git pre-commit hook for automatic formatting.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config --local core.hooksPath .git-hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Scoped Namespaces
|
||||||
|
|
||||||
|
We recently migrated to using file scoped namespaces to save some horizontal space. All previous branches will need to update to avoid large merge conflicts using the following steps:
|
||||||
|
|
||||||
|
1. Check out your local Branch
|
||||||
|
2. Run `git merge 9b7aef0763ad14e229b337c3b5b27cb411009792`
|
||||||
|
3. Resolve any merge conflicts, commit.
|
||||||
|
4. Run `dotnet format`
|
||||||
|
5. Commit
|
||||||
|
6. Run `git merge -Xours 7f5f010e1eea400300c47f776604ecf46c4b4f2d`
|
||||||
|
7. Fix Merge conflicts
|
||||||
|
8. Push
|
||||||
|
42
SECURITY.md
42
SECURITY.md
@ -1,39 +1,11 @@
|
|||||||
Bitwarden believes that working with security researchers across the globe is crucial to keeping our
|
Bitwarden believes that working with security researchers across the globe is crucial to keeping our users safe. If you believe you've found a security issue in our product or service, we encourage you to please submit a report through our [HackerOne Program](https://hackerone.com/bitwarden/). We welcome working with you to resolve the issue promptly. Thanks in advance!
|
||||||
users safe. If you believe you've found a security issue in our product or service, we encourage you to
|
|
||||||
notify us. We welcome working with you to resolve the issue promptly. Thanks in advance!
|
|
||||||
|
|
||||||
# Disclosure Policy
|
# Disclosure Policy
|
||||||
|
|
||||||
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
|
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every effort to quickly resolve the issue.
|
||||||
effort to quickly resolve the issue.
|
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a third-party. We may publicly disclose the issue before resolving it, if appropriate.
|
||||||
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
|
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our service. Only interact with accounts you own or with explicit permission of the account holder.
|
||||||
third-party. We may publicly disclose the issue before resolving it, if appropriate.
|
- If you would like to encrypt your report, please use the PGP key with long ID `0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
|
||||||
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
|
|
||||||
degradation of our service. Only interact with accounts you own or with explicit permission of the
|
|
||||||
account holder.
|
|
||||||
- If you would like to encrypt your report, please use the PGP key with long ID
|
|
||||||
`0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
|
|
||||||
|
|
||||||
# In-scope
|
|
||||||
|
|
||||||
- Security issues in any current release of Bitwarden. This includes the web vault, browser extension,
|
|
||||||
and mobile apps (iOS and Android). Product downloads are available at https://bitwarden.com. Source
|
|
||||||
code is available at https://github.com/bitwarden.
|
|
||||||
|
|
||||||
# Exclusions
|
|
||||||
|
|
||||||
The following bug classes are out-of scope:
|
|
||||||
|
|
||||||
- Bugs that are already reported on any of Bitwarden's issue trackers (https://github.com/bitwarden),
|
|
||||||
or that we already know of. Note that some of our issue tracking is private.
|
|
||||||
- Issues in an upstream software dependency (ex: Xamarin, ASP.NET) which are already reported to the
|
|
||||||
upstream maintainer.
|
|
||||||
- Attacks requiring physical access to a user's device.
|
|
||||||
- Self-XSS
|
|
||||||
- Issues related to software or protocols not under Bitwarden's control
|
|
||||||
- Vulnerabilities in outdated versions of Bitwarden
|
|
||||||
- Missing security best practices that do not directly lead to a vulnerability
|
|
||||||
- Issues that do not have any impact on the general public
|
|
||||||
|
|
||||||
While researching, we'd like to ask you to refrain from:
|
While researching, we'd like to ask you to refrain from:
|
||||||
|
|
||||||
@ -42,4 +14,8 @@ While researching, we'd like to ask you to refrain from:
|
|||||||
- Social engineering (including phishing) of Bitwarden staff or contractors
|
- Social engineering (including phishing) of Bitwarden staff or contractors
|
||||||
- Any physical attempts against Bitwarden property or data centers
|
- Any physical attempts against Bitwarden property or data centers
|
||||||
|
|
||||||
|
# We want to help you!
|
||||||
|
|
||||||
|
If you have something that you feel is close to exploitation, or if you'd like some information regarding the internal API, or generally have any questions regarding the app that would help in your efforts, please email us at https://bitwarden.com/contact and ask for that information. As stated above, Bitwarden wants to help you find issues, and is more than willing to help.
|
||||||
|
|
||||||
Thank you for helping keep Bitwarden and our users safe!
|
Thank you for helping keep Bitwarden and our users safe!
|
||||||
|
197
SETUP.md
197
SETUP.md
@ -1,197 +0,0 @@
|
|||||||
# Server Architecture
|
|
||||||
|
|
||||||
The Server is divided into a number of services. Each service is a Visual Studio project in the Server solution. These are:
|
|
||||||
|
|
||||||
* Admin
|
|
||||||
* Api
|
|
||||||
* Icons
|
|
||||||
* Identity
|
|
||||||
* Notifications
|
|
||||||
* SQL
|
|
||||||
|
|
||||||
Each service is built and run separately. The Bitwarden clients can use different servers for different services.
|
|
||||||
|
|
||||||
This means that you don't need to run all services locally for a development environment. You can run only those services that you intend to modify, and use Bitwarden.com or a self-hosted instance for all other services required.
|
|
||||||
|
|
||||||
By default some of the services depends on the Bitwarden licensed `CommCore`, however it can easily be disabled by including the `/p:DefineConstants="OSS"` as an argument to `dotnet`.
|
|
||||||
|
|
||||||
# Local Development Environment Setup
|
|
||||||
|
|
||||||
This guide will show you how to set up the Api, Identity and SQL projects for development. These are the minimum projects for any development work. You may need to set up additional projects depending on the changes you want to make.
|
|
||||||
|
|
||||||
We recommend using [Visual Studio](https://visualstudio.microsoft.com/vs/), and [PowerShell](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7.1) which is used for the helper scripts.
|
|
||||||
|
|
||||||
## Docker containers
|
|
||||||
|
|
||||||
To simplify the setup process we provide a [Docker Compose](https://docs.docker.com/compose/) application model. This is split up into multiple service profiles to facilitate easily customization.
|
|
||||||
|
|
||||||
Some settings can be customized by modifying the `dev/.env` file, such as the `MSSQL_PASSWORD` which should be modified before starting the project.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Copy the example environment file
|
|
||||||
cp ./.env.example ./.env
|
|
||||||
|
|
||||||
# We recommend running the following command when developing for self-hosted
|
|
||||||
docker compose --profile mssql --profile mail up
|
|
||||||
|
|
||||||
# We also provide a storage profile which uses Azurite to emulate some services used by the cloud instance
|
|
||||||
# Usually only needed by internal Bitwarden developers
|
|
||||||
docker compose --profile cloud --profile mail up
|
|
||||||
```
|
|
||||||
|
|
||||||
### SQL Server
|
|
||||||
|
|
||||||
We recommend changing the `MSSQL_PASSWORD` variable in `dev/.env` to avoid exposing the sqlserver with a default password. **Note**: changing this after first running docker compose may require a re-creation of the storage volume. To do this, stop the running containers and run `docker volume rm bitwardenserver_mssql_dev_data`. (**Warning:** this will delete your development database.)
|
|
||||||
|
|
||||||
We provide a helper script which will create the development database `vault_dev` and also run all migrations. This command should be run after starting docker the first time, as well as after syncing against upstream and after creating a new migration.
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\dev\migrate.ps1
|
|
||||||
|
|
||||||
# You can also re-run the last migration using
|
|
||||||
.\dev\migrate.ps1 -r
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** If all or many migrations are skipped even though this is a new database, make sure that there is not a `last_migration` file located in `dev/.data/mssql`. If there is, remove it and run the helper script again. This can happen if you create the database, run migrations, then delete it.
|
|
||||||
|
|
||||||
### Azurite
|
|
||||||
|
|
||||||
[Azurite](https://github.com/Azure/Azurite) is a emulator for Azure Storage API and supports Blob, Queues and Table storage. We use it to avoid a hard dependency on online services for cloud development.
|
|
||||||
|
|
||||||
To bootstrap the local Azurite instance please run the following command:
|
|
||||||
```powershell
|
|
||||||
# This script requires the Az module, which can be installed using
|
|
||||||
Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -Force
|
|
||||||
|
|
||||||
.\dev\setup_azurite.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mailcatcher
|
|
||||||
|
|
||||||
Since the server uses emails for many user interactions a working SMTP server is a requirement, we provide a pre-setup instance of [MailCatcher](https://mailcatcher.me/) which exposes a web interface at http://localhost:1080.
|
|
||||||
|
|
||||||
## Certificates
|
|
||||||
In order to run Bitwarden, we require two certificates which for local development can be resolved by using self signed certificates.
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
|
|
||||||
We provide a helper script which will generate and add the certificates to the users Certificate Store. After running the script it will output the thumbprints needed for the next step. The certificates can later be accessed using `certml.msc` under `Personal/Certificates`.
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\create_certificates_windows.ps1
|
|
||||||
|
|
||||||
PSParentPath: Microsoft.PowerShell.Security\Certificate::CurrentUser\My
|
|
||||||
Thumbprint Subject
|
|
||||||
---------- -------
|
|
||||||
0BE8A0072214AB37C6928968752F698EEC3A68B5 CN=Bitwarden Identity Server Dev
|
|
||||||
C3A6CECAD3DB580F91A52FC9C767FE780300D8AB CN=Bitwarden Data Protection Dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### MacOS
|
|
||||||
|
|
||||||
We provide a helper script which will generate the certificates and add them to the keychain.
|
|
||||||
|
|
||||||
**Note:** You should update the Trust options for each certificate to `always trust` using *Keychain Access*.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./create_certificates_mac.sh
|
|
||||||
|
|
||||||
Certificate fingerprints:
|
|
||||||
Identity Server Dev: 0BE8A0072214AB37C6928968752F698EEC3A68B5
|
|
||||||
Data Protection Dev: C3A6CECAD3DB580F91A52FC9C767FE780300D8AB
|
|
||||||
```
|
|
||||||
|
|
||||||
## User Secrets
|
|
||||||
User secrets are a method for managing application settings on a per-developer basis. They are stored outside of the local git repository so that they are not pushed to remote.
|
|
||||||
|
|
||||||
User secrets override the settings in `appsettings.json` of each project. Your user secrets file should match the structure of the `appsettings.json` file for the settings you intend to override.
|
|
||||||
|
|
||||||
For more information, see: [Safe storage of app secrets in development in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-5.0).
|
|
||||||
|
|
||||||
### Automated Helper script
|
|
||||||
|
|
||||||
We provide a helper scripts which simplifies setting user secrets for all projects in the repository.
|
|
||||||
|
|
||||||
Start by copying the `secret.json.example` file to `secrets.json` and modify the existing settings and add any other required setting. Afterwards run the following command which will add the settings to each project in the bitwarden repository.
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\setup_secrets.ps1
|
|
||||||
|
|
||||||
# The script also supports an optional flag which removes all existing settings before re-applying them
|
|
||||||
.\setup_secrets.ps1 -clear 1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manually creating and modifying
|
|
||||||
|
|
||||||
It is also possible to manually create and modify the user secrets using either the `dotnet` CLI or `Visual Studio` on Windows. For more details see [Appendix A](#user-secrets).
|
|
||||||
|
|
||||||
### Required User Secrets
|
|
||||||
|
|
||||||
**selfhosted**: It is highly recommended that you use the `selfHosted: true` setting when running a local development environment. This tells the system not to use cloud services, assuming that you are running your own local SQL instance.
|
|
||||||
|
|
||||||
**sqlServer__connectionString**: this provides the information required for the Server to connect to the SQL instance. See the example connection string in `secrets.json.example`. You may need to change the default password in the connection string.
|
|
||||||
|
|
||||||
**licenseDirectory**: this must be set to avoid errors, but it can be set to an arbitrary empty folder.
|
|
||||||
|
|
||||||
**installation__key** and **installation__id**: request your own private Installation Id and Installation Key for self-hosting: https://bitwarden.com/host/.
|
|
||||||
|
|
||||||
## Running and Debugging
|
|
||||||
After you have completed the above steps, you should be ready to launch your development environment for the Api and Identity projects.
|
|
||||||
|
|
||||||
### Visual Studio
|
|
||||||
|
|
||||||
To debug:
|
|
||||||
* On Windows, right-click on each project > click **Debug** > click **Start New Instance**
|
|
||||||
* On MacOS, right-click each project > click **Start Debugging Project**
|
|
||||||
|
|
||||||
To run without debugging, open a terminal and navigate to the location of the .csproj file for that project (usually in `src/ProjectName`). Start the project with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dotnet run
|
|
||||||
```
|
|
||||||
|
|
||||||
NOTE: check the output of the running project to find the port it is listening on. If this is different to the default in `appsettings.json`, you may need to update your user secrets to override this (typically the Api user secrets for the Identity URL).
|
|
||||||
|
|
||||||
### Rider
|
|
||||||
From within Rider, launch both the Api project and the Identity project by clicking the "play" button for each project separately.
|
|
||||||
|
|
||||||
### Testing your deployment
|
|
||||||
* To test the deployment of each project, navigate to the following pages in your browser. You should see server output and no errors:
|
|
||||||
* Test the Api deployment: http://localhost:4000/alive
|
|
||||||
* Test the Identity deployment: http://localhost:33656/.well-known/openid-configuration
|
|
||||||
* If your test was successful, you can connect a GUI client to the dev environment by following the instructions here: [Change your client application's environment](https://bitwarden.com/help/article/change-client-environment/). If you are following this guide, you should only set the API Server URL and Identity Server URL to localhost:port and leave all other fields blank.
|
|
||||||
* If you are using the CLI client, you will also need to set the Node environment variables for your self-signed certificates by following the instructions here: [The Bitwarden command-line tool (CLI) > Self-signed certificates](https://bitwarden.com/help/article/cli/#self-signed-certificates).
|
|
||||||
|
|
||||||
### Troubleshooting
|
|
||||||
* If you get a 404 error, the projects may be listening on a non-default port. Check the output of your running projects to check the port they are listening on.
|
|
||||||
|
|
||||||
|
|
||||||
# <a name="user-secrets"></a>Appendix A (User Secrets)
|
|
||||||
|
|
||||||
### Editing user secrets - Visual Studio on Windows
|
|
||||||
Right-click on the project in the Solution Explorer and click **Manage User Secrets**.
|
|
||||||
|
|
||||||
### Editing user secrets - Visual Studio on macOS
|
|
||||||
Open a terminal and navigate to the project directory. Once there, initiate and create the blank user secrets file by running:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dotnet user-secrets init
|
|
||||||
```
|
|
||||||
|
|
||||||
Add a user secret by running:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dotnet user-secrets set "<key>" "<value>"
|
|
||||||
```
|
|
||||||
|
|
||||||
View currently set secrets by running:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dotnet user-secrets list
|
|
||||||
```
|
|
||||||
|
|
||||||
By default, user secret files are located in `~/.microsoft/usersecrets/<project name>/secrets.json`. After the file has been created, you can edit this directly with VSCode, which is much easier than using the command line tool.
|
|
||||||
|
|
||||||
### Editing user secrets - Rider
|
|
||||||
* Navigate to **Preferences -> Plugins** and Install .NET Core User Secrets
|
|
||||||
* Right click on the a project and click **Tools** > **Open project user secrets**
|
|
@ -61,18 +61,56 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sso", "bitwarden_license\sr
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Icons.Test", "test\Icons.Test\Icons.Test.csproj", "{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Icons.Test", "test\Icons.Test\Icons.Test.csproj", "{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommCore", "bitwarden_license\src\CommCore\CommCore.csproj", "{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Commercial.Core", "bitwarden_license\src\Commercial.Core\Commercial.Core.csproj", "{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}"
|
||||||
ProjectSection(ProjectDependencies) = postProject
|
ProjectSection(ProjectDependencies) = postProject
|
||||||
{3973D21B-A692-4B60-9B70-3631C057423A} = {3973D21B-A692-4B60-9B70-3631C057423A}
|
{3973D21B-A692-4B60-9B70-3631C057423A} = {3973D21B-A692-4B60-9B70-3631C057423A}
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommCore.Test", "bitwarden_license\test\CmmCore.Test\CommCore.Test.csproj", "{0E99A21B-684B-4C59-9831-90F775CAB6F7}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Commercial.Core.Test", "bitwarden_license\test\Commercial.Core.Test\Commercial.Core.Test.csproj", "{0E99A21B-684B-4C59-9831-90F775CAB6F7}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test - Bitwarden License", "test - Bitwarden License", "{287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test - Bitwarden License", "test - Bitwarden License", "{287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MySqlMigrations", "util\MySqlMigrations\MySqlMigrations.csproj", "{BDC1D592-5947-47ED-9903-7CDBB12A50C8}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySqlMigrations", "util\MySqlMigrations\MySqlMigrations.csproj", "{BDC1D592-5947-47ED-9903-7CDBB12A50C8}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostgresMigrations", "util\PostgresMigrations\PostgresMigrations.csproj", "{F72E0229-2EF7-49B3-9004-FF4C0043816E}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PostgresMigrations", "util\PostgresMigrations\PostgresMigrations.csproj", "{F72E0229-2EF7-49B3-9004-FF4C0043816E}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "test\Common\Common.csproj", "{17DA09D7-0212-4009-879E-6B9CFDE5FA60}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure.Dapper", "src\Infrastructure.Dapper\Infrastructure.Dapper.csproj", "{AD933445-27CE-4D30-A6ED-9065309464AD}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedWeb", "src\SharedWeb\SharedWeb.csproj", "{713D44C0-1BC1-4024-96A3-A98A49F33908}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure.EntityFramework", "src\Infrastructure.EntityFramework\Infrastructure.EntityFramework.csproj", "{ED880735-0250-43C7-9662-FDC7C7416E7F}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Billing.Test", "test\Billing.Test\Billing.Test.csproj", "{B8639B10-2157-44BC-8CE1-D9EB4B50971F}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Identity.Test", "test\Identity.Test\Identity.Test.csproj", "{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Identity.IntegrationTest", "test\Identity.IntegrationTest\Identity.IntegrationTest.csproj", "{0D3B2BD2-53F3-421D-AD8F-C19B954C796B}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestCommon", "test\IntegrationTestCommon\IntegrationTestCommon.csproj", "{0923DE59-5FB1-44F2-9302-A09D2236B470}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scim", "bitwarden_license\src\Scim\Scim.csproj", "{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlServerEFScaffold", "util\SqlServerEFScaffold\SqlServerEFScaffold.csproj", "{2F2E8BB0-6838-48DA-B581-71B9F13DE364}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Commercial.Infrastructure.EntityFramework", "bitwarden_license\src\Commercial.Infrastructure.EntityFramework\Commercial.Infrastructure.EntityFramework.csproj", "{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.EFIntegration.Test", "test\Infrastructure.EFIntegration.Test\Infrastructure.EFIntegration.Test.csproj", "{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.IntegrationTest", "test\Api.IntegrationTest\Api.IntegrationTest.csproj", "{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim.IntegrationTest", "bitwarden_license\test\Scim.IntegrationTest\Scim.IntegrationTest.csproj", "{FE998849-5FC8-41A2-B7C9-9227901471A0}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{EC2D422A-6060-48E2-AAD2-37220D759F03}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroBenchmarks", "perf\MicroBenchmarks\MicroBenchmarks.csproj", "{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim.Test", "bitwarden_license\test\Scim.Test\Scim.Test.csproj", "{B1595DA3-4C60-41AA-8BF0-499A5F75A885}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.IntegrationTest", "test\Infrastructure.IntegrationTest\Infrastructure.IntegrationTest.csproj", "{7E9A7DD5-EB78-4AC5-BFD5-64573FD2533B}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqliteMigrations", "util\SqliteMigrations\SqliteMigrations.csproj", "{07143DFA-F242-47A4-A15E-39C9314D4140}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@ -150,15 +188,6 @@ Global
|
|||||||
{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}.Release|Any CPU.Build.0 = Release|Any CPU
|
{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{BDC1D592-5947-47ED-9903-7CDBB12A50C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{BDC1D592-5947-47ED-9903-7CDBB12A50C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{BDC1D592-5947-47ED-9903-7CDBB12A50C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{BDC1D592-5947-47ED-9903-7CDBB12A50C8}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{F72E0229-2EF7-49B3-9004-FF4C0043816E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{F72E0229-2EF7-49B3-9004-FF4C0043816E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{F72E0229-2EF7-49B3-9004-FF4C0043816E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{F72E0229-2EF7-49B3-9004-FF4C0043816E}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
|
|
||||||
{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
@ -167,6 +196,86 @@ Global
|
|||||||
{0E99A21B-684B-4C59-9831-90F775CAB6F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{0E99A21B-684B-4C59-9831-90F775CAB6F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{0E99A21B-684B-4C59-9831-90F775CAB6F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{0E99A21B-684B-4C59-9831-90F775CAB6F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{0E99A21B-684B-4C59-9831-90F775CAB6F7}.Release|Any CPU.Build.0 = Release|Any CPU
|
{0E99A21B-684B-4C59-9831-90F775CAB6F7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{BDC1D592-5947-47ED-9903-7CDBB12A50C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{BDC1D592-5947-47ED-9903-7CDBB12A50C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{BDC1D592-5947-47ED-9903-7CDBB12A50C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{BDC1D592-5947-47ED-9903-7CDBB12A50C8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F72E0229-2EF7-49B3-9004-FF4C0043816E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F72E0229-2EF7-49B3-9004-FF4C0043816E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F72E0229-2EF7-49B3-9004-FF4C0043816E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{F72E0229-2EF7-49B3-9004-FF4C0043816E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{17DA09D7-0212-4009-879E-6B9CFDE5FA60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{17DA09D7-0212-4009-879E-6B9CFDE5FA60}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{17DA09D7-0212-4009-879E-6B9CFDE5FA60}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{17DA09D7-0212-4009-879E-6B9CFDE5FA60}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{AD933445-27CE-4D30-A6ED-9065309464AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{AD933445-27CE-4D30-A6ED-9065309464AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{AD933445-27CE-4D30-A6ED-9065309464AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{AD933445-27CE-4D30-A6ED-9065309464AD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{713D44C0-1BC1-4024-96A3-A98A49F33908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{713D44C0-1BC1-4024-96A3-A98A49F33908}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{713D44C0-1BC1-4024-96A3-A98A49F33908}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{713D44C0-1BC1-4024-96A3-A98A49F33908}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{ED880735-0250-43C7-9662-FDC7C7416E7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{ED880735-0250-43C7-9662-FDC7C7416E7F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{ED880735-0250-43C7-9662-FDC7C7416E7F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{ED880735-0250-43C7-9662-FDC7C7416E7F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B8639B10-2157-44BC-8CE1-D9EB4B50971F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B8639B10-2157-44BC-8CE1-D9EB4B50971F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B8639B10-2157-44BC-8CE1-D9EB4B50971F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B8639B10-2157-44BC-8CE1-D9EB4B50971F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{0D3B2BD2-53F3-421D-AD8F-C19B954C796B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{0D3B2BD2-53F3-421D-AD8F-C19B954C796B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0D3B2BD2-53F3-421D-AD8F-C19B954C796B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{0D3B2BD2-53F3-421D-AD8F-C19B954C796B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{0923DE59-5FB1-44F2-9302-A09D2236B470}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{0923DE59-5FB1-44F2-9302-A09D2236B470}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0923DE59-5FB1-44F2-9302-A09D2236B470}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{0923DE59-5FB1-44F2-9302-A09D2236B470}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{2F2E8BB0-6838-48DA-B581-71B9F13DE364}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{2F2E8BB0-6838-48DA-B581-71B9F13DE364}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{2F2E8BB0-6838-48DA-B581-71B9F13DE364}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{2F2E8BB0-6838-48DA-B581-71B9F13DE364}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7E9A7DD5-EB78-4AC5-BFD5-64573FD2533B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7E9A7DD5-EB78-4AC5-BFD5-64573FD2533B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7E9A7DD5-EB78-4AC5-BFD5-64573FD2533B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7E9A7DD5-EB78-4AC5-BFD5-64573FD2533B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{07143DFA-F242-47A4-A15E-39C9314D4140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{07143DFA-F242-47A4-A15E-39C9314D4140}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{07143DFA-F242-47A4-A15E-39C9314D4140}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{07143DFA-F242-47A4-A15E-39C9314D4140}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -189,10 +298,28 @@ Global
|
|||||||
{860DE301-0B3E-4717-9C21-A9B4C3C2B121} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{860DE301-0B3E-4717-9C21-A9B4C3C2B121} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
{4866AF64-6640-4C65-A662-A31E02FF9064} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}
|
{4866AF64-6640-4C65-A662-A31E02FF9064} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}
|
||||||
{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
{BDC1D592-5947-47ED-9903-7CDBB12A50C8} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
|
||||||
{F72E0229-2EF7-49B3-9004-FF4C0043816E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
|
||||||
{EDC0D688-D58C-4CE1-AA07-3606AC6874B8} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}
|
{EDC0D688-D58C-4CE1-AA07-3606AC6874B8} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}
|
||||||
{0E99A21B-684B-4C59-9831-90F775CAB6F7} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
|
{0E99A21B-684B-4C59-9831-90F775CAB6F7} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
|
||||||
|
{BDC1D592-5947-47ED-9903-7CDBB12A50C8} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||||
|
{F72E0229-2EF7-49B3-9004-FF4C0043816E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||||
|
{17DA09D7-0212-4009-879E-6B9CFDE5FA60} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
|
{AD933445-27CE-4D30-A6ED-9065309464AD} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
|
||||||
|
{713D44C0-1BC1-4024-96A3-A98A49F33908} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
|
||||||
|
{ED880735-0250-43C7-9662-FDC7C7416E7F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
|
||||||
|
{B8639B10-2157-44BC-8CE1-D9EB4B50971F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
|
{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
|
{0D3B2BD2-53F3-421D-AD8F-C19B954C796B} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
|
{0923DE59-5FB1-44F2-9302-A09D2236B470} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
|
{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}
|
||||||
|
{2F2E8BB0-6838-48DA-B581-71B9F13DE364} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||||
|
{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}
|
||||||
|
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
|
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
|
{FE998849-5FC8-41A2-B7C9-9227901471A0} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
|
||||||
|
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61} = {EC2D422A-6060-48E2-AAD2-37220D759F03}
|
||||||
|
{B1595DA3-4C60-41AA-8BF0-499A5F75A885} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
|
||||||
|
{7E9A7DD5-EB78-4AC5-BFD5-64573FD2533B} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
|
{07143DFA-F242-47A4-A15E-39C9314D4140} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||||
|
@ -1,513 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.Core.Context;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Enums.Provider;
|
|
||||||
using Bit.Core.Exceptions;
|
|
||||||
using Bit.Core.Models.Business;
|
|
||||||
using Bit.Core.Models.Business.Provider;
|
|
||||||
using Bit.Core.Models.Data;
|
|
||||||
using Bit.Core.Models.Table;
|
|
||||||
using Bit.Core.Models.Table.Provider;
|
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Settings;
|
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
|
||||||
|
|
||||||
namespace Bit.CommCore.Services
|
|
||||||
{
|
|
||||||
public class ProviderService : IProviderService
|
|
||||||
{
|
|
||||||
public static PlanType[] ProviderDisllowedOrganizationTypes = new[] { PlanType.Free, PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2019 };
|
|
||||||
|
|
||||||
private readonly IDataProtector _dataProtector;
|
|
||||||
private readonly IMailService _mailService;
|
|
||||||
private readonly IEventService _eventService;
|
|
||||||
private readonly GlobalSettings _globalSettings;
|
|
||||||
private readonly IProviderRepository _providerRepository;
|
|
||||||
private readonly IProviderUserRepository _providerUserRepository;
|
|
||||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
|
||||||
private readonly IUserRepository _userRepository;
|
|
||||||
private readonly IUserService _userService;
|
|
||||||
private readonly IOrganizationService _organizationService;
|
|
||||||
private readonly ICurrentContext _currentContext;
|
|
||||||
|
|
||||||
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
|
||||||
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
|
|
||||||
IUserService userService, IOrganizationService organizationService, IMailService mailService,
|
|
||||||
IDataProtectionProvider dataProtectionProvider, IEventService eventService,
|
|
||||||
IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
|
|
||||||
ICurrentContext currentContext)
|
|
||||||
{
|
|
||||||
_providerRepository = providerRepository;
|
|
||||||
_providerUserRepository = providerUserRepository;
|
|
||||||
_providerOrganizationRepository = providerOrganizationRepository;
|
|
||||||
_organizationRepository = organizationRepository;
|
|
||||||
_userRepository = userRepository;
|
|
||||||
_userService = userService;
|
|
||||||
_organizationService = organizationService;
|
|
||||||
_mailService = mailService;
|
|
||||||
_eventService = eventService;
|
|
||||||
_globalSettings = globalSettings;
|
|
||||||
_dataProtector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
|
||||||
_currentContext = currentContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CreateAsync(string ownerEmail)
|
|
||||||
{
|
|
||||||
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
|
|
||||||
if (owner == null)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Invalid owner. Owner must be an existing Bitwarden user.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var provider = new Provider
|
|
||||||
{
|
|
||||||
Status = ProviderStatusType.Pending,
|
|
||||||
Enabled = true,
|
|
||||||
UseEvents = true,
|
|
||||||
};
|
|
||||||
await _providerRepository.CreateAsync(provider);
|
|
||||||
|
|
||||||
var providerUser = new ProviderUser
|
|
||||||
{
|
|
||||||
ProviderId = provider.Id,
|
|
||||||
UserId = owner.Id,
|
|
||||||
Type = ProviderUserType.ProviderAdmin,
|
|
||||||
Status = ProviderUserStatusType.Confirmed,
|
|
||||||
};
|
|
||||||
await _providerUserRepository.CreateAsync(providerUser);
|
|
||||||
await SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key)
|
|
||||||
{
|
|
||||||
var owner = await _userService.GetUserByIdAsync(ownerUserId);
|
|
||||||
if (owner == null)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Invalid owner.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider.Status != ProviderStatusType.Pending)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Provider is already setup.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!CoreHelpers.TokenIsValid("ProviderSetupInvite", _dataProtector, token, owner.Email, provider.Id,
|
|
||||||
_globalSettings.OrganizationInviteExpirationHours))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Invalid token.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var providerUser = await _providerUserRepository.GetByProviderUserAsync(provider.Id, ownerUserId);
|
|
||||||
if (!(providerUser is { Type: ProviderUserType.ProviderAdmin }))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Invalid owner.");
|
|
||||||
}
|
|
||||||
|
|
||||||
provider.Status = ProviderStatusType.Created;
|
|
||||||
await _providerRepository.UpsertAsync(provider);
|
|
||||||
|
|
||||||
providerUser.Key = key;
|
|
||||||
await _providerUserRepository.ReplaceAsync(providerUser);
|
|
||||||
|
|
||||||
return provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateAsync(Provider provider, bool updateBilling = false)
|
|
||||||
{
|
|
||||||
if (provider.Id == default)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Cannot create provider this way.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await _providerRepository.ReplaceAsync(provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<ProviderUser>> InviteUserAsync(ProviderUserInvite<string> invite)
|
|
||||||
{
|
|
||||||
if (!_currentContext.ProviderManageUsers(invite.ProviderId))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Invalid permissions.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var emails = invite?.UserIdentifiers;
|
|
||||||
var invitingUser = await _providerUserRepository.GetByProviderUserAsync(invite.ProviderId, invite.InvitingUserId);
|
|
||||||
|
|
||||||
var provider = await _providerRepository.GetByIdAsync(invite.ProviderId);
|
|
||||||
if (provider == null || emails == null || !emails.Any())
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var providerUsers = new List<ProviderUser>();
|
|
||||||
foreach (var email in emails)
|
|
||||||
{
|
|
||||||
// Make sure user is not already invited
|
|
||||||
var existingProviderUserCount =
|
|
||||||
await _providerUserRepository.GetCountByProviderAsync(invite.ProviderId, email, false);
|
|
||||||
if (existingProviderUserCount > 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var providerUser = new ProviderUser
|
|
||||||
{
|
|
||||||
ProviderId = invite.ProviderId,
|
|
||||||
UserId = null,
|
|
||||||
Email = email.ToLowerInvariant(),
|
|
||||||
Key = null,
|
|
||||||
Type = invite.Type,
|
|
||||||
Status = ProviderUserStatusType.Invited,
|
|
||||||
CreationDate = DateTime.UtcNow,
|
|
||||||
RevisionDate = DateTime.UtcNow,
|
|
||||||
};
|
|
||||||
|
|
||||||
await _providerUserRepository.CreateAsync(providerUser);
|
|
||||||
|
|
||||||
await SendInviteAsync(providerUser, provider);
|
|
||||||
providerUsers.Add(providerUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _eventService.LogProviderUsersEventAsync(providerUsers.Select(pu => (pu, EventType.ProviderUser_Invited, null as DateTime?)));
|
|
||||||
|
|
||||||
return providerUsers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<Tuple<ProviderUser, string>>> ResendInvitesAsync(ProviderUserInvite<Guid> invite)
|
|
||||||
{
|
|
||||||
if (!_currentContext.ProviderManageUsers(invite.ProviderId))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Invalid permissions.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var providerUsers = await _providerUserRepository.GetManyAsync(invite.UserIdentifiers);
|
|
||||||
var provider = await _providerRepository.GetByIdAsync(invite.ProviderId);
|
|
||||||
|
|
||||||
var result = new List<Tuple<ProviderUser, string>>();
|
|
||||||
foreach (var providerUser in providerUsers)
|
|
||||||
{
|
|
||||||
if (providerUser.Status != ProviderUserStatusType.Invited || providerUser.ProviderId != invite.ProviderId)
|
|
||||||
{
|
|
||||||
result.Add(Tuple.Create(providerUser, "User invalid."));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await SendInviteAsync(providerUser, provider);
|
|
||||||
result.Add(Tuple.Create(providerUser, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ProviderUser> AcceptUserAsync(Guid providerUserId, User user, string token)
|
|
||||||
{
|
|
||||||
var providerUser = await _providerUserRepository.GetByIdAsync(providerUserId);
|
|
||||||
if (providerUser == null)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("User invalid.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (providerUser.Status != ProviderUserStatusType.Invited)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Already accepted.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!CoreHelpers.TokenIsValid("ProviderUserInvite", _dataProtector, token, user.Email, providerUser.Id,
|
|
||||||
_globalSettings.OrganizationInviteExpirationHours))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Invalid token.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(providerUser.Email) ||
|
|
||||||
!providerUser.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("User email does not match invite.");
|
|
||||||
}
|
|
||||||
|
|
||||||
providerUser.Status = ProviderUserStatusType.Accepted;
|
|
||||||
providerUser.UserId = user.Id;
|
|
||||||
providerUser.Email = null;
|
|
||||||
|
|
||||||
await _providerUserRepository.ReplaceAsync(providerUser);
|
|
||||||
|
|
||||||
return providerUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<Tuple<ProviderUser, string>>> ConfirmUsersAsync(Guid providerId, Dictionary<Guid, string> keys,
|
|
||||||
Guid confirmingUserId)
|
|
||||||
{
|
|
||||||
var providerUsers = await _providerUserRepository.GetManyAsync(keys.Keys);
|
|
||||||
var validProviderUsers = providerUsers
|
|
||||||
.Where(u => u.UserId != null)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (!validProviderUsers.Any())
|
|
||||||
{
|
|
||||||
return new List<Tuple<ProviderUser, string>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var validOrganizationUserIds = validProviderUsers.Select(u => u.UserId.Value).ToList();
|
|
||||||
|
|
||||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
|
||||||
var users = await _userRepository.GetManyAsync(validOrganizationUserIds);
|
|
||||||
|
|
||||||
var keyedFilteredUsers = validProviderUsers.ToDictionary(u => u.UserId.Value, u => u);
|
|
||||||
|
|
||||||
var result = new List<Tuple<ProviderUser, string>>();
|
|
||||||
var events = new List<(ProviderUser, EventType, DateTime?)>();
|
|
||||||
|
|
||||||
foreach (var user in users)
|
|
||||||
{
|
|
||||||
if (!keyedFilteredUsers.ContainsKey(user.Id))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
var providerUser = keyedFilteredUsers[user.Id];
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (providerUser.Status != ProviderUserStatusType.Accepted || providerUser.ProviderId != providerId)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Invalid user.");
|
|
||||||
}
|
|
||||||
|
|
||||||
providerUser.Status = ProviderUserStatusType.Confirmed;
|
|
||||||
providerUser.Key = keys[providerUser.Id];
|
|
||||||
providerUser.Email = null;
|
|
||||||
|
|
||||||
await _providerUserRepository.ReplaceAsync(providerUser);
|
|
||||||
events.Add((providerUser, EventType.ProviderUser_Confirmed, null));
|
|
||||||
await _mailService.SendProviderConfirmedEmailAsync(provider.Name, user.Email);
|
|
||||||
result.Add(Tuple.Create(providerUser, ""));
|
|
||||||
}
|
|
||||||
catch (BadRequestException e)
|
|
||||||
{
|
|
||||||
result.Add(Tuple.Create(providerUser, e.Message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _eventService.LogProviderUsersEventAsync(events);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveUserAsync(ProviderUser user, Guid savingUserId)
|
|
||||||
{
|
|
||||||
if (user.Id.Equals(default))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Invite the user first.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.Type != ProviderUserType.ProviderAdmin &&
|
|
||||||
!await HasConfirmedProviderAdminExceptAsync(user.ProviderId, new[] { user.Id }))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Provider must have at least one confirmed ProviderAdmin.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await _providerUserRepository.ReplaceAsync(user);
|
|
||||||
await _eventService.LogProviderUserEventAsync(user, EventType.ProviderUser_Updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<Tuple<ProviderUser, string>>> DeleteUsersAsync(Guid providerId,
|
|
||||||
IEnumerable<Guid> providerUserIds, Guid deletingUserId)
|
|
||||||
{
|
|
||||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
|
||||||
|
|
||||||
if (provider == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var providerUsers = await _providerUserRepository.GetManyAsync(providerUserIds);
|
|
||||||
var users = await _userRepository.GetManyAsync(providerUsers.Where(pu => pu.UserId.HasValue)
|
|
||||||
.Select(pu => pu.UserId.Value));
|
|
||||||
var keyedUsers = users.ToDictionary(u => u.Id);
|
|
||||||
|
|
||||||
if (!await HasConfirmedProviderAdminExceptAsync(providerId, providerUserIds))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Provider must have at least one confirmed ProviderAdmin.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = new List<Tuple<ProviderUser, string>>();
|
|
||||||
var deletedUserIds = new List<Guid>();
|
|
||||||
var events = new List<(ProviderUser, EventType, DateTime?)>();
|
|
||||||
|
|
||||||
foreach (var providerUser in providerUsers)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (providerUser.ProviderId != providerId)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Invalid user.");
|
|
||||||
}
|
|
||||||
if (providerUser.UserId == deletingUserId)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("You cannot remove yourself.");
|
|
||||||
}
|
|
||||||
|
|
||||||
events.Add((providerUser, EventType.ProviderUser_Removed, null));
|
|
||||||
|
|
||||||
var user = keyedUsers.GetValueOrDefault(providerUser.UserId.GetValueOrDefault());
|
|
||||||
var email = user == null ? providerUser.Email : user.Email;
|
|
||||||
if (!string.IsNullOrWhiteSpace(email))
|
|
||||||
{
|
|
||||||
await _mailService.SendProviderUserRemoved(provider.Name, email);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Add(Tuple.Create(providerUser, ""));
|
|
||||||
deletedUserIds.Add(providerUser.Id);
|
|
||||||
}
|
|
||||||
catch (BadRequestException e)
|
|
||||||
{
|
|
||||||
result.Add(Tuple.Create(providerUser, e.Message));
|
|
||||||
}
|
|
||||||
|
|
||||||
await _providerUserRepository.DeleteManyAsync(deletedUserIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _eventService.LogProviderUsersEventAsync(events);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task AddOrganization(Guid providerId, Guid organizationId, Guid addingUserId, string key)
|
|
||||||
{
|
|
||||||
var po = await _providerOrganizationRepository.GetByOrganizationId(organizationId);
|
|
||||||
if (po != null)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Organization already belongs to a provider.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
|
||||||
ThrowOnInvalidPlanType(organization.PlanType);
|
|
||||||
|
|
||||||
var providerOrganization = new ProviderOrganization
|
|
||||||
{
|
|
||||||
ProviderId = providerId,
|
|
||||||
OrganizationId = organizationId,
|
|
||||||
Key = key,
|
|
||||||
};
|
|
||||||
|
|
||||||
await _providerOrganizationRepository.CreateAsync(providerOrganization);
|
|
||||||
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId,
|
|
||||||
OrganizationSignup organizationSignup, string clientOwnerEmail, User user)
|
|
||||||
{
|
|
||||||
ThrowOnInvalidPlanType(organizationSignup.Plan);
|
|
||||||
|
|
||||||
var (organization, _) = await _organizationService.SignUpAsync(organizationSignup, true);
|
|
||||||
|
|
||||||
var providerOrganization = new ProviderOrganization
|
|
||||||
{
|
|
||||||
ProviderId = providerId,
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
Key = organizationSignup.OwnerKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
await _providerOrganizationRepository.CreateAsync(providerOrganization);
|
|
||||||
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Created);
|
|
||||||
|
|
||||||
await _organizationService.InviteUsersAsync(organization.Id, user.Id,
|
|
||||||
new (OrganizationUserInvite, string)[]
|
|
||||||
{
|
|
||||||
(
|
|
||||||
new OrganizationUserInvite
|
|
||||||
{
|
|
||||||
Emails = new[] { clientOwnerEmail },
|
|
||||||
AccessAll = true,
|
|
||||||
Type = OrganizationUserType.Owner,
|
|
||||||
Permissions = null,
|
|
||||||
Collections = Array.Empty<SelectionReadOnly>(),
|
|
||||||
},
|
|
||||||
null
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
return providerOrganization;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RemoveOrganizationAsync(Guid providerId, Guid providerOrganizationId, Guid removingUserId)
|
|
||||||
{
|
|
||||||
var providerOrganization = await _providerOrganizationRepository.GetByIdAsync(providerOrganizationId);
|
|
||||||
if (providerOrganization == null || providerOrganization.ProviderId != providerId)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Invalid organization.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await _organizationService.HasConfirmedOwnersExceptAsync(providerOrganization.OrganizationId, new Guid[] { }, includeProvider: false))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Organization needs to have at least one confirmed owner.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await _providerOrganizationRepository.DeleteAsync(providerOrganization);
|
|
||||||
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid ownerId)
|
|
||||||
{
|
|
||||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
|
||||||
var owner = await _userRepository.GetByIdAsync(ownerId);
|
|
||||||
if (owner == null)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Invalid owner.");
|
|
||||||
}
|
|
||||||
await SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail)
|
|
||||||
{
|
|
||||||
var token = _dataProtector.Protect($"ProviderSetupInvite {provider.Id} {ownerEmail} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
|
||||||
await _mailService.SendProviderSetupInviteEmailAsync(provider, token, ownerEmail);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogProviderAccessToOrganizationAsync(Guid organizationId)
|
|
||||||
{
|
|
||||||
if (organizationId == default)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var providerOrganization = await _providerOrganizationRepository.GetByOrganizationId(organizationId);
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
|
||||||
if (providerOrganization != null)
|
|
||||||
{
|
|
||||||
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_VaultAccessed);
|
|
||||||
}
|
|
||||||
if (organization != null)
|
|
||||||
{
|
|
||||||
await _eventService.LogOrganizationEventAsync(organization, EventType.Organization_VaultAccessed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SendInviteAsync(ProviderUser providerUser, Provider provider)
|
|
||||||
{
|
|
||||||
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
|
||||||
var token = _dataProtector.Protect(
|
|
||||||
$"ProviderUserInvite {providerUser.Id} {providerUser.Email} {nowMillis}");
|
|
||||||
await _mailService.SendProviderInviteEmailAsync(provider.Name, providerUser, token, providerUser.Email);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> HasConfirmedProviderAdminExceptAsync(Guid providerId, IEnumerable<Guid> providerUserIds)
|
|
||||||
{
|
|
||||||
var providerAdmins = await _providerUserRepository.GetManyByProviderAsync(providerId,
|
|
||||||
ProviderUserType.ProviderAdmin);
|
|
||||||
var confirmedOwners = providerAdmins.Where(o => o.Status == ProviderUserStatusType.Confirmed);
|
|
||||||
var confirmedOwnersIds = confirmedOwners.Select(u => u.Id);
|
|
||||||
return confirmedOwnersIds.Except(providerUserIds).Any();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ThrowOnInvalidPlanType(PlanType requestedType)
|
|
||||||
{
|
|
||||||
if (ProviderDisllowedOrganizationTypes.Contains(requestedType))
|
|
||||||
{
|
|
||||||
throw new BadRequestException($"Providers cannot manage organizations with the requested plan type ({requestedType}). Only Teams and Enterprise accounts are allowed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
using Bit.CommCore.Services;
|
|
||||||
using Bit.Core.Services;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Bit.CommCore.Utilities
|
|
||||||
{
|
|
||||||
public static class ServiceCollectionExtensions
|
|
||||||
{
|
|
||||||
public static void AddCommCoreServices(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddScoped<IProviderService, ProviderService>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +1,5 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\src\Core\Core.csproj" />
|
<ProjectReference Include="..\..\..\src\Core\Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
@ -0,0 +1,45 @@
|
|||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Entities;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||||
|
|
||||||
|
public class CreateAccessPoliciesCommand : ICreateAccessPoliciesCommand
|
||||||
|
{
|
||||||
|
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||||
|
|
||||||
|
public CreateAccessPoliciesCommand(IAccessPolicyRepository accessPolicyRepository)
|
||||||
|
{
|
||||||
|
_accessPolicyRepository = accessPolicyRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<BaseAccessPolicy>> CreateAsync(List<BaseAccessPolicy> accessPolicies)
|
||||||
|
{
|
||||||
|
var distinctAccessPolicies = accessPolicies.DistinctBy(baseAccessPolicy =>
|
||||||
|
{
|
||||||
|
return baseAccessPolicy switch
|
||||||
|
{
|
||||||
|
UserProjectAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.OrganizationUserId, ap.GrantedProjectId),
|
||||||
|
GroupProjectAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.GroupId, ap.GrantedProjectId),
|
||||||
|
ServiceAccountProjectAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.ServiceAccountId, ap.GrantedProjectId),
|
||||||
|
_ => throw new ArgumentException("Unsupported access policy type provided.", nameof(baseAccessPolicy))
|
||||||
|
};
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
if (accessPolicies.Count != distinctAccessPolicies.Count)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Resources must be unique");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var accessPolicy in accessPolicies)
|
||||||
|
{
|
||||||
|
if (await _accessPolicyRepository.AccessPolicyExists(accessPolicy))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Resource already exists");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _accessPolicyRepository.CreateManyAsync(accessPolicies);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||||
|
|
||||||
|
public class DeleteAccessPolicyCommand : IDeleteAccessPolicyCommand
|
||||||
|
{
|
||||||
|
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||||
|
|
||||||
|
public DeleteAccessPolicyCommand(IAccessPolicyRepository accessPolicyRepository)
|
||||||
|
{
|
||||||
|
_accessPolicyRepository = accessPolicyRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task DeleteAsync(Guid id)
|
||||||
|
{
|
||||||
|
var accessPolicy = await _accessPolicyRepository.GetByIdAsync(id);
|
||||||
|
if (accessPolicy == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _accessPolicyRepository.DeleteAsync(id);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Entities;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||||
|
|
||||||
|
public class UpdateAccessPolicyCommand : IUpdateAccessPolicyCommand
|
||||||
|
{
|
||||||
|
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||||
|
|
||||||
|
public UpdateAccessPolicyCommand(IAccessPolicyRepository accessPolicyRepository)
|
||||||
|
{
|
||||||
|
_accessPolicyRepository = accessPolicyRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BaseAccessPolicy> UpdateAsync(Guid id, bool read, bool write)
|
||||||
|
{
|
||||||
|
var accessPolicy = await _accessPolicyRepository.GetByIdAsync(id);
|
||||||
|
if (accessPolicy == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
accessPolicy.Read = read;
|
||||||
|
accessPolicy.Write = write;
|
||||||
|
accessPolicy.RevisionDate = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _accessPolicyRepository.ReplaceAsync(accessPolicy);
|
||||||
|
return accessPolicy;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Entities;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessTokens;
|
||||||
|
|
||||||
|
public class CreateAccessTokenCommand : ICreateAccessTokenCommand
|
||||||
|
{
|
||||||
|
private readonly IApiKeyRepository _apiKeyRepository;
|
||||||
|
private readonly int _clientSecretMaxLength = 30;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||||
|
|
||||||
|
public CreateAccessTokenCommand(
|
||||||
|
IApiKeyRepository apiKeyRepository,
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IServiceAccountRepository serviceAccountRepository)
|
||||||
|
{
|
||||||
|
_apiKeyRepository = apiKeyRepository;
|
||||||
|
_currentContext = currentContext;
|
||||||
|
_serviceAccountRepository = serviceAccountRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiKey> CreateAsync(ApiKey apiKey, Guid userId)
|
||||||
|
{
|
||||||
|
if (apiKey.ServiceAccountId == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(apiKey.ServiceAccountId.Value);
|
||||||
|
var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId);
|
||||||
|
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
|
||||||
|
|
||||||
|
var hasAccess = accessClient switch
|
||||||
|
{
|
||||||
|
AccessClientType.NoAccessCheck => true,
|
||||||
|
AccessClientType.User => await _serviceAccountRepository.UserHasWriteAccessToServiceAccount(
|
||||||
|
apiKey.ServiceAccountId.Value, userId),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasAccess)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKey.ClientSecret = CoreHelpers.SecureRandomString(_clientSecretMaxLength);
|
||||||
|
return await _apiKeyRepository.CreateAsync(apiKey);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Entities;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretsManager.Commands.Projects;
|
||||||
|
|
||||||
|
public class CreateProjectCommand : ICreateProjectCommand
|
||||||
|
{
|
||||||
|
private readonly IProjectRepository _projectRepository;
|
||||||
|
|
||||||
|
public CreateProjectCommand(IProjectRepository projectRepository)
|
||||||
|
{
|
||||||
|
_projectRepository = projectRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Project> CreateAsync(Project project)
|
||||||
|
{
|
||||||
|
return await _projectRepository.CreateAsync(project);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Entities;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretsManager.Commands.Projects;
|
||||||
|
|
||||||
|
public class DeleteProjectCommand : IDeleteProjectCommand
|
||||||
|
{
|
||||||
|
private readonly IProjectRepository _projectRepository;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
|
||||||
|
public DeleteProjectCommand(IProjectRepository projectRepository, ICurrentContext currentContext)
|
||||||
|
{
|
||||||
|
_projectRepository = projectRepository;
|
||||||
|
_currentContext = currentContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Tuple<Project, string>>> DeleteProjects(List<Guid> ids, Guid userId)
|
||||||
|
{
|
||||||
|
if (ids.Any() != true || userId == new Guid())
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var projects = (await _projectRepository.GetManyByIds(ids))?.ToList();
|
||||||
|
|
||||||
|
if (projects?.Any() != true || projects.Count != ids.Count)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure all projects belongs to the same organization
|
||||||
|
var organizationId = projects.First().OrganizationId;
|
||||||
|
if (projects.Any(p => p.OrganizationId != organizationId))
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
|
||||||
|
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
|
||||||
|
|
||||||
|
var results = new List<Tuple<Project, String>>(projects.Count);
|
||||||
|
var deleteIds = new List<Guid>();
|
||||||
|
|
||||||
|
foreach (var project in projects)
|
||||||
|
{
|
||||||
|
var hasAccess = accessClient switch
|
||||||
|
{
|
||||||
|
AccessClientType.NoAccessCheck => true,
|
||||||
|
AccessClientType.User => await _projectRepository.UserHasWriteAccessToProject(project.Id, userId),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasAccess)
|
||||||
|
{
|
||||||
|
results.Add(new Tuple<Project, string>(project, "access denied"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
results.Add(new Tuple<Project, string>(project, ""));
|
||||||
|
deleteIds.Add(project.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteIds.Count > 0)
|
||||||
|
{
|
||||||
|
await _projectRepository.DeleteManyByIdAsync(deleteIds);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
|||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Entities;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretsManager.Commands.Projects;
|
||||||
|
|
||||||
|
public class UpdateProjectCommand : IUpdateProjectCommand
|
||||||
|
{
|
||||||
|
private readonly IProjectRepository _projectRepository;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
|
||||||
|
public UpdateProjectCommand(IProjectRepository projectRepository, ICurrentContext currentContext)
|
||||||
|
{
|
||||||
|
_projectRepository = projectRepository;
|
||||||
|
_currentContext = currentContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Project> UpdateAsync(Project updatedProject, Guid userId)
|
||||||
|
{
|
||||||
|
var project = await _projectRepository.GetByIdAsync(updatedProject.Id);
|
||||||
|
if (project == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId);
|
||||||
|
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
|
||||||
|
|
||||||
|
var hasAccess = accessClient switch
|
||||||
|
{
|
||||||
|
AccessClientType.NoAccessCheck => true,
|
||||||
|
AccessClientType.User => await _projectRepository.UserHasWriteAccessToProject(updatedProject.Id, userId),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasAccess)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
project.Name = updatedProject.Name;
|
||||||
|
project.RevisionDate = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _projectRepository.ReplaceAsync(project);
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Entities;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretsManager.Commands.Secrets;
|
||||||
|
|
||||||
|
public class CreateSecretCommand : ICreateSecretCommand
|
||||||
|
{
|
||||||
|
private readonly ISecretRepository _secretRepository;
|
||||||
|
|
||||||
|
public CreateSecretCommand(ISecretRepository secretRepository)
|
||||||
|
{
|
||||||
|
_secretRepository = secretRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Secret> CreateAsync(Secret secret)
|
||||||
|
{
|
||||||
|
return await _secretRepository.CreateAsync(secret);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Entities;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretsManager.Commands.Secrets;
|
||||||
|
|
||||||
|
public class DeleteSecretCommand : IDeleteSecretCommand
|
||||||
|
{
|
||||||
|
private readonly ISecretRepository _secretRepository;
|
||||||
|
|
||||||
|
public DeleteSecretCommand(ISecretRepository secretRepository)
|
||||||
|
{
|
||||||
|
_secretRepository = secretRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Tuple<Secret, string>>> DeleteSecrets(List<Guid> ids)
|
||||||
|
{
|
||||||
|
var secrets = await _secretRepository.GetManyByIds(ids);
|
||||||
|
|
||||||
|
if (secrets?.Any() != true)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = ids.Select(id =>
|
||||||
|
{
|
||||||
|
var secret = secrets.FirstOrDefault(secret => secret.Id == id);
|
||||||
|
if (secret == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
// TODO Once permissions are implemented add check for each secret here.
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new Tuple<Secret, string>(secret, "");
|
||||||
|
}
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
await _secretRepository.SoftDeleteManyByIdAsync(ids);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
|||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Entities;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretsManager.Commands.Secrets;
|
||||||
|
|
||||||
|
public class UpdateSecretCommand : IUpdateSecretCommand
|
||||||
|
{
|
||||||
|
private readonly ISecretRepository _secretRepository;
|
||||||
|
|
||||||
|
public UpdateSecretCommand(ISecretRepository secretRepository)
|
||||||
|
{
|
||||||
|
_secretRepository = secretRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Secret> UpdateAsync(Secret secret)
|
||||||
|
{
|
||||||
|
var existingSecret = await _secretRepository.GetByIdAsync(secret.Id);
|
||||||
|
if (existingSecret == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
secret.OrganizationId = existingSecret.OrganizationId;
|
||||||
|
secret.CreationDate = existingSecret.CreationDate;
|
||||||
|
secret.DeletedDate = existingSecret.DeletedDate;
|
||||||
|
secret.RevisionDate = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _secretRepository.UpdateAsync(secret);
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Entities;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
|
||||||
|
|
||||||
|
public class CreateServiceAccountCommand : ICreateServiceAccountCommand
|
||||||
|
{
|
||||||
|
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||||
|
|
||||||
|
public CreateServiceAccountCommand(IServiceAccountRepository serviceAccountRepository)
|
||||||
|
{
|
||||||
|
_serviceAccountRepository = serviceAccountRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount)
|
||||||
|
{
|
||||||
|
return await _serviceAccountRepository.CreateAsync(serviceAccount);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Entities;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
|
||||||
|
|
||||||
|
public class UpdateServiceAccountCommand : IUpdateServiceAccountCommand
|
||||||
|
{
|
||||||
|
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
|
||||||
|
public UpdateServiceAccountCommand(IServiceAccountRepository serviceAccountRepository, ICurrentContext currentContext)
|
||||||
|
{
|
||||||
|
_serviceAccountRepository = serviceAccountRepository;
|
||||||
|
_currentContext = currentContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceAccount> UpdateAsync(ServiceAccount updatedServiceAccount, Guid userId)
|
||||||
|
{
|
||||||
|
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(updatedServiceAccount.Id);
|
||||||
|
if (serviceAccount == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId);
|
||||||
|
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
|
||||||
|
|
||||||
|
var hasAccess = accessClient switch
|
||||||
|
{
|
||||||
|
AccessClientType.NoAccessCheck => true,
|
||||||
|
AccessClientType.User => await _serviceAccountRepository.UserHasWriteAccessToServiceAccount(updatedServiceAccount.Id, userId),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasAccess)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceAccount.Name = updatedServiceAccount.Name;
|
||||||
|
serviceAccount.RevisionDate = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _serviceAccountRepository.ReplaceAsync(serviceAccount);
|
||||||
|
return serviceAccount;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
|
||||||
|
using Bit.Commercial.Core.SecretsManager.Commands.AccessTokens;
|
||||||
|
using Bit.Commercial.Core.SecretsManager.Commands.Projects;
|
||||||
|
using Bit.Commercial.Core.SecretsManager.Commands.Secrets;
|
||||||
|
using Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
|
||||||
|
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretsManager;
|
||||||
|
|
||||||
|
public static class SecretsManagerCollectionExtensions
|
||||||
|
{
|
||||||
|
public static void AddSecretsManagerServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
|
||||||
|
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
|
||||||
|
services.AddScoped<IDeleteSecretCommand, DeleteSecretCommand>();
|
||||||
|
services.AddScoped<ICreateProjectCommand, CreateProjectCommand>();
|
||||||
|
services.AddScoped<IUpdateProjectCommand, UpdateProjectCommand>();
|
||||||
|
services.AddScoped<IDeleteProjectCommand, DeleteProjectCommand>();
|
||||||
|
services.AddScoped<ICreateServiceAccountCommand, CreateServiceAccountCommand>();
|
||||||
|
services.AddScoped<IUpdateServiceAccountCommand, UpdateServiceAccountCommand>();
|
||||||
|
services.AddScoped<ICreateAccessTokenCommand, CreateAccessTokenCommand>();
|
||||||
|
services.AddScoped<ICreateAccessPoliciesCommand, CreateAccessPoliciesCommand>();
|
||||||
|
services.AddScoped<IUpdateAccessPolicyCommand, UpdateAccessPolicyCommand>();
|
||||||
|
services.AddScoped<IDeleteAccessPolicyCommand, DeleteAccessPolicyCommand>();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.SecretsManager;
|
||||||
|
|
||||||
|
public static class SecretsManagerServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static void AddCommercialSecretsManagerServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSecretsManagerServices();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,508 @@
|
|||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Entities.Provider;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Enums.Provider;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Models.Business.Provider;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.Services;
|
||||||
|
|
||||||
|
public class ProviderService : IProviderService
|
||||||
|
{
|
||||||
|
public static PlanType[] ProviderDisllowedOrganizationTypes = new[] { PlanType.Free, PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2019 };
|
||||||
|
|
||||||
|
private readonly IDataProtector _dataProtector;
|
||||||
|
private readonly IMailService _mailService;
|
||||||
|
private readonly IEventService _eventService;
|
||||||
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
private readonly IProviderRepository _providerRepository;
|
||||||
|
private readonly IProviderUserRepository _providerUserRepository;
|
||||||
|
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly IOrganizationService _organizationService;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
|
||||||
|
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
||||||
|
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
|
||||||
|
IUserService userService, IOrganizationService organizationService, IMailService mailService,
|
||||||
|
IDataProtectionProvider dataProtectionProvider, IEventService eventService,
|
||||||
|
IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
|
||||||
|
ICurrentContext currentContext)
|
||||||
|
{
|
||||||
|
_providerRepository = providerRepository;
|
||||||
|
_providerUserRepository = providerUserRepository;
|
||||||
|
_providerOrganizationRepository = providerOrganizationRepository;
|
||||||
|
_organizationRepository = organizationRepository;
|
||||||
|
_userRepository = userRepository;
|
||||||
|
_userService = userService;
|
||||||
|
_organizationService = organizationService;
|
||||||
|
_mailService = mailService;
|
||||||
|
_eventService = eventService;
|
||||||
|
_globalSettings = globalSettings;
|
||||||
|
_dataProtector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
||||||
|
_currentContext = currentContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateAsync(string ownerEmail)
|
||||||
|
{
|
||||||
|
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
|
||||||
|
if (owner == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid owner. Owner must be an existing Bitwarden user.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var provider = new Provider
|
||||||
|
{
|
||||||
|
Status = ProviderStatusType.Pending,
|
||||||
|
Enabled = true,
|
||||||
|
UseEvents = true,
|
||||||
|
};
|
||||||
|
await _providerRepository.CreateAsync(provider);
|
||||||
|
|
||||||
|
var providerUser = new ProviderUser
|
||||||
|
{
|
||||||
|
ProviderId = provider.Id,
|
||||||
|
UserId = owner.Id,
|
||||||
|
Type = ProviderUserType.ProviderAdmin,
|
||||||
|
Status = ProviderUserStatusType.Confirmed,
|
||||||
|
};
|
||||||
|
await _providerUserRepository.CreateAsync(providerUser);
|
||||||
|
await SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key)
|
||||||
|
{
|
||||||
|
var owner = await _userService.GetUserByIdAsync(ownerUserId);
|
||||||
|
if (owner == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid owner.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider.Status != ProviderStatusType.Pending)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Provider is already setup.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CoreHelpers.TokenIsValid("ProviderSetupInvite", _dataProtector, token, owner.Email, provider.Id,
|
||||||
|
_globalSettings.OrganizationInviteExpirationHours))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerUser = await _providerUserRepository.GetByProviderUserAsync(provider.Id, ownerUserId);
|
||||||
|
if (!(providerUser is { Type: ProviderUserType.ProviderAdmin }))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid owner.");
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.Status = ProviderStatusType.Created;
|
||||||
|
await _providerRepository.UpsertAsync(provider);
|
||||||
|
|
||||||
|
providerUser.Key = key;
|
||||||
|
await _providerUserRepository.ReplaceAsync(providerUser);
|
||||||
|
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(Provider provider, bool updateBilling = false)
|
||||||
|
{
|
||||||
|
if (provider.Id == default)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Cannot create provider this way.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _providerRepository.ReplaceAsync(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<ProviderUser>> InviteUserAsync(ProviderUserInvite<string> invite)
|
||||||
|
{
|
||||||
|
if (!_currentContext.ProviderManageUsers(invite.ProviderId))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Invalid permissions.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var emails = invite?.UserIdentifiers;
|
||||||
|
var invitingUser = await _providerUserRepository.GetByProviderUserAsync(invite.ProviderId, invite.InvitingUserId);
|
||||||
|
|
||||||
|
var provider = await _providerRepository.GetByIdAsync(invite.ProviderId);
|
||||||
|
if (provider == null || emails == null || !emails.Any())
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerUsers = new List<ProviderUser>();
|
||||||
|
foreach (var email in emails)
|
||||||
|
{
|
||||||
|
// Make sure user is not already invited
|
||||||
|
var existingProviderUserCount =
|
||||||
|
await _providerUserRepository.GetCountByProviderAsync(invite.ProviderId, email, false);
|
||||||
|
if (existingProviderUserCount > 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerUser = new ProviderUser
|
||||||
|
{
|
||||||
|
ProviderId = invite.ProviderId,
|
||||||
|
UserId = null,
|
||||||
|
Email = email.ToLowerInvariant(),
|
||||||
|
Key = null,
|
||||||
|
Type = invite.Type,
|
||||||
|
Status = ProviderUserStatusType.Invited,
|
||||||
|
CreationDate = DateTime.UtcNow,
|
||||||
|
RevisionDate = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
|
||||||
|
await _providerUserRepository.CreateAsync(providerUser);
|
||||||
|
|
||||||
|
await SendInviteAsync(providerUser, provider);
|
||||||
|
providerUsers.Add(providerUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _eventService.LogProviderUsersEventAsync(providerUsers.Select(pu => (pu, EventType.ProviderUser_Invited, null as DateTime?)));
|
||||||
|
|
||||||
|
return providerUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Tuple<ProviderUser, string>>> ResendInvitesAsync(ProviderUserInvite<Guid> invite)
|
||||||
|
{
|
||||||
|
if (!_currentContext.ProviderManageUsers(invite.ProviderId))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid permissions.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerUsers = await _providerUserRepository.GetManyAsync(invite.UserIdentifiers);
|
||||||
|
var provider = await _providerRepository.GetByIdAsync(invite.ProviderId);
|
||||||
|
|
||||||
|
var result = new List<Tuple<ProviderUser, string>>();
|
||||||
|
foreach (var providerUser in providerUsers)
|
||||||
|
{
|
||||||
|
if (providerUser.Status != ProviderUserStatusType.Invited || providerUser.ProviderId != invite.ProviderId)
|
||||||
|
{
|
||||||
|
result.Add(Tuple.Create(providerUser, "User invalid."));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendInviteAsync(providerUser, provider);
|
||||||
|
result.Add(Tuple.Create(providerUser, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ProviderUser> AcceptUserAsync(Guid providerUserId, User user, string token)
|
||||||
|
{
|
||||||
|
var providerUser = await _providerUserRepository.GetByIdAsync(providerUserId);
|
||||||
|
if (providerUser == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("User invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (providerUser.Status != ProviderUserStatusType.Invited)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Already accepted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CoreHelpers.TokenIsValid("ProviderUserInvite", _dataProtector, token, user.Email, providerUser.Id,
|
||||||
|
_globalSettings.OrganizationInviteExpirationHours))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(providerUser.Email) ||
|
||||||
|
!providerUser.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("User email does not match invite.");
|
||||||
|
}
|
||||||
|
|
||||||
|
providerUser.Status = ProviderUserStatusType.Accepted;
|
||||||
|
providerUser.UserId = user.Id;
|
||||||
|
providerUser.Email = null;
|
||||||
|
|
||||||
|
await _providerUserRepository.ReplaceAsync(providerUser);
|
||||||
|
|
||||||
|
return providerUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Tuple<ProviderUser, string>>> ConfirmUsersAsync(Guid providerId, Dictionary<Guid, string> keys,
|
||||||
|
Guid confirmingUserId)
|
||||||
|
{
|
||||||
|
var providerUsers = await _providerUserRepository.GetManyAsync(keys.Keys);
|
||||||
|
var validProviderUsers = providerUsers
|
||||||
|
.Where(u => u.UserId != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (!validProviderUsers.Any())
|
||||||
|
{
|
||||||
|
return new List<Tuple<ProviderUser, string>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var validOrganizationUserIds = validProviderUsers.Select(u => u.UserId.Value).ToList();
|
||||||
|
|
||||||
|
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||||
|
var users = await _userRepository.GetManyAsync(validOrganizationUserIds);
|
||||||
|
|
||||||
|
var keyedFilteredUsers = validProviderUsers.ToDictionary(u => u.UserId.Value, u => u);
|
||||||
|
|
||||||
|
var result = new List<Tuple<ProviderUser, string>>();
|
||||||
|
var events = new List<(ProviderUser, EventType, DateTime?)>();
|
||||||
|
|
||||||
|
foreach (var user in users)
|
||||||
|
{
|
||||||
|
if (!keyedFilteredUsers.ContainsKey(user.Id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var providerUser = keyedFilteredUsers[user.Id];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (providerUser.Status != ProviderUserStatusType.Accepted || providerUser.ProviderId != providerId)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid user.");
|
||||||
|
}
|
||||||
|
|
||||||
|
providerUser.Status = ProviderUserStatusType.Confirmed;
|
||||||
|
providerUser.Key = keys[providerUser.Id];
|
||||||
|
providerUser.Email = null;
|
||||||
|
|
||||||
|
await _providerUserRepository.ReplaceAsync(providerUser);
|
||||||
|
events.Add((providerUser, EventType.ProviderUser_Confirmed, null));
|
||||||
|
await _mailService.SendProviderConfirmedEmailAsync(provider.Name, user.Email);
|
||||||
|
result.Add(Tuple.Create(providerUser, ""));
|
||||||
|
}
|
||||||
|
catch (BadRequestException e)
|
||||||
|
{
|
||||||
|
result.Add(Tuple.Create(providerUser, e.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _eventService.LogProviderUsersEventAsync(events);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveUserAsync(ProviderUser user, Guid savingUserId)
|
||||||
|
{
|
||||||
|
if (user.Id.Equals(default))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invite the user first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.Type != ProviderUserType.ProviderAdmin &&
|
||||||
|
!await HasConfirmedProviderAdminExceptAsync(user.ProviderId, new[] { user.Id }))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Provider must have at least one confirmed ProviderAdmin.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _providerUserRepository.ReplaceAsync(user);
|
||||||
|
await _eventService.LogProviderUserEventAsync(user, EventType.ProviderUser_Updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Tuple<ProviderUser, string>>> DeleteUsersAsync(Guid providerId,
|
||||||
|
IEnumerable<Guid> providerUserIds, Guid deletingUserId)
|
||||||
|
{
|
||||||
|
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||||
|
|
||||||
|
if (provider == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerUsers = await _providerUserRepository.GetManyAsync(providerUserIds);
|
||||||
|
var users = await _userRepository.GetManyAsync(providerUsers.Where(pu => pu.UserId.HasValue)
|
||||||
|
.Select(pu => pu.UserId.Value));
|
||||||
|
var keyedUsers = users.ToDictionary(u => u.Id);
|
||||||
|
|
||||||
|
if (!await HasConfirmedProviderAdminExceptAsync(providerId, providerUserIds))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Provider must have at least one confirmed ProviderAdmin.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<Tuple<ProviderUser, string>>();
|
||||||
|
var deletedUserIds = new List<Guid>();
|
||||||
|
var events = new List<(ProviderUser, EventType, DateTime?)>();
|
||||||
|
|
||||||
|
foreach (var providerUser in providerUsers)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (providerUser.ProviderId != providerId)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid user.");
|
||||||
|
}
|
||||||
|
if (providerUser.UserId == deletingUserId)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You cannot remove yourself.");
|
||||||
|
}
|
||||||
|
|
||||||
|
events.Add((providerUser, EventType.ProviderUser_Removed, null));
|
||||||
|
|
||||||
|
var user = keyedUsers.GetValueOrDefault(providerUser.UserId.GetValueOrDefault());
|
||||||
|
var email = user == null ? providerUser.Email : user.Email;
|
||||||
|
if (!string.IsNullOrWhiteSpace(email))
|
||||||
|
{
|
||||||
|
await _mailService.SendProviderUserRemoved(provider.Name, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(Tuple.Create(providerUser, ""));
|
||||||
|
deletedUserIds.Add(providerUser.Id);
|
||||||
|
}
|
||||||
|
catch (BadRequestException e)
|
||||||
|
{
|
||||||
|
result.Add(Tuple.Create(providerUser, e.Message));
|
||||||
|
}
|
||||||
|
|
||||||
|
await _providerUserRepository.DeleteManyAsync(deletedUserIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _eventService.LogProviderUsersEventAsync(events);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddOrganization(Guid providerId, Guid organizationId, Guid addingUserId, string key)
|
||||||
|
{
|
||||||
|
var po = await _providerOrganizationRepository.GetByOrganizationId(organizationId);
|
||||||
|
if (po != null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Organization already belongs to a provider.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
ThrowOnInvalidPlanType(organization.PlanType);
|
||||||
|
|
||||||
|
var providerOrganization = new ProviderOrganization
|
||||||
|
{
|
||||||
|
ProviderId = providerId,
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
Key = key,
|
||||||
|
};
|
||||||
|
|
||||||
|
await _providerOrganizationRepository.CreateAsync(providerOrganization);
|
||||||
|
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId,
|
||||||
|
OrganizationSignup organizationSignup, string clientOwnerEmail, User user)
|
||||||
|
{
|
||||||
|
ThrowOnInvalidPlanType(organizationSignup.Plan);
|
||||||
|
|
||||||
|
var (organization, _) = await _organizationService.SignUpAsync(organizationSignup, true);
|
||||||
|
|
||||||
|
var providerOrganization = new ProviderOrganization
|
||||||
|
{
|
||||||
|
ProviderId = providerId,
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
Key = organizationSignup.OwnerKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
await _providerOrganizationRepository.CreateAsync(providerOrganization);
|
||||||
|
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Created);
|
||||||
|
|
||||||
|
await _organizationService.InviteUsersAsync(organization.Id, user.Id,
|
||||||
|
new (OrganizationUserInvite, string)[]
|
||||||
|
{
|
||||||
|
(
|
||||||
|
new OrganizationUserInvite
|
||||||
|
{
|
||||||
|
Emails = new[] { clientOwnerEmail },
|
||||||
|
AccessAll = true,
|
||||||
|
Type = OrganizationUserType.Owner,
|
||||||
|
Permissions = null,
|
||||||
|
Collections = Array.Empty<CollectionAccessSelection>(),
|
||||||
|
},
|
||||||
|
null
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
return providerOrganization;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveOrganizationAsync(Guid providerId, Guid providerOrganizationId, Guid removingUserId)
|
||||||
|
{
|
||||||
|
var providerOrganization = await _providerOrganizationRepository.GetByIdAsync(providerOrganizationId);
|
||||||
|
if (providerOrganization == null || providerOrganization.ProviderId != providerId)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid organization.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await _organizationService.HasConfirmedOwnersExceptAsync(providerOrganization.OrganizationId, new Guid[] { }, includeProvider: false))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Organization needs to have at least one confirmed owner.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _providerOrganizationRepository.DeleteAsync(providerOrganization);
|
||||||
|
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid ownerId)
|
||||||
|
{
|
||||||
|
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||||
|
var owner = await _userRepository.GetByIdAsync(ownerId);
|
||||||
|
if (owner == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid owner.");
|
||||||
|
}
|
||||||
|
await SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail)
|
||||||
|
{
|
||||||
|
var token = _dataProtector.Protect($"ProviderSetupInvite {provider.Id} {ownerEmail} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||||
|
await _mailService.SendProviderSetupInviteEmailAsync(provider, token, ownerEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LogProviderAccessToOrganizationAsync(Guid organizationId)
|
||||||
|
{
|
||||||
|
if (organizationId == default)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerOrganization = await _providerOrganizationRepository.GetByOrganizationId(organizationId);
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
if (providerOrganization != null)
|
||||||
|
{
|
||||||
|
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_VaultAccessed);
|
||||||
|
}
|
||||||
|
if (organization != null)
|
||||||
|
{
|
||||||
|
await _eventService.LogOrganizationEventAsync(organization, EventType.Organization_VaultAccessed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendInviteAsync(ProviderUser providerUser, Provider provider)
|
||||||
|
{
|
||||||
|
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
||||||
|
var token = _dataProtector.Protect(
|
||||||
|
$"ProviderUserInvite {providerUser.Id} {providerUser.Email} {nowMillis}");
|
||||||
|
await _mailService.SendProviderInviteEmailAsync(provider.Name, providerUser, token, providerUser.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> HasConfirmedProviderAdminExceptAsync(Guid providerId, IEnumerable<Guid> providerUserIds)
|
||||||
|
{
|
||||||
|
var providerAdmins = await _providerUserRepository.GetManyByProviderAsync(providerId,
|
||||||
|
ProviderUserType.ProviderAdmin);
|
||||||
|
var confirmedOwners = providerAdmins.Where(o => o.Status == ProviderUserStatusType.Confirmed);
|
||||||
|
var confirmedOwnersIds = confirmedOwners.Select(u => u.Id);
|
||||||
|
return confirmedOwnersIds.Except(providerUserIds).Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThrowOnInvalidPlanType(PlanType requestedType)
|
||||||
|
{
|
||||||
|
if (ProviderDisllowedOrganizationTypes.Contains(requestedType))
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Providers cannot manage organizations with the requested plan type ({requestedType}). Only Teams and Enterprise accounts are allowed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
using Bit.Commercial.Core.Services;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.Utilities;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static void AddCommercialCoreServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<IProviderService, ProviderService>();
|
||||||
|
}
|
||||||
|
}
|
2586
bitwarden_license/src/Commercial.Core/packages.lock.json
Normal file
2586
bitwarden_license/src/Commercial.Core/packages.lock.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\Core\Core.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\Infrastructure.EntityFramework\Infrastructure.EntityFramework.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
@ -0,0 +1,184 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPolicyRepository
|
||||||
|
{
|
||||||
|
public AccessPolicyRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base(serviceScopeFactory,
|
||||||
|
mapper)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Core.SecretsManager.Entities.BaseAccessPolicy>> CreateManyAsync(List<Core.SecretsManager.Entities.BaseAccessPolicy> baseAccessPolicies)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
foreach (var baseAccessPolicy in baseAccessPolicies)
|
||||||
|
{
|
||||||
|
baseAccessPolicy.SetNewId();
|
||||||
|
switch (baseAccessPolicy)
|
||||||
|
{
|
||||||
|
case Core.SecretsManager.Entities.UserProjectAccessPolicy accessPolicy:
|
||||||
|
{
|
||||||
|
var entity =
|
||||||
|
Mapper.Map<UserProjectAccessPolicy>(accessPolicy);
|
||||||
|
await dbContext.AddAsync(entity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Core.SecretsManager.Entities.UserServiceAccountAccessPolicy accessPolicy:
|
||||||
|
{
|
||||||
|
var entity =
|
||||||
|
Mapper.Map<UserServiceAccountAccessPolicy>(accessPolicy);
|
||||||
|
await dbContext.AddAsync(entity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Core.SecretsManager.Entities.GroupProjectAccessPolicy accessPolicy:
|
||||||
|
{
|
||||||
|
var entity = Mapper.Map<GroupProjectAccessPolicy>(accessPolicy);
|
||||||
|
await dbContext.AddAsync(entity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy accessPolicy:
|
||||||
|
{
|
||||||
|
var entity = Mapper.Map<GroupServiceAccountAccessPolicy>(accessPolicy);
|
||||||
|
await dbContext.AddAsync(entity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy accessPolicy:
|
||||||
|
{
|
||||||
|
var entity = Mapper.Map<ServiceAccountProjectAccessPolicy>(accessPolicy);
|
||||||
|
await dbContext.AddAsync(entity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
return baseAccessPolicies;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> AccessPolicyExists(Core.SecretsManager.Entities.BaseAccessPolicy baseAccessPolicy)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
switch (baseAccessPolicy)
|
||||||
|
{
|
||||||
|
case Core.SecretsManager.Entities.UserProjectAccessPolicy accessPolicy:
|
||||||
|
{
|
||||||
|
var policy = await dbContext.UserProjectAccessPolicy
|
||||||
|
.Where(c => c.OrganizationUserId == accessPolicy.OrganizationUserId &&
|
||||||
|
c.GrantedProjectId == accessPolicy.GrantedProjectId)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
return policy != null;
|
||||||
|
}
|
||||||
|
case Core.SecretsManager.Entities.GroupProjectAccessPolicy accessPolicy:
|
||||||
|
{
|
||||||
|
var policy = await dbContext.GroupProjectAccessPolicy
|
||||||
|
.Where(c => c.GroupId == accessPolicy.GroupId &&
|
||||||
|
c.GrantedProjectId == accessPolicy.GrantedProjectId)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
return policy != null;
|
||||||
|
}
|
||||||
|
case Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy accessPolicy:
|
||||||
|
{
|
||||||
|
var policy = await dbContext.ServiceAccountProjectAccessPolicy
|
||||||
|
.Where(c => c.ServiceAccountId == accessPolicy.ServiceAccountId &&
|
||||||
|
c.GrantedProjectId == accessPolicy.GrantedProjectId)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
return policy != null;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new ArgumentException("Unsupported access policy type provided.", nameof(baseAccessPolicy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Core.SecretsManager.Entities.BaseAccessPolicy?> GetByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var entity = await dbContext.AccessPolicies.Where(ap => ap.Id == id)
|
||||||
|
.Include(ap => ((UserProjectAccessPolicy)ap).OrganizationUser.User)
|
||||||
|
.Include(ap => ((GroupProjectAccessPolicy)ap).Group)
|
||||||
|
.Include(ap => ((ServiceAccountProjectAccessPolicy)ap).ServiceAccount)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (entity == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MapToCore(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReplaceAsync(Core.SecretsManager.Entities.BaseAccessPolicy baseAccessPolicy)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var entity = await dbContext.AccessPolicies.FindAsync(baseAccessPolicy.Id);
|
||||||
|
if (entity != null)
|
||||||
|
{
|
||||||
|
dbContext.AccessPolicies.Attach(entity);
|
||||||
|
entity.Write = baseAccessPolicy.Write;
|
||||||
|
entity.Read = baseAccessPolicy.Read;
|
||||||
|
entity.RevisionDate = baseAccessPolicy.RevisionDate;
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>?> GetManyByProjectId(Guid id)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
|
||||||
|
var entities = await dbContext.AccessPolicies.Where(ap =>
|
||||||
|
((UserProjectAccessPolicy)ap).GrantedProjectId == id ||
|
||||||
|
((GroupProjectAccessPolicy)ap).GrantedProjectId == id ||
|
||||||
|
((ServiceAccountProjectAccessPolicy)ap).GrantedProjectId == id)
|
||||||
|
.Include(ap => ((UserProjectAccessPolicy)ap).OrganizationUser.User)
|
||||||
|
.Include(ap => ((GroupProjectAccessPolicy)ap).Group)
|
||||||
|
.Include(ap => ((ServiceAccountProjectAccessPolicy)ap).ServiceAccount)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return !entities.Any() ? null : entities.Select(MapToCore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(Guid id)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var entity = await dbContext.AccessPolicies.FindAsync(id);
|
||||||
|
if (entity != null)
|
||||||
|
{
|
||||||
|
dbContext.Remove(entity);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Core.SecretsManager.Entities.BaseAccessPolicy MapToCore(BaseAccessPolicy baseAccessPolicyEntity)
|
||||||
|
{
|
||||||
|
return baseAccessPolicyEntity switch
|
||||||
|
{
|
||||||
|
UserProjectAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.UserProjectAccessPolicy>(ap),
|
||||||
|
GroupProjectAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.GroupProjectAccessPolicy>(ap),
|
||||||
|
ServiceAccountProjectAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy>(ap),
|
||||||
|
_ => throw new ArgumentException("Unsupported access policy type")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,106 @@
|
|||||||
|
using System.Linq.Expressions;
|
||||||
|
using AutoMapper;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project, Project, Guid>, IProjectRepository
|
||||||
|
{
|
||||||
|
public ProjectRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
|
||||||
|
: base(serviceScopeFactory, mapper, db => db.Project)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public override async Task<Core.SecretsManager.Entities.Project> GetByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var project = await dbContext.Project
|
||||||
|
.Where(c => c.Id == id && c.DeletedDate == null)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
return Mapper.Map<Core.SecretsManager.Entities.Project>(project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Core.SecretsManager.Entities.Project>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var query = dbContext.Project.Where(p => p.OrganizationId == organizationId && p.DeletedDate == null);
|
||||||
|
|
||||||
|
query = accessType switch
|
||||||
|
{
|
||||||
|
AccessClientType.NoAccessCheck => query,
|
||||||
|
AccessClientType.User => query.Where(UserHasReadAccessToProject(userId)),
|
||||||
|
AccessClientType.ServiceAccount => query.Where(ServiceAccountHasReadAccessToProject(userId)),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
var projects = await query.OrderBy(p => p.RevisionDate).ToListAsync();
|
||||||
|
return Mapper.Map<List<Core.SecretsManager.Entities.Project>>(projects);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Expression<Func<Project, bool>> UserHasReadAccessToProject(Guid userId) => p =>
|
||||||
|
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
||||||
|
p.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));
|
||||||
|
|
||||||
|
private static Expression<Func<Project, bool>> UserHasWriteAccessToProject(Guid userId) => p =>
|
||||||
|
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
|
||||||
|
p.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write));
|
||||||
|
|
||||||
|
private static Expression<Func<Project, bool>> ServiceAccountHasReadAccessToProject(Guid serviceAccountId) => p =>
|
||||||
|
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read);
|
||||||
|
|
||||||
|
public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var projects = dbContext.Project.Where(c => ids.Contains(c.Id));
|
||||||
|
await projects.ForEachAsync(project =>
|
||||||
|
{
|
||||||
|
dbContext.Remove(project);
|
||||||
|
});
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Core.SecretsManager.Entities.Project>> GetManyByIds(IEnumerable<Guid> ids)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var projects = await dbContext.Project
|
||||||
|
.Where(c => ids.Contains(c.Id) && c.DeletedDate == null)
|
||||||
|
.ToListAsync();
|
||||||
|
return Mapper.Map<List<Core.SecretsManager.Entities.Project>>(projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UserHasReadAccessToProject(Guid id, Guid userId)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var query = dbContext.Project
|
||||||
|
.Where(p => p.Id == id)
|
||||||
|
.Where(UserHasReadAccessToProject(userId));
|
||||||
|
|
||||||
|
return await query.AnyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UserHasWriteAccessToProject(Guid id, Guid userId)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var query = dbContext.Project
|
||||||
|
.Where(p => p.Id == id)
|
||||||
|
.Where(UserHasWriteAccessToProject(userId));
|
||||||
|
|
||||||
|
return await query.AnyAsync();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,139 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret, Secret, Guid>, ISecretRepository
|
||||||
|
{
|
||||||
|
public SecretRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
|
||||||
|
: base(serviceScopeFactory, mapper, db => db.Secret)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public override async Task<Core.SecretsManager.Entities.Secret> GetByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var secret = await dbContext.Secret
|
||||||
|
.Include("Projects")
|
||||||
|
.Where(c => c.Id == id && c.DeletedDate == null)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
return Mapper.Map<Core.SecretsManager.Entities.Secret>(secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByIds(IEnumerable<Guid> ids)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var secrets = await dbContext.Secret
|
||||||
|
.Where(c => ids.Contains(c.Id) && c.DeletedDate == null)
|
||||||
|
.ToListAsync();
|
||||||
|
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdAsync(Guid organizationId)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var secrets = await dbContext.Secret
|
||||||
|
.Where(c => c.OrganizationId == organizationId && c.DeletedDate == null)
|
||||||
|
.Include("Projects")
|
||||||
|
.OrderBy(c => c.RevisionDate)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByProjectIdAsync(Guid projectId)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var secrets = await dbContext.Secret
|
||||||
|
.Where(s => s.Projects.Any(p => p.Id == projectId) && s.DeletedDate == null).Include("Projects")
|
||||||
|
.OrderBy(s => s.RevisionDate).ToListAsync();
|
||||||
|
|
||||||
|
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<Core.SecretsManager.Entities.Secret> CreateAsync(Core.SecretsManager.Entities.Secret secret)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
secret.SetNewId();
|
||||||
|
var entity = Mapper.Map<Secret>(secret);
|
||||||
|
|
||||||
|
if (secret.Projects?.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var p in entity.Projects)
|
||||||
|
{
|
||||||
|
dbContext.Attach(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.AddAsync(entity);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
secret.Id = entity.Id;
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Core.SecretsManager.Entities.Secret> UpdateAsync(Core.SecretsManager.Entities.Secret secret)
|
||||||
|
{
|
||||||
|
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var mappedEntity = Mapper.Map<Secret>(secret);
|
||||||
|
var entity = await dbContext.Secret
|
||||||
|
.Include("Projects")
|
||||||
|
.FirstAsync(s => s.Id == secret.Id);
|
||||||
|
|
||||||
|
foreach (var p in entity.Projects?.Where(p => mappedEntity.Projects.All(mp => mp.Id != p.Id)))
|
||||||
|
{
|
||||||
|
entity.Projects.Remove(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new relationships
|
||||||
|
foreach (var project in mappedEntity.Projects?.Where(p => entity.Projects.All(ep => ep.Id != p.Id)))
|
||||||
|
{
|
||||||
|
var p = dbContext.AttachToOrGet<Project>(_ => _.Id == project.Id, () => project);
|
||||||
|
entity.Projects.Add(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
dbContext.Entry(entity).CurrentValues.SetValues(mappedEntity);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SoftDeleteManyByIdAsync(IEnumerable<Guid> ids)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var utcNow = DateTime.UtcNow;
|
||||||
|
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
|
||||||
|
await secrets.ForEachAsync(secret =>
|
||||||
|
{
|
||||||
|
dbContext.Attach(secret);
|
||||||
|
secret.DeletedDate = utcNow;
|
||||||
|
secret.RevisionDate = utcNow;
|
||||||
|
});
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
using System.Linq.Expressions;
|
||||||
|
using AutoMapper;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.ServiceAccount, ServiceAccount, Guid>, IServiceAccountRepository
|
||||||
|
{
|
||||||
|
public ServiceAccountRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
|
||||||
|
: base(serviceScopeFactory, mapper, db => db.ServiceAccount)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Core.SecretsManager.Entities.ServiceAccount>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var query = dbContext.ServiceAccount.Where(c => c.OrganizationId == organizationId);
|
||||||
|
|
||||||
|
query = accessType switch
|
||||||
|
{
|
||||||
|
AccessClientType.NoAccessCheck => query,
|
||||||
|
AccessClientType.User => query.Where(UserHasReadAccessToServiceAccount(userId)),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
var serviceAccounts = await query.OrderBy(c => c.RevisionDate).ToListAsync();
|
||||||
|
return Mapper.Map<List<Core.SecretsManager.Entities.ServiceAccount>>(serviceAccounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UserHasReadAccessToServiceAccount(Guid id, Guid userId)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var query = dbContext.ServiceAccount
|
||||||
|
.Where(sa => sa.Id == id)
|
||||||
|
.Where(UserHasReadAccessToServiceAccount(userId));
|
||||||
|
|
||||||
|
return await query.AnyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UserHasWriteAccessToServiceAccount(Guid id, Guid userId)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var query = dbContext.ServiceAccount
|
||||||
|
.Where(sa => sa.Id == id)
|
||||||
|
.Where(UserHasWriteAccessToServiceAccount(userId));
|
||||||
|
|
||||||
|
return await query.AnyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Expression<Func<ServiceAccount, bool>> UserHasReadAccessToServiceAccount(Guid userId) => sa =>
|
||||||
|
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
||||||
|
sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));
|
||||||
|
|
||||||
|
private static Expression<Func<ServiceAccount, bool>> UserHasWriteAccessToServiceAccount(Guid userId) => sa =>
|
||||||
|
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
|
||||||
|
sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write));
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
using Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager;
|
||||||
|
|
||||||
|
public static class SecretsManagerEfServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static void AddSecretsManagerEfRepositories(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<IAccessPolicyRepository, AccessPolicyRepository>();
|
||||||
|
services.AddSingleton<ISecretRepository, SecretRepository>();
|
||||||
|
services.AddSingleton<IProjectRepository, ProjectRepository>();
|
||||||
|
services.AddSingleton<IServiceAccountRepository, ServiceAccountRepository>();
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
4
bitwarden_license/src/Scim/.dockerignore
Normal file
4
bitwarden_license/src/Scim/.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
*
|
||||||
|
!obj/build-output/publish/*
|
||||||
|
!obj/Docker/empty/
|
||||||
|
!entrypoint.sh
|
20
bitwarden_license/src/Scim/Context/IScimContext.cs
Normal file
20
bitwarden_license/src/Scim/Context/IScimContext.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Context;
|
||||||
|
|
||||||
|
public interface IScimContext
|
||||||
|
{
|
||||||
|
ScimProviderType RequestScimProvider { get; set; }
|
||||||
|
ScimConfig ScimConfiguration { get; set; }
|
||||||
|
Guid? OrganizationId { get; set; }
|
||||||
|
Organization Organization { get; set; }
|
||||||
|
Task BuildAsync(
|
||||||
|
HttpContext httpContext,
|
||||||
|
GlobalSettings globalSettings,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationConnectionRepository organizationConnectionRepository);
|
||||||
|
}
|
63
bitwarden_license/src/Scim/Context/ScimContext.cs
Normal file
63
bitwarden_license/src/Scim/Context/ScimContext.cs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Context;
|
||||||
|
|
||||||
|
public class ScimContext : IScimContext
|
||||||
|
{
|
||||||
|
private bool _builtHttpContext;
|
||||||
|
|
||||||
|
public ScimProviderType RequestScimProvider { get; set; } = ScimProviderType.Default;
|
||||||
|
public ScimConfig ScimConfiguration { get; set; }
|
||||||
|
public Guid? OrganizationId { get; set; }
|
||||||
|
public Organization Organization { get; set; }
|
||||||
|
|
||||||
|
public async virtual Task BuildAsync(
|
||||||
|
HttpContext httpContext,
|
||||||
|
GlobalSettings globalSettings,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationConnectionRepository organizationConnectionRepository)
|
||||||
|
{
|
||||||
|
if (_builtHttpContext)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_builtHttpContext = true;
|
||||||
|
|
||||||
|
string orgIdString = null;
|
||||||
|
if (httpContext.Request.RouteValues.TryGetValue("organizationId", out var orgIdObject))
|
||||||
|
{
|
||||||
|
orgIdString = orgIdObject?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Guid.TryParse(orgIdString, out var orgId))
|
||||||
|
{
|
||||||
|
OrganizationId = orgId;
|
||||||
|
Organization = await organizationRepository.GetByIdAsync(orgId);
|
||||||
|
if (Organization != null)
|
||||||
|
{
|
||||||
|
var scimConnections = await organizationConnectionRepository.GetByOrganizationIdTypeAsync(Organization.Id,
|
||||||
|
OrganizationConnectionType.Scim);
|
||||||
|
ScimConfiguration = scimConnections?.FirstOrDefault()?.GetConfig<ScimConfig>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RequestScimProvider == ScimProviderType.Default &&
|
||||||
|
httpContext.Request.Headers.TryGetValue("User-Agent", out var userAgent))
|
||||||
|
{
|
||||||
|
if (userAgent.ToString().StartsWith("Okta"))
|
||||||
|
{
|
||||||
|
RequestScimProvider = ScimProviderType.Okta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (RequestScimProvider == ScimProviderType.Default &&
|
||||||
|
httpContext.Request.Headers.ContainsKey("Adscimversion"))
|
||||||
|
{
|
||||||
|
RequestScimProvider = ScimProviderType.AzureAd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
bitwarden_license/src/Scim/Controllers/InfoController.cs
Normal file
22
bitwarden_license/src/Scim/Controllers/InfoController.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Controllers;
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class InfoController : Controller
|
||||||
|
{
|
||||||
|
[HttpGet("~/alive")]
|
||||||
|
[HttpGet("~/now")]
|
||||||
|
public DateTime GetAlive()
|
||||||
|
{
|
||||||
|
return DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("~/version")]
|
||||||
|
public JsonResult GetVersion()
|
||||||
|
{
|
||||||
|
return Json(AssemblyHelpers.GetVersion());
|
||||||
|
}
|
||||||
|
}
|
109
bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs
Normal file
109
bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.OrganizationFeatures.Groups.Interfaces;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Scim.Groups.Interfaces;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
using Bit.Scim.Utilities;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Controllers.v2;
|
||||||
|
|
||||||
|
[Authorize("Scim")]
|
||||||
|
[Route("v2/{organizationId}/groups")]
|
||||||
|
[ExceptionHandlerFilter]
|
||||||
|
public class GroupsController : Controller
|
||||||
|
{
|
||||||
|
private readonly IGroupRepository _groupRepository;
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IGetGroupsListQuery _getGroupsListQuery;
|
||||||
|
private readonly IDeleteGroupCommand _deleteGroupCommand;
|
||||||
|
private readonly IPatchGroupCommand _patchGroupCommand;
|
||||||
|
private readonly IPostGroupCommand _postGroupCommand;
|
||||||
|
private readonly IPutGroupCommand _putGroupCommand;
|
||||||
|
private readonly ILogger<GroupsController> _logger;
|
||||||
|
|
||||||
|
public GroupsController(
|
||||||
|
IGroupRepository groupRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IGetGroupsListQuery getGroupsListQuery,
|
||||||
|
IDeleteGroupCommand deleteGroupCommand,
|
||||||
|
IPatchGroupCommand patchGroupCommand,
|
||||||
|
IPostGroupCommand postGroupCommand,
|
||||||
|
IPutGroupCommand putGroupCommand,
|
||||||
|
ILogger<GroupsController> logger)
|
||||||
|
{
|
||||||
|
_groupRepository = groupRepository;
|
||||||
|
_organizationRepository = organizationRepository;
|
||||||
|
_getGroupsListQuery = getGroupsListQuery;
|
||||||
|
_deleteGroupCommand = deleteGroupCommand;
|
||||||
|
_patchGroupCommand = patchGroupCommand;
|
||||||
|
_postGroupCommand = postGroupCommand;
|
||||||
|
_putGroupCommand = putGroupCommand;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<IActionResult> Get(Guid organizationId, Guid id)
|
||||||
|
{
|
||||||
|
var group = await _groupRepository.GetByIdAsync(id);
|
||||||
|
if (group == null || group.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
throw new NotFoundException("Group not found.");
|
||||||
|
}
|
||||||
|
return Ok(new ScimGroupResponseModel(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("")]
|
||||||
|
public async Task<IActionResult> Get(
|
||||||
|
Guid organizationId,
|
||||||
|
[FromQuery] string filter,
|
||||||
|
[FromQuery] int? count,
|
||||||
|
[FromQuery] int? startIndex)
|
||||||
|
{
|
||||||
|
var groupsListQueryResult = await _getGroupsListQuery.GetGroupsListAsync(organizationId, filter, count, startIndex);
|
||||||
|
var scimListResponseModel = new ScimListResponseModel<ScimGroupResponseModel>
|
||||||
|
{
|
||||||
|
Resources = groupsListQueryResult.groupList.Select(g => new ScimGroupResponseModel(g)).ToList(),
|
||||||
|
ItemsPerPage = count.GetValueOrDefault(groupsListQueryResult.groupList.Count()),
|
||||||
|
TotalResults = groupsListQueryResult.totalResults,
|
||||||
|
StartIndex = startIndex.GetValueOrDefault(1),
|
||||||
|
};
|
||||||
|
return Ok(scimListResponseModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("")]
|
||||||
|
public async Task<IActionResult> Post(Guid organizationId, [FromBody] ScimGroupRequestModel model)
|
||||||
|
{
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
var group = await _postGroupCommand.PostGroupAsync(organization, model);
|
||||||
|
var scimGroupResponseModel = new ScimGroupResponseModel(group);
|
||||||
|
return new CreatedResult(Url.Action(nameof(Get), new { group.OrganizationId, group.Id }), scimGroupResponseModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
public async Task<IActionResult> Put(Guid organizationId, Guid id, [FromBody] ScimGroupRequestModel model)
|
||||||
|
{
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
var group = await _putGroupCommand.PutGroupAsync(organization, id, model);
|
||||||
|
var response = new ScimGroupResponseModel(group);
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch("{id}")]
|
||||||
|
public async Task<IActionResult> Patch(Guid organizationId, Guid id, [FromBody] ScimPatchModel model)
|
||||||
|
{
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
await _patchGroupCommand.PatchGroupAsync(organization, id, model);
|
||||||
|
return new NoContentResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<IActionResult> Delete(Guid organizationId, Guid id)
|
||||||
|
{
|
||||||
|
await _deleteGroupCommand.DeleteGroupAsync(organizationId, id, EventSystemUser.SCIM);
|
||||||
|
return new NoContentResult();
|
||||||
|
}
|
||||||
|
}
|
125
bitwarden_license/src/Scim/Controllers/v2/UsersController.cs
Normal file
125
bitwarden_license/src/Scim/Controllers/v2/UsersController.cs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
using Bit.Scim.Users.Interfaces;
|
||||||
|
using Bit.Scim.Utilities;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Controllers.v2;
|
||||||
|
|
||||||
|
[Authorize("Scim")]
|
||||||
|
[Route("v2/{organizationId}/users")]
|
||||||
|
[ExceptionHandlerFilter]
|
||||||
|
public class UsersController : Controller
|
||||||
|
{
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
private readonly IOrganizationService _organizationService;
|
||||||
|
private readonly IGetUsersListQuery _getUsersListQuery;
|
||||||
|
private readonly IDeleteOrganizationUserCommand _deleteOrganizationUserCommand;
|
||||||
|
private readonly IPatchUserCommand _patchUserCommand;
|
||||||
|
private readonly IPostUserCommand _postUserCommand;
|
||||||
|
private readonly ILogger<UsersController> _logger;
|
||||||
|
|
||||||
|
public UsersController(
|
||||||
|
IUserService userService,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationService organizationService,
|
||||||
|
IGetUsersListQuery getUsersListQuery,
|
||||||
|
IDeleteOrganizationUserCommand deleteOrganizationUserCommand,
|
||||||
|
IPatchUserCommand patchUserCommand,
|
||||||
|
IPostUserCommand postUserCommand,
|
||||||
|
ILogger<UsersController> logger)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
_organizationUserRepository = organizationUserRepository;
|
||||||
|
_organizationService = organizationService;
|
||||||
|
_getUsersListQuery = getUsersListQuery;
|
||||||
|
_deleteOrganizationUserCommand = deleteOrganizationUserCommand;
|
||||||
|
_patchUserCommand = patchUserCommand;
|
||||||
|
_postUserCommand = postUserCommand;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<IActionResult> Get(Guid organizationId, Guid id)
|
||||||
|
{
|
||||||
|
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||||
|
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
throw new NotFoundException("User not found.");
|
||||||
|
}
|
||||||
|
return Ok(new ScimUserResponseModel(orgUser));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("")]
|
||||||
|
public async Task<IActionResult> Get(
|
||||||
|
Guid organizationId,
|
||||||
|
[FromQuery] string filter,
|
||||||
|
[FromQuery] int? count,
|
||||||
|
[FromQuery] int? startIndex)
|
||||||
|
{
|
||||||
|
var usersListQueryResult = await _getUsersListQuery.GetUsersListAsync(organizationId, filter, count, startIndex);
|
||||||
|
var scimListResponseModel = new ScimListResponseModel<ScimUserResponseModel>
|
||||||
|
{
|
||||||
|
Resources = usersListQueryResult.userList.Select(u => new ScimUserResponseModel(u)).ToList(),
|
||||||
|
ItemsPerPage = count.GetValueOrDefault(usersListQueryResult.userList.Count()),
|
||||||
|
TotalResults = usersListQueryResult.totalResults,
|
||||||
|
StartIndex = startIndex.GetValueOrDefault(1),
|
||||||
|
};
|
||||||
|
return Ok(scimListResponseModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("")]
|
||||||
|
public async Task<IActionResult> Post(Guid organizationId, [FromBody] ScimUserRequestModel model)
|
||||||
|
{
|
||||||
|
var orgUser = await _postUserCommand.PostUserAsync(organizationId, model);
|
||||||
|
var scimUserResponseModel = new ScimUserResponseModel(orgUser);
|
||||||
|
return new CreatedResult(Url.Action(nameof(Get), new { orgUser.OrganizationId, orgUser.Id }), scimUserResponseModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
public async Task<IActionResult> Put(Guid organizationId, Guid id, [FromBody] ScimUserRequestModel model)
|
||||||
|
{
|
||||||
|
var orgUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||||
|
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||||
|
{
|
||||||
|
Status = 404,
|
||||||
|
Detail = "User not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.Active && orgUser.Status == OrganizationUserStatusType.Revoked)
|
||||||
|
{
|
||||||
|
await _organizationService.RestoreUserAsync(orgUser, null, _userService);
|
||||||
|
}
|
||||||
|
else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Revoked)
|
||||||
|
{
|
||||||
|
await _organizationService.RevokeUserAsync(orgUser, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Have to get full details object for response model
|
||||||
|
var orgUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||||
|
return new ObjectResult(new ScimUserResponseModel(orgUserDetails));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch("{id}")]
|
||||||
|
public async Task<IActionResult> Patch(Guid organizationId, Guid id, [FromBody] ScimPatchModel model)
|
||||||
|
{
|
||||||
|
await _patchUserCommand.PatchUserAsync(organizationId, id, model);
|
||||||
|
return new NoContentResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<IActionResult> Delete(Guid organizationId, Guid id)
|
||||||
|
{
|
||||||
|
await _deleteOrganizationUserCommand.DeleteUserAsync(organizationId, id, EventSystemUser.SCIM);
|
||||||
|
return new NoContentResult();
|
||||||
|
}
|
||||||
|
}
|
20
bitwarden_license/src/Scim/Dockerfile
Normal file
20
bitwarden_license/src/Scim/Dockerfile
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:6.0
|
||||||
|
|
||||||
|
LABEL com.bitwarden.product="bitwarden"
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
gosu \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV ASPNETCORE_URLS http://+:5000
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 5000
|
||||||
|
COPY obj/build-output/publish .
|
||||||
|
COPY entrypoint.sh /
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
64
bitwarden_license/src/Scim/Groups/GetGroupsListQuery.cs
Normal file
64
bitwarden_license/src/Scim/Groups/GetGroupsListQuery.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Scim.Groups.Interfaces;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Groups;
|
||||||
|
|
||||||
|
public class GetGroupsListQuery : IGetGroupsListQuery
|
||||||
|
{
|
||||||
|
private readonly IGroupRepository _groupRepository;
|
||||||
|
|
||||||
|
public GetGroupsListQuery(IGroupRepository groupRepository)
|
||||||
|
{
|
||||||
|
_groupRepository = groupRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(Guid organizationId, string filter, int? count, int? startIndex)
|
||||||
|
{
|
||||||
|
string nameFilter = null;
|
||||||
|
string externalIdFilter = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter))
|
||||||
|
{
|
||||||
|
if (filter.StartsWith("displayName eq "))
|
||||||
|
{
|
||||||
|
nameFilter = filter.Substring(15).Trim('"');
|
||||||
|
}
|
||||||
|
else if (filter.StartsWith("externalId eq "))
|
||||||
|
{
|
||||||
|
externalIdFilter = filter.Substring(14).Trim('"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupList = new List<Group>();
|
||||||
|
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organizationId);
|
||||||
|
var totalResults = 0;
|
||||||
|
if (!string.IsNullOrWhiteSpace(nameFilter))
|
||||||
|
{
|
||||||
|
var group = groups.FirstOrDefault(g => g.Name == nameFilter);
|
||||||
|
if (group != null)
|
||||||
|
{
|
||||||
|
groupList.Add(group);
|
||||||
|
}
|
||||||
|
totalResults = groupList.Count;
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(externalIdFilter))
|
||||||
|
{
|
||||||
|
var group = groups.FirstOrDefault(ou => ou.ExternalId == externalIdFilter);
|
||||||
|
if (group != null)
|
||||||
|
{
|
||||||
|
groupList.Add(group);
|
||||||
|
}
|
||||||
|
totalResults = groupList.Count;
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrWhiteSpace(filter) && startIndex.HasValue && count.HasValue)
|
||||||
|
{
|
||||||
|
groupList = groups.OrderBy(g => g.Name)
|
||||||
|
.Skip(startIndex.Value - 1)
|
||||||
|
.Take(count.Value)
|
||||||
|
.ToList();
|
||||||
|
totalResults = groups.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (groupList, totalResults);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Groups.Interfaces;
|
||||||
|
|
||||||
|
public interface IGetGroupsListQuery
|
||||||
|
{
|
||||||
|
Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(Guid organizationId, string filter, int? count, int? startIndex);
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Groups.Interfaces;
|
||||||
|
|
||||||
|
public interface IPatchGroupCommand
|
||||||
|
{
|
||||||
|
Task PatchGroupAsync(Organization organization, Guid id, ScimPatchModel model);
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Groups.Interfaces;
|
||||||
|
|
||||||
|
public interface IPostGroupCommand
|
||||||
|
{
|
||||||
|
Task<Group> PostGroupAsync(Organization organization, ScimGroupRequestModel model);
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Groups.Interfaces;
|
||||||
|
|
||||||
|
public interface IPutGroupCommand
|
||||||
|
{
|
||||||
|
Task<Group> PutGroupAsync(Organization organization, Guid id, ScimGroupRequestModel model);
|
||||||
|
}
|
153
bitwarden_license/src/Scim/Groups/PatchGroupCommand.cs
Normal file
153
bitwarden_license/src/Scim/Groups/PatchGroupCommand.cs
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.OrganizationFeatures.Groups.Interfaces;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Scim.Groups.Interfaces;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Groups;
|
||||||
|
|
||||||
|
public class PatchGroupCommand : IPatchGroupCommand
|
||||||
|
{
|
||||||
|
private readonly IGroupRepository _groupRepository;
|
||||||
|
private readonly IGroupService _groupService;
|
||||||
|
private readonly IUpdateGroupCommand _updateGroupCommand;
|
||||||
|
private readonly ILogger<PatchGroupCommand> _logger;
|
||||||
|
|
||||||
|
public PatchGroupCommand(
|
||||||
|
IGroupRepository groupRepository,
|
||||||
|
IGroupService groupService,
|
||||||
|
IUpdateGroupCommand updateGroupCommand,
|
||||||
|
ILogger<PatchGroupCommand> logger)
|
||||||
|
{
|
||||||
|
_groupRepository = groupRepository;
|
||||||
|
_groupService = groupService;
|
||||||
|
_updateGroupCommand = updateGroupCommand;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PatchGroupAsync(Organization organization, Guid id, ScimPatchModel model)
|
||||||
|
{
|
||||||
|
var group = await _groupRepository.GetByIdAsync(id);
|
||||||
|
if (group == null || group.OrganizationId != organization.Id)
|
||||||
|
{
|
||||||
|
throw new NotFoundException("Group not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var operationHandled = false;
|
||||||
|
foreach (var operation in model.Operations)
|
||||||
|
{
|
||||||
|
// Replace operations
|
||||||
|
if (operation.Op?.ToLowerInvariant() == "replace")
|
||||||
|
{
|
||||||
|
// Replace a list of members
|
||||||
|
if (operation.Path?.ToLowerInvariant() == "members")
|
||||||
|
{
|
||||||
|
var ids = GetOperationValueIds(operation.Value);
|
||||||
|
await _groupRepository.UpdateUsersAsync(group.Id, ids);
|
||||||
|
operationHandled = true;
|
||||||
|
}
|
||||||
|
// Replace group name from path
|
||||||
|
else if (operation.Path?.ToLowerInvariant() == "displayname")
|
||||||
|
{
|
||||||
|
group.Name = operation.Value.GetString();
|
||||||
|
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
||||||
|
operationHandled = true;
|
||||||
|
}
|
||||||
|
// Replace group name from value object
|
||||||
|
else if (string.IsNullOrWhiteSpace(operation.Path) &&
|
||||||
|
operation.Value.TryGetProperty("displayName", out var displayNameProperty))
|
||||||
|
{
|
||||||
|
group.Name = displayNameProperty.GetString();
|
||||||
|
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
||||||
|
operationHandled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add a single member
|
||||||
|
else if (operation.Op?.ToLowerInvariant() == "add" &&
|
||||||
|
!string.IsNullOrWhiteSpace(operation.Path) &&
|
||||||
|
operation.Path.ToLowerInvariant().StartsWith("members[value eq "))
|
||||||
|
{
|
||||||
|
var addId = GetOperationPathId(operation.Path);
|
||||||
|
if (addId.HasValue)
|
||||||
|
{
|
||||||
|
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
||||||
|
orgUserIds.Add(addId.Value);
|
||||||
|
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
||||||
|
operationHandled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add a list of members
|
||||||
|
else if (operation.Op?.ToLowerInvariant() == "add" &&
|
||||||
|
operation.Path?.ToLowerInvariant() == "members")
|
||||||
|
{
|
||||||
|
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
||||||
|
foreach (var v in GetOperationValueIds(operation.Value))
|
||||||
|
{
|
||||||
|
orgUserIds.Add(v);
|
||||||
|
}
|
||||||
|
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
||||||
|
operationHandled = true;
|
||||||
|
}
|
||||||
|
// Remove a single member
|
||||||
|
else if (operation.Op?.ToLowerInvariant() == "remove" &&
|
||||||
|
!string.IsNullOrWhiteSpace(operation.Path) &&
|
||||||
|
operation.Path.ToLowerInvariant().StartsWith("members[value eq "))
|
||||||
|
{
|
||||||
|
var removeId = GetOperationPathId(operation.Path);
|
||||||
|
if (removeId.HasValue)
|
||||||
|
{
|
||||||
|
await _groupService.DeleteUserAsync(group, removeId.Value, EventSystemUser.SCIM);
|
||||||
|
operationHandled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove a list of members
|
||||||
|
else if (operation.Op?.ToLowerInvariant() == "remove" &&
|
||||||
|
operation.Path?.ToLowerInvariant() == "members")
|
||||||
|
{
|
||||||
|
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
||||||
|
foreach (var v in GetOperationValueIds(operation.Value))
|
||||||
|
{
|
||||||
|
orgUserIds.Remove(v);
|
||||||
|
}
|
||||||
|
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
||||||
|
operationHandled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!operationHandled)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Group patch operation not handled: {0} : ",
|
||||||
|
string.Join(", ", model.Operations.Select(o => $"{o.Op}:{o.Path}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Guid> GetOperationValueIds(JsonElement objArray)
|
||||||
|
{
|
||||||
|
var ids = new List<Guid>();
|
||||||
|
foreach (var obj in objArray.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (obj.TryGetProperty("value", out var valueProperty))
|
||||||
|
{
|
||||||
|
if (valueProperty.TryGetGuid(out var guid))
|
||||||
|
{
|
||||||
|
ids.Add(guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Guid? GetOperationPathId(string path)
|
||||||
|
{
|
||||||
|
// Parse Guid from string like: members[value eq "{GUID}"}]
|
||||||
|
if (Guid.TryParse(path.Substring(18).Replace("\"]", string.Empty), out var id))
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
77
bitwarden_license/src/Scim/Groups/PostGroupCommand.cs
Normal file
77
bitwarden_license/src/Scim/Groups/PostGroupCommand.cs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.OrganizationFeatures.Groups.Interfaces;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Scim.Context;
|
||||||
|
using Bit.Scim.Groups.Interfaces;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Groups;
|
||||||
|
|
||||||
|
public class PostGroupCommand : IPostGroupCommand
|
||||||
|
{
|
||||||
|
private readonly IGroupRepository _groupRepository;
|
||||||
|
private readonly IScimContext _scimContext;
|
||||||
|
private readonly ICreateGroupCommand _createGroupCommand;
|
||||||
|
|
||||||
|
public PostGroupCommand(
|
||||||
|
IGroupRepository groupRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IScimContext scimContext,
|
||||||
|
ICreateGroupCommand createGroupCommand)
|
||||||
|
{
|
||||||
|
_groupRepository = groupRepository;
|
||||||
|
_scimContext = scimContext;
|
||||||
|
_createGroupCommand = createGroupCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Group> PostGroupAsync(Organization organization, ScimGroupRequestModel model)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(model.DisplayName))
|
||||||
|
{
|
||||||
|
throw new BadRequestException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);
|
||||||
|
if (!string.IsNullOrWhiteSpace(model.ExternalId) && groups.Any(g => g.ExternalId == model.ExternalId))
|
||||||
|
{
|
||||||
|
throw new ConflictException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var group = model.ToGroup(organization.Id);
|
||||||
|
await _createGroupCommand.CreateGroupAsync(group, organization, EventSystemUser.SCIM, collections: null);
|
||||||
|
await UpdateGroupMembersAsync(group, model);
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model)
|
||||||
|
{
|
||||||
|
if (_scimContext.RequestScimProvider != Core.Enums.ScimProviderType.Okta)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.Members == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var memberIds = new List<Guid>();
|
||||||
|
foreach (var id in model.Members.Select(i => i.Value))
|
||||||
|
{
|
||||||
|
if (Guid.TryParse(id, out var guidId))
|
||||||
|
{
|
||||||
|
memberIds.Add(guidId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!memberIds.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _groupRepository.UpdateUsersAsync(group.Id, memberIds);
|
||||||
|
}
|
||||||
|
}
|
66
bitwarden_license/src/Scim/Groups/PutGroupCommand.cs
Normal file
66
bitwarden_license/src/Scim/Groups/PutGroupCommand.cs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.OrganizationFeatures.Groups.Interfaces;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Scim.Context;
|
||||||
|
using Bit.Scim.Groups.Interfaces;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Groups;
|
||||||
|
|
||||||
|
public class PutGroupCommand : IPutGroupCommand
|
||||||
|
{
|
||||||
|
private readonly IGroupRepository _groupRepository;
|
||||||
|
private readonly IScimContext _scimContext;
|
||||||
|
private readonly IUpdateGroupCommand _updateGroupCommand;
|
||||||
|
|
||||||
|
public PutGroupCommand(
|
||||||
|
IGroupRepository groupRepository,
|
||||||
|
IScimContext scimContext,
|
||||||
|
IUpdateGroupCommand updateGroupCommand)
|
||||||
|
{
|
||||||
|
_groupRepository = groupRepository;
|
||||||
|
_scimContext = scimContext;
|
||||||
|
_updateGroupCommand = updateGroupCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Group> PutGroupAsync(Organization organization, Guid id, ScimGroupRequestModel model)
|
||||||
|
{
|
||||||
|
var group = await _groupRepository.GetByIdAsync(id);
|
||||||
|
if (group == null || group.OrganizationId != organization.Id)
|
||||||
|
{
|
||||||
|
throw new NotFoundException("Group not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
group.Name = model.DisplayName;
|
||||||
|
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
||||||
|
await UpdateGroupMembersAsync(group, model);
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model)
|
||||||
|
{
|
||||||
|
if (_scimContext.RequestScimProvider != Core.Enums.ScimProviderType.Okta)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.Members == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var memberIds = new List<Guid>();
|
||||||
|
foreach (var id in model.Members.Select(i => i.Value))
|
||||||
|
{
|
||||||
|
if (Guid.TryParse(id, out var guidId))
|
||||||
|
{
|
||||||
|
memberIds.Add(guidId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _groupRepository.UpdateUsersAsync(group.Id, memberIds);
|
||||||
|
}
|
||||||
|
}
|
17
bitwarden_license/src/Scim/Models/BaseScimGroupModel.cs
Normal file
17
bitwarden_license/src/Scim/Models/BaseScimGroupModel.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using Bit.Scim.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
public abstract class BaseScimGroupModel : BaseScimModel
|
||||||
|
{
|
||||||
|
public BaseScimGroupModel(bool initSchema = false)
|
||||||
|
{
|
||||||
|
if (initSchema)
|
||||||
|
{
|
||||||
|
Schemas = new List<string> { ScimConstants.Scim2SchemaGroup };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
public string ExternalId { get; set; }
|
||||||
|
}
|
14
bitwarden_license/src/Scim/Models/BaseScimModel.cs
Normal file
14
bitwarden_license/src/Scim/Models/BaseScimModel.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
public abstract class BaseScimModel
|
||||||
|
{
|
||||||
|
public BaseScimModel()
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public BaseScimModel(string schema)
|
||||||
|
{
|
||||||
|
Schemas = new List<string> { schema };
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> Schemas { get; set; }
|
||||||
|
}
|
55
bitwarden_license/src/Scim/Models/BaseScimUserModel.cs
Normal file
55
bitwarden_license/src/Scim/Models/BaseScimUserModel.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
using Bit.Scim.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
public abstract class BaseScimUserModel : BaseScimModel
|
||||||
|
{
|
||||||
|
public BaseScimUserModel(bool initSchema = false)
|
||||||
|
{
|
||||||
|
if (initSchema)
|
||||||
|
{
|
||||||
|
Schemas = new List<string> { ScimConstants.Scim2SchemaUser };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public NameModel Name { get; set; }
|
||||||
|
public List<EmailModel> Emails { get; set; }
|
||||||
|
public string PrimaryEmail => Emails?.FirstOrDefault(e => e.Primary)?.Value;
|
||||||
|
public string WorkEmail => Emails?.FirstOrDefault(e => e.Type == "work")?.Value;
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
public bool Active { get; set; }
|
||||||
|
public List<string> Groups { get; set; }
|
||||||
|
public string ExternalId { get; set; }
|
||||||
|
|
||||||
|
public class NameModel
|
||||||
|
{
|
||||||
|
public NameModel() { }
|
||||||
|
|
||||||
|
public NameModel(string name)
|
||||||
|
{
|
||||||
|
Formatted = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Formatted { get; set; }
|
||||||
|
public string GivenName { get; set; }
|
||||||
|
public string MiddleName { get; set; }
|
||||||
|
public string FamilyName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EmailModel
|
||||||
|
{
|
||||||
|
public EmailModel() { }
|
||||||
|
|
||||||
|
public EmailModel(string email)
|
||||||
|
{
|
||||||
|
Primary = true;
|
||||||
|
Value = email;
|
||||||
|
Type = "work";
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Primary { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
}
|
||||||
|
}
|
13
bitwarden_license/src/Scim/Models/ScimErrorResponseModel.cs
Normal file
13
bitwarden_license/src/Scim/Models/ScimErrorResponseModel.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Bit.Scim.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
public class ScimErrorResponseModel : BaseScimModel
|
||||||
|
{
|
||||||
|
public ScimErrorResponseModel()
|
||||||
|
: base(ScimConstants.Scim2SchemaError)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public string Detail { get; set; }
|
||||||
|
public int Status { get; set; }
|
||||||
|
}
|
30
bitwarden_license/src/Scim/Models/ScimGroupRequestModel.cs
Normal file
30
bitwarden_license/src/Scim/Models/ScimGroupRequestModel.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
public class ScimGroupRequestModel : BaseScimGroupModel
|
||||||
|
{
|
||||||
|
public ScimGroupRequestModel()
|
||||||
|
: base(false)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public Group ToGroup(Guid organizationId)
|
||||||
|
{
|
||||||
|
var externalId = string.IsNullOrWhiteSpace(ExternalId) ? CoreHelpers.RandomString(15) : ExternalId;
|
||||||
|
return new Group
|
||||||
|
{
|
||||||
|
Name = DisplayName,
|
||||||
|
ExternalId = externalId,
|
||||||
|
OrganizationId = organizationId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<GroupMembersModel> Members { get; set; }
|
||||||
|
|
||||||
|
public class GroupMembersModel
|
||||||
|
{
|
||||||
|
public string Value { get; set; }
|
||||||
|
public string Display { get; set; }
|
||||||
|
}
|
||||||
|
}
|
25
bitwarden_license/src/Scim/Models/ScimGroupResponseModel.cs
Normal file
25
bitwarden_license/src/Scim/Models/ScimGroupResponseModel.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
public class ScimGroupResponseModel : BaseScimGroupModel
|
||||||
|
{
|
||||||
|
public ScimGroupResponseModel()
|
||||||
|
: base(true)
|
||||||
|
{
|
||||||
|
Meta = new ScimMetaModel("Group");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScimGroupResponseModel(Group group)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
Id = group.Id.ToString();
|
||||||
|
DisplayName = group.Name;
|
||||||
|
ExternalId = group.ExternalId;
|
||||||
|
Meta.Created = group.CreationDate;
|
||||||
|
Meta.LastModified = group.RevisionDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Id { get; set; }
|
||||||
|
public ScimMetaModel Meta { get; private set; }
|
||||||
|
}
|
15
bitwarden_license/src/Scim/Models/ScimListResponseModel.cs
Normal file
15
bitwarden_license/src/Scim/Models/ScimListResponseModel.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using Bit.Scim.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
public class ScimListResponseModel<T> : BaseScimModel
|
||||||
|
{
|
||||||
|
public ScimListResponseModel()
|
||||||
|
: base(ScimConstants.Scim2SchemaListResponse)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public int TotalResults { get; set; }
|
||||||
|
public int StartIndex { get; set; }
|
||||||
|
public int ItemsPerPage { get; set; }
|
||||||
|
public List<T> Resources { get; set; }
|
||||||
|
}
|
13
bitwarden_license/src/Scim/Models/ScimMetaModel.cs
Normal file
13
bitwarden_license/src/Scim/Models/ScimMetaModel.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
public class ScimMetaModel
|
||||||
|
{
|
||||||
|
public ScimMetaModel(string resourceType)
|
||||||
|
{
|
||||||
|
ResourceType = resourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ResourceType { get; set; }
|
||||||
|
public DateTime? Created { get; set; }
|
||||||
|
public DateTime? LastModified { get; set; }
|
||||||
|
}
|
18
bitwarden_license/src/Scim/Models/ScimPatchModel.cs
Normal file
18
bitwarden_license/src/Scim/Models/ScimPatchModel.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
public class ScimPatchModel : BaseScimModel
|
||||||
|
{
|
||||||
|
public ScimPatchModel()
|
||||||
|
: base() { }
|
||||||
|
|
||||||
|
public List<OperationModel> Operations { get; set; }
|
||||||
|
|
||||||
|
public class OperationModel
|
||||||
|
{
|
||||||
|
public string Op { get; set; }
|
||||||
|
public string Path { get; set; }
|
||||||
|
public JsonElement Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
public class ScimUserRequestModel : BaseScimUserModel
|
||||||
|
{
|
||||||
|
public ScimUserRequestModel()
|
||||||
|
: base(false)
|
||||||
|
{ }
|
||||||
|
}
|
28
bitwarden_license/src/Scim/Models/ScimUserResponseModel.cs
Normal file
28
bitwarden_license/src/Scim/Models/ScimUserResponseModel.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Models;
|
||||||
|
|
||||||
|
public class ScimUserResponseModel : BaseScimUserModel
|
||||||
|
{
|
||||||
|
public ScimUserResponseModel()
|
||||||
|
: base(true)
|
||||||
|
{
|
||||||
|
Meta = new ScimMetaModel("User");
|
||||||
|
Groups = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScimUserResponseModel(OrganizationUserUserDetails orgUser)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
Id = orgUser.Id.ToString();
|
||||||
|
ExternalId = orgUser.ExternalId;
|
||||||
|
UserName = orgUser.Email;
|
||||||
|
DisplayName = orgUser.Name;
|
||||||
|
Emails = new List<EmailModel> { new EmailModel(orgUser.Email) };
|
||||||
|
Name = new NameModel(orgUser.Name);
|
||||||
|
Active = orgUser.Status != Core.Enums.OrganizationUserStatusType.Revoked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Id { get; set; }
|
||||||
|
public ScimMetaModel Meta { get; private set; }
|
||||||
|
}
|
32
bitwarden_license/src/Scim/Program.cs
Normal file
32
bitwarden_license/src/Scim/Program.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Scim;
|
||||||
|
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Host
|
||||||
|
.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
|
{
|
||||||
|
webBuilder.UseStartup<Startup>();
|
||||||
|
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||||
|
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||||
|
{
|
||||||
|
var context = e.Properties["SourceContext"].ToString();
|
||||||
|
|
||||||
|
if (e.Properties.ContainsKey("RequestPath") &&
|
||||||
|
!string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) &&
|
||||||
|
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Level >= globalSettings.MinLogLevel.ScimSettings.Default;
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.Build()
|
||||||
|
.Run();
|
||||||
|
}
|
||||||
|
}
|
29
bitwarden_license/src/Scim/Properties/launchSettings.json
Normal file
29
bitwarden_license/src/Scim/Properties/launchSettings.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:44558/",
|
||||||
|
"sslPort": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": false,
|
||||||
|
"launchUrl": "http://localhost:44558",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Scim": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": false,
|
||||||
|
"launchUrl": "http://localhost:44558",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"applicationUrl": "http://localhost:44559"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
bitwarden_license/src/Scim/Scim.csproj
Normal file
17
bitwarden_license/src/Scim/Scim.csproj
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<UserSecretsId>bitwarden-Scim</UserSecretsId>
|
||||||
|
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="5.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\src\Core\Core.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\src\SharedWeb\SharedWeb.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
5
bitwarden_license/src/Scim/ScimSettings.cs
Normal file
5
bitwarden_license/src/Scim/ScimSettings.cs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
namespace Bit.Scim;
|
||||||
|
|
||||||
|
public class ScimSettings
|
||||||
|
{
|
||||||
|
}
|
120
bitwarden_license/src/Scim/Startup.cs
Normal file
120
bitwarden_license/src/Scim/Startup.cs
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Scim.Context;
|
||||||
|
using Bit.Scim.Utilities;
|
||||||
|
using Bit.SharedWeb.Utilities;
|
||||||
|
using IdentityModel;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
|
namespace Bit.Scim;
|
||||||
|
|
||||||
|
public class Startup
|
||||||
|
{
|
||||||
|
public Startup(IWebHostEnvironment env, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
|
||||||
|
Configuration = configuration;
|
||||||
|
Environment = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConfiguration Configuration { get; }
|
||||||
|
public IWebHostEnvironment Environment { get; set; }
|
||||||
|
|
||||||
|
public void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
// Options
|
||||||
|
services.AddOptions();
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);
|
||||||
|
services.Configure<ScimSettings>(Configuration.GetSection("ScimSettings"));
|
||||||
|
|
||||||
|
// Data Protection
|
||||||
|
services.AddCustomDataProtectionServices(Environment, globalSettings);
|
||||||
|
|
||||||
|
// Stripe Billing
|
||||||
|
StripeConfiguration.ApiKey = globalSettings.Stripe.ApiKey;
|
||||||
|
StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries;
|
||||||
|
|
||||||
|
// Repositories
|
||||||
|
services.AddDatabaseRepositories(globalSettings);
|
||||||
|
|
||||||
|
// Context
|
||||||
|
services.AddScoped<ICurrentContext, CurrentContext>();
|
||||||
|
services.AddScoped<IScimContext, ScimContext>();
|
||||||
|
|
||||||
|
// Authentication
|
||||||
|
services.AddAuthentication(ApiKeyAuthenticationOptions.DefaultScheme)
|
||||||
|
.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(
|
||||||
|
ApiKeyAuthenticationOptions.DefaultScheme, null);
|
||||||
|
|
||||||
|
services.AddAuthorization(config =>
|
||||||
|
{
|
||||||
|
config.AddPolicy("Scim", policy =>
|
||||||
|
{
|
||||||
|
policy.RequireAuthenticatedUser();
|
||||||
|
policy.RequireClaim(JwtClaimTypes.Scope, "api.scim");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Identity
|
||||||
|
services.AddCustomIdentityServices(globalSettings);
|
||||||
|
|
||||||
|
// Services
|
||||||
|
services.AddBaseServices(globalSettings);
|
||||||
|
services.AddDefaultServices(globalSettings);
|
||||||
|
|
||||||
|
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
|
|
||||||
|
// Mvc
|
||||||
|
services.AddMvc(config =>
|
||||||
|
{
|
||||||
|
config.Filters.Add(new LoggingExceptionHandlerFilterAttribute());
|
||||||
|
});
|
||||||
|
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
|
||||||
|
|
||||||
|
services.AddScimGroupCommands();
|
||||||
|
services.AddScimGroupQueries();
|
||||||
|
services.AddScimUserQueries();
|
||||||
|
services.AddScimUserCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(
|
||||||
|
IApplicationBuilder app,
|
||||||
|
IWebHostEnvironment env,
|
||||||
|
IHostApplicationLifetime appLifetime,
|
||||||
|
GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
app.UseSerilog(env, appLifetime, globalSettings);
|
||||||
|
|
||||||
|
// Add general security headers
|
||||||
|
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||||
|
|
||||||
|
if (env.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseDeveloperExceptionPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default Middleware
|
||||||
|
app.UseDefaultMiddleware(env, globalSettings);
|
||||||
|
|
||||||
|
// Add routing
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
// Add Scim context
|
||||||
|
app.UseMiddleware<ScimContextMiddleware>();
|
||||||
|
|
||||||
|
// Add authentication and authorization to the request pipeline.
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
// Add current context
|
||||||
|
app.UseMiddleware<CurrentContextMiddleware>();
|
||||||
|
|
||||||
|
// Add MVC to the request pipeline.
|
||||||
|
app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
|
||||||
|
}
|
||||||
|
}
|
69
bitwarden_license/src/Scim/Users/GetUsersListQuery.cs
Normal file
69
bitwarden_license/src/Scim/Users/GetUsersListQuery.cs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Scim.Users.Interfaces;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Users;
|
||||||
|
|
||||||
|
public class GetUsersListQuery : IGetUsersListQuery
|
||||||
|
{
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
|
||||||
|
public GetUsersListQuery(IOrganizationUserRepository organizationUserRepository)
|
||||||
|
{
|
||||||
|
_organizationUserRepository = organizationUserRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(IEnumerable<OrganizationUserUserDetails> userList, int totalResults)> GetUsersListAsync(Guid organizationId, string filter, int? count, int? startIndex)
|
||||||
|
{
|
||||||
|
string emailFilter = null;
|
||||||
|
string usernameFilter = null;
|
||||||
|
string externalIdFilter = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter))
|
||||||
|
{
|
||||||
|
if (filter.StartsWith("userName eq "))
|
||||||
|
{
|
||||||
|
usernameFilter = filter.Substring(12).Trim('"').ToLowerInvariant();
|
||||||
|
if (usernameFilter.Contains("@"))
|
||||||
|
{
|
||||||
|
emailFilter = usernameFilter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (filter.StartsWith("externalId eq "))
|
||||||
|
{
|
||||||
|
externalIdFilter = filter.Substring(14).Trim('"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var userList = new List<OrganizationUserUserDetails>();
|
||||||
|
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
||||||
|
var totalResults = 0;
|
||||||
|
if (!string.IsNullOrWhiteSpace(emailFilter))
|
||||||
|
{
|
||||||
|
var orgUser = orgUsers.FirstOrDefault(ou => ou.Email.ToLowerInvariant() == emailFilter);
|
||||||
|
if (orgUser != null)
|
||||||
|
{
|
||||||
|
userList.Add(orgUser);
|
||||||
|
}
|
||||||
|
totalResults = userList.Count;
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(externalIdFilter))
|
||||||
|
{
|
||||||
|
var orgUser = orgUsers.FirstOrDefault(ou => ou.ExternalId == externalIdFilter);
|
||||||
|
if (orgUser != null)
|
||||||
|
{
|
||||||
|
userList.Add(orgUser);
|
||||||
|
}
|
||||||
|
totalResults = userList.Count;
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrWhiteSpace(filter) && startIndex.HasValue && count.HasValue)
|
||||||
|
{
|
||||||
|
userList = orgUsers.OrderBy(ou => ou.Email)
|
||||||
|
.Skip(startIndex.Value - 1)
|
||||||
|
.Take(count.Value)
|
||||||
|
.ToList();
|
||||||
|
totalResults = orgUsers.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (userList, totalResults);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Users.Interfaces;
|
||||||
|
|
||||||
|
public interface IGetUsersListQuery
|
||||||
|
{
|
||||||
|
Task<(IEnumerable<OrganizationUserUserDetails> userList, int totalResults)> GetUsersListAsync(Guid organizationId, string filter, int? count, int? startIndex);
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
using Bit.Scim.Models;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Users.Interfaces;
|
||||||
|
|
||||||
|
public interface IPatchUserCommand
|
||||||
|
{
|
||||||
|
Task PatchUserAsync(Guid organizationId, Guid id, ScimPatchModel model);
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Users.Interfaces;
|
||||||
|
|
||||||
|
public interface IPostUserCommand
|
||||||
|
{
|
||||||
|
Task<OrganizationUserUserDetails> PostUserAsync(Guid organizationId, ScimUserRequestModel model);
|
||||||
|
}
|
87
bitwarden_license/src/Scim/Users/PatchUserCommand.cs
Normal file
87
bitwarden_license/src/Scim/Users/PatchUserCommand.cs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
using Bit.Scim.Users.Interfaces;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Users;
|
||||||
|
|
||||||
|
public class PatchUserCommand : IPatchUserCommand
|
||||||
|
{
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
private readonly IOrganizationService _organizationService;
|
||||||
|
private readonly ILogger<PatchUserCommand> _logger;
|
||||||
|
|
||||||
|
public PatchUserCommand(
|
||||||
|
IUserService userService,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationService organizationService,
|
||||||
|
ILogger<PatchUserCommand> logger)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
_organizationUserRepository = organizationUserRepository;
|
||||||
|
_organizationService = organizationService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PatchUserAsync(Guid organizationId, Guid id, ScimPatchModel model)
|
||||||
|
{
|
||||||
|
var orgUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||||
|
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
throw new NotFoundException("User not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var operationHandled = false;
|
||||||
|
foreach (var operation in model.Operations)
|
||||||
|
{
|
||||||
|
// Replace operations
|
||||||
|
if (operation.Op?.ToLowerInvariant() == "replace")
|
||||||
|
{
|
||||||
|
// Active from path
|
||||||
|
if (operation.Path?.ToLowerInvariant() == "active")
|
||||||
|
{
|
||||||
|
var active = operation.Value.ToString()?.ToLowerInvariant();
|
||||||
|
var handled = await HandleActiveOperationAsync(orgUser, active == "true");
|
||||||
|
if (!operationHandled)
|
||||||
|
{
|
||||||
|
operationHandled = handled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Active from value object
|
||||||
|
else if (string.IsNullOrWhiteSpace(operation.Path) &&
|
||||||
|
operation.Value.TryGetProperty("active", out var activeProperty))
|
||||||
|
{
|
||||||
|
var handled = await HandleActiveOperationAsync(orgUser, activeProperty.GetBoolean());
|
||||||
|
if (!operationHandled)
|
||||||
|
{
|
||||||
|
operationHandled = handled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!operationHandled)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("User patch operation not handled: {operation} : ",
|
||||||
|
string.Join(", ", model.Operations.Select(o => $"{o.Op}:{o.Path}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> HandleActiveOperationAsync(Core.Entities.OrganizationUser orgUser, bool active)
|
||||||
|
{
|
||||||
|
if (active && orgUser.Status == OrganizationUserStatusType.Revoked)
|
||||||
|
{
|
||||||
|
await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM, _userService);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked)
|
||||||
|
{
|
||||||
|
await _organizationService.RevokeUserAsync(orgUser, EventSystemUser.SCIM);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
88
bitwarden_license/src/Scim/Users/PostUserCommand.cs
Normal file
88
bitwarden_license/src/Scim/Users/PostUserCommand.cs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Scim.Context;
|
||||||
|
using Bit.Scim.Models;
|
||||||
|
using Bit.Scim.Users.Interfaces;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Users;
|
||||||
|
|
||||||
|
public class PostUserCommand : IPostUserCommand
|
||||||
|
{
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
private readonly IOrganizationService _organizationService;
|
||||||
|
private readonly IScimContext _scimContext;
|
||||||
|
|
||||||
|
public PostUserCommand(
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationService organizationService,
|
||||||
|
IScimContext scimContext)
|
||||||
|
{
|
||||||
|
_organizationUserRepository = organizationUserRepository;
|
||||||
|
_organizationService = organizationService;
|
||||||
|
_scimContext = scimContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OrganizationUserUserDetails> PostUserAsync(Guid organizationId, ScimUserRequestModel model)
|
||||||
|
{
|
||||||
|
var email = model.PrimaryEmail?.ToLowerInvariant();
|
||||||
|
if (string.IsNullOrWhiteSpace(email))
|
||||||
|
{
|
||||||
|
switch (_scimContext.RequestScimProvider)
|
||||||
|
{
|
||||||
|
case ScimProviderType.AzureAd:
|
||||||
|
email = model.UserName?.ToLowerInvariant();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
email = model.WorkEmail?.ToLowerInvariant();
|
||||||
|
if (string.IsNullOrWhiteSpace(email))
|
||||||
|
{
|
||||||
|
email = model.Emails?.FirstOrDefault()?.Value?.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(email) || !model.Active)
|
||||||
|
{
|
||||||
|
throw new BadRequestException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
||||||
|
var orgUserByEmail = orgUsers.FirstOrDefault(ou => ou.Email?.ToLowerInvariant() == email);
|
||||||
|
if (orgUserByEmail != null)
|
||||||
|
{
|
||||||
|
throw new ConflictException();
|
||||||
|
}
|
||||||
|
|
||||||
|
string externalId = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(model.ExternalId))
|
||||||
|
{
|
||||||
|
externalId = model.ExternalId;
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(model.UserName))
|
||||||
|
{
|
||||||
|
externalId = model.UserName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
externalId = CoreHelpers.RandomString(15);
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgUserByExternalId = orgUsers.FirstOrDefault(ou => ou.ExternalId == externalId);
|
||||||
|
if (orgUserByExternalId != null)
|
||||||
|
{
|
||||||
|
throw new ConflictException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var invitedOrgUser = await _organizationService.InviteUserAsync(organizationId, EventSystemUser.SCIM, email,
|
||||||
|
OrganizationUserType.User, false, externalId, new List<CollectionAccessSelection>(), new List<Guid>());
|
||||||
|
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(invitedOrgUser.Id);
|
||||||
|
|
||||||
|
return orgUser;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Scim.Context;
|
||||||
|
using IdentityModel;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Bit.Scim.Utilities;
|
||||||
|
|
||||||
|
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
|
||||||
|
{
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||||
|
private readonly IScimContext _scimContext;
|
||||||
|
|
||||||
|
public ApiKeyAuthenticationHandler(
|
||||||
|
IOptionsMonitor<ApiKeyAuthenticationOptions> options,
|
||||||
|
ILoggerFactory logger,
|
||||||
|
UrlEncoder encoder,
|
||||||
|
ISystemClock clock,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||||
|
IScimContext scimContext) :
|
||||||
|
base(options, logger, encoder, clock)
|
||||||
|
{
|
||||||
|
_organizationRepository = organizationRepository;
|
||||||
|
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||||
|
_scimContext = scimContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
|
{
|
||||||
|
var endpoint = Context.GetEndpoint();
|
||||||
|
if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
|
||||||
|
{
|
||||||
|
return AuthenticateResult.NoResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_scimContext.OrganizationId.HasValue || _scimContext.Organization == null)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("No organization.");
|
||||||
|
return AuthenticateResult.Fail("Invalid parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Request.Headers.TryGetValue("Authorization", out var authHeader) || authHeader.Count != 1)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("An API request was received without the Authorization header");
|
||||||
|
return AuthenticateResult.Fail("Invalid parameters");
|
||||||
|
}
|
||||||
|
var apiKey = authHeader.ToString();
|
||||||
|
if (apiKey.StartsWith("Bearer "))
|
||||||
|
{
|
||||||
|
apiKey = apiKey.Substring(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_scimContext.Organization.Enabled || !_scimContext.Organization.UseScim ||
|
||||||
|
_scimContext.ScimConfiguration == null || !_scimContext.ScimConfiguration.Enabled)
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Org {organizationId} not able to use Scim.", _scimContext.OrganizationId);
|
||||||
|
return AuthenticateResult.Fail("Invalid parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgApiKey = (await _organizationApiKeyRepository
|
||||||
|
.GetManyByOrganizationIdTypeAsync(_scimContext.Organization.Id, OrganizationApiKeyType.Scim))
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (orgApiKey?.ApiKey != apiKey)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("An API request was received with an invalid API key: {apiKey}", apiKey);
|
||||||
|
return AuthenticateResult.Fail("Invalid parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogInformation("Org {organizationId} authenticated", _scimContext.OrganizationId);
|
||||||
|
|
||||||
|
var claims = new[]
|
||||||
|
{
|
||||||
|
new Claim(JwtClaimTypes.ClientId, $"organization.{_scimContext.OrganizationId.Value}"),
|
||||||
|
new Claim("client_sub", _scimContext.OrganizationId.Value.ToString()),
|
||||||
|
new Claim(JwtClaimTypes.Scope, "api.scim"),
|
||||||
|
};
|
||||||
|
var identity = new ClaimsIdentity(claims, nameof(ApiKeyAuthenticationHandler));
|
||||||
|
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity),
|
||||||
|
ApiKeyAuthenticationOptions.DefaultScheme);
|
||||||
|
|
||||||
|
return AuthenticateResult.Success(ticket);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user