mirror of
https://github.com/bitwarden/server.git
synced 2025-12-10 00:42:07 -06:00
Compare commits
460 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d5d7e58a6 | ||
|
|
dd74e966e5 | ||
|
|
579d8004ff | ||
|
|
3e12cfc6df | ||
|
|
d1ae1fffd6 | ||
|
|
d26b5fa029 | ||
|
|
2e0a4161be | ||
|
|
b5f7f9f6a0 | ||
|
|
acc2529353 | ||
|
|
014376b545 | ||
|
|
bd75c71d10 | ||
|
|
d687e8a84b | ||
|
|
01da3c91a7 | ||
|
|
b1390c9dfe | ||
|
|
f8ec5fa0b2 | ||
|
|
2504fd9de4 | ||
|
|
3ff59021ae | ||
|
|
b18506a0c1 | ||
|
|
813fad8021 | ||
|
|
2f893768f5 | ||
|
|
d5f39eac91 | ||
|
|
5469d8be0e | ||
|
|
18a8829476 | ||
|
|
80ee31b4fe | ||
|
|
3605b4d2ff | ||
|
|
101ff9d6ed | ||
|
|
d88fff4262 | ||
|
|
d619a49998 | ||
|
|
655054aa56 | ||
|
|
b0f6b22b3d | ||
|
|
ed7a234eeb | ||
|
|
98212a7f49 | ||
|
|
ded1c58c27 | ||
|
|
1566a6d587 | ||
|
|
28e9c24f33 | ||
|
|
ee26a701e9 | ||
|
|
89a2eab32a | ||
|
|
de5a81bdc4 | ||
|
|
5b8b394982 | ||
|
|
71be3865ea | ||
|
|
b3573c15fd | ||
|
|
63855cbb5a | ||
|
|
aa3172e24f | ||
|
|
20efb5eb5e | ||
|
|
02568c8e7c | ||
|
|
267759db45 | ||
|
|
599fbc0efd | ||
|
|
c3301ce475 | ||
|
|
a5ea603817 | ||
|
|
62cbe36ce1 | ||
|
|
5af060fbf5 | ||
|
|
3df68ea36f | ||
|
|
8a67aafbe5 | ||
|
|
f151abee54 | ||
|
|
480c20a480 | ||
|
|
7cbc50de98 | ||
|
|
a17f94e150 | ||
|
|
6a5430ff0a | ||
|
|
3133dc91ae | ||
|
|
3ad486068d | ||
|
|
eed856cc04 | ||
|
|
d3e4ac76b4 | ||
|
|
1334ed89f0 | ||
|
|
219993cc2e | ||
|
|
9fcedd5b91 | ||
|
|
35a0b675a2 | ||
|
|
35b4b0754c | ||
|
|
3559759f4b | ||
|
|
1413dd7689 | ||
|
|
f0f10bcb95 | ||
|
|
ebe5306fd2 | ||
|
|
5fb69e42b0 | ||
|
|
9131427622 | ||
|
|
931f0c65af | ||
|
|
9573cab37e | ||
|
|
fcc879bd23 | ||
|
|
fdfec0ac4d | ||
|
|
042279682a | ||
|
|
62705917ab | ||
|
|
1cc787f1f0 | ||
|
|
08ce693fbf | ||
|
|
59b6cd0cdb | ||
|
|
c81b0dcd49 | ||
|
|
8b169551f5 | ||
|
|
a434419313 | ||
|
|
6653b9802e | ||
|
|
c0700a6946 | ||
|
|
55fb80b2fc | ||
|
|
d81c61637e | ||
|
|
3c874646e8 | ||
|
|
4c543fa824 | ||
|
|
f595818ede | ||
|
|
1eb396cb40 | ||
|
|
a6f87c3f72 | ||
|
|
33842d6d4f | ||
|
|
20cd3f5ceb | ||
|
|
5c1d586a25 | ||
|
|
2b926ef1c5 | ||
|
|
a724c933dc | ||
|
|
75d8250f3a | ||
|
|
f52a630197 | ||
|
|
c2075781ca | ||
|
|
902fcb3704 | ||
|
|
a9bb01031a | ||
|
|
c620ec2aca | ||
|
|
1274fe6562 | ||
|
|
7eaca9bb7d | ||
|
|
92e511284b | ||
|
|
b4c7ab8773 | ||
|
|
9b3adf0ddc | ||
|
|
30ff175f8e | ||
|
|
e7b4837be9 | ||
|
|
59a64af345 | ||
|
|
de4955a875 | ||
|
|
a03994d16a | ||
|
|
0311807951 | ||
|
|
a836ada6a7 | ||
|
|
7f04830f77 | ||
|
|
f0ec201745 | ||
|
|
de90108e0f | ||
|
|
691047039b | ||
|
|
ea233580d2 | ||
|
|
db36c52c62 | ||
|
|
4de10c830d | ||
|
|
4fac635272 | ||
|
|
212f10d22b | ||
|
|
746b413cff | ||
|
|
b2543b5c0f | ||
|
|
e7f3b6b12f | ||
|
|
7d39efe29f | ||
|
|
22fe50c67a | ||
|
|
d1fecc2a0f | ||
|
|
356e4263d2 | ||
|
|
43d14971f5 | ||
|
|
5dbce33f74 | ||
|
|
087c6915e7 | ||
|
|
a1be1ae40b | ||
|
|
4aed97b76b | ||
|
|
3668a445e5 | ||
|
|
04ed8abf5a | ||
|
|
7e54773a6e | ||
|
|
b4d6f3cb35 | ||
|
|
bda2bd8ac1 | ||
|
|
b329305b77 | ||
|
|
1e2e4b9d4d | ||
|
|
de56b7f327 | ||
|
|
0ea9e2e48a | ||
|
|
e11458196c | ||
|
|
09564947e8 | ||
|
|
21cc0b38b0 | ||
|
|
d40d705aac | ||
|
|
410e754cd9 | ||
|
|
e102a7488e | ||
|
|
b8325414bf | ||
|
|
07a18d31a9 | ||
|
|
cfe818e0aa | ||
|
|
ca0d5bf8cb | ||
|
|
4b1685d346 | ||
|
|
d97593e91d | ||
|
|
5f0e0383a5 | ||
|
|
8f2f2046b7 | ||
|
|
394e91d639 | ||
|
|
880a1fd13d | ||
|
|
d307b843f9 | ||
|
|
a111aa9fcd | ||
|
|
2b10907ef3 | ||
|
|
653de07bd7 | ||
|
|
62a0936c2e | ||
|
|
02be34159d | ||
|
|
76d7534d85 | ||
|
|
a71eaeaed2 | ||
|
|
df1d7184f8 | ||
|
|
427600d0cc | ||
|
|
9b313d9c0a | ||
|
|
bd52cf56e7 | ||
|
|
86eb86dac5 | ||
|
|
0b4ce8765e | ||
|
|
ff4b3eb9e5 | ||
|
|
b15913ce73 | ||
|
|
dd1f0a120a | ||
|
|
69f0464e05 | ||
|
|
76de64263c | ||
|
|
9ce1ecba49 | ||
|
|
6a3fc08957 | ||
|
|
0a7e6ae3ca | ||
|
|
f82125f416 | ||
|
|
c58f3d590c | ||
|
|
b63fdfab0d | ||
|
|
3866bc5155 | ||
|
|
0a205722b4 | ||
|
|
44a82d3b22 | ||
|
|
9c51c9971b | ||
|
|
6324f692b8 | ||
|
|
8d52ae869c | ||
|
|
629672c4b0 | ||
|
|
c6f1acede9 | ||
|
|
43779cf2e8 | ||
|
|
dbce45291c | ||
|
|
4bf7cf956b | ||
|
|
a68e2b9eb5 | ||
|
|
39f38f5031 | ||
|
|
652da9644d | ||
|
|
2965b499e9 | ||
|
|
449603d180 | ||
|
|
132db95fb7 | ||
|
|
0fb7099620 | ||
|
|
018ecc05ed | ||
|
|
58e9b4aa9c | ||
|
|
f743d5e853 | ||
|
|
c2302c0dad | ||
|
|
2d41e64a8b | ||
|
|
fedc6b865b | ||
|
|
d3fbfc75f0 | ||
|
|
42568b6494 | ||
|
|
84534eb8f9 | ||
|
|
ac20c84611 | ||
|
|
d722314e2d | ||
|
|
6072104153 | ||
|
|
a565fd9ee4 | ||
|
|
3272586e31 | ||
|
|
c9970a0782 | ||
|
|
b39e486e08 | ||
|
|
3bef57259d | ||
|
|
34f5ffd981 | ||
|
|
712926996e | ||
|
|
a6726d2e04 | ||
|
|
fe32e885c6 | ||
|
|
0fbc314fb4 | ||
|
|
14aa450e7c | ||
|
|
e191ae9651 | ||
|
|
0e1edadeb7 | ||
|
|
98210b5e9c | ||
|
|
876a2133b3 | ||
|
|
474e3f163c | ||
|
|
6834cf72ba | ||
|
|
7ceccafa7e | ||
|
|
8f41379548 | ||
|
|
b0cc2d23e2 | ||
|
|
4450d43c5b | ||
|
|
f75ad36770 | ||
|
|
a15974029e | ||
|
|
baf53d1a5a | ||
|
|
53d9d52405 | ||
|
|
60d701c945 | ||
|
|
d2577f670e | ||
|
|
a14cc9f932 | ||
|
|
cde458760c | ||
|
|
1dc4c327e4 | ||
|
|
fe88275cd0 | ||
|
|
28fa3037a4 | ||
|
|
e2f96be4dc | ||
|
|
61265c7533 | ||
|
|
7cefca330b | ||
|
|
bca1d585c5 | ||
|
|
721fda0aaa | ||
|
|
12303b3acf | ||
|
|
fc07dec3a6 | ||
|
|
718d96cc58 | ||
|
|
8784907736 | ||
|
|
f6b99a7906 | ||
|
|
ca3d05c723 | ||
|
|
46958cc838 | ||
|
|
f1af331a0c | ||
|
|
e0ccd7f578 | ||
|
|
a36340e9ad | ||
|
|
3dd4ee7a07 | ||
|
|
3a6b9564d5 | ||
|
|
b9e8b11311 | ||
|
|
80e7f4d85c | ||
|
|
9e0b767c98 | ||
|
|
ef54bc814d | ||
|
|
0df22ff581 | ||
|
|
6466c00acd | ||
|
|
222436589c | ||
|
|
179684a9e6 | ||
|
|
b83f95f78c | ||
|
|
f0953ed6b0 | ||
|
|
4b10c16419 | ||
|
|
68f7e8c15c | ||
|
|
6edab46d97 | ||
|
|
6e4f05ebd3 | ||
|
|
ff092a031e | ||
|
|
744f11733d | ||
|
|
0b4b605524 | ||
|
|
3b54fea309 | ||
|
|
c6f5d5e36e | ||
|
|
ed5e4271df | ||
|
|
8c238ce08d | ||
|
|
0b6b93048b | ||
|
|
fe7e96eb6a | ||
|
|
dc2828291b | ||
|
|
d384c0cfe6 | ||
|
|
3ac3b8c8d9 | ||
|
|
14b307c15b | ||
|
|
d2c2ae5b4d | ||
|
|
7e4dac9837 | ||
|
|
9d3d35e0bf | ||
|
|
c93c346473 | ||
|
|
866a572d26 | ||
|
|
780400fcf9 | ||
|
|
e46365ac20 | ||
|
|
26e574e8d7 | ||
|
|
d83395aeb0 | ||
|
|
57f891f391 | ||
|
|
6e309c6e04 | ||
|
|
da48603c18 | ||
|
|
4b3ac2ea61 | ||
|
|
6c512f1bc2 | ||
|
|
2dd89b488d | ||
|
|
b9f58946a3 | ||
|
|
a173e7e2da | ||
|
|
0ee307a027 | ||
|
|
981ff51d57 | ||
|
|
b249c4e4d7 | ||
|
|
b4a0555a72 | ||
|
|
6ade09312f | ||
|
|
854abb0993 | ||
|
|
4e64d35f89 | ||
|
|
18aed0bd79 | ||
|
|
7eb5035d94 | ||
|
|
ba57ca5f67 | ||
|
|
c2cf290054 | ||
|
|
aab50ef5c4 | ||
|
|
51c9958ff1 | ||
|
|
2c860df34b | ||
|
|
bd1745a50d | ||
|
|
04cb7820a6 | ||
|
|
e57569ad57 | ||
|
|
a458db319e | ||
|
|
d43b00dad9 | ||
|
|
52045b89fa | ||
|
|
5f76804f47 | ||
|
|
48a262ff1e | ||
|
|
3283e6c1a6 | ||
|
|
4f4b35e4bf | ||
|
|
c4f22a4508 | ||
|
|
2986a883eb | ||
|
|
3dd5accb56 | ||
|
|
ac718351a8 | ||
|
|
d0778a8a7b | ||
|
|
226f274a72 | ||
|
|
cb0d5a5ba6 | ||
|
|
747e212b1b | ||
|
|
39ad020418 | ||
|
|
0fbbb6a984 | ||
|
|
7e50a46d3b | ||
|
|
2a01c804af | ||
|
|
b7200837c3 | ||
|
|
353b596a6d | ||
|
|
87bc9299e6 | ||
|
|
6d4129c6b7 | ||
|
|
2c0c113420 | ||
|
|
1b0be3e87f | ||
|
|
8b30c33eae | ||
|
|
e456b4ce21 | ||
|
|
96fe09af89 | ||
|
|
cdf1d7f074 | ||
|
|
4b79b98b31 | ||
|
|
0385347a3a | ||
|
|
93f4666df4 | ||
|
|
3731c7c40c | ||
|
|
ef8c7f656d | ||
|
|
fa8d65cc1f | ||
|
|
1dade9d4b8 | ||
|
|
99058891d0 | ||
|
|
d627b0a064 | ||
|
|
0bfbfaa17c | ||
|
|
d2d3e0f11b | ||
|
|
a5bed5dcaa | ||
|
|
53e5ddb1a7 | ||
|
|
a180317509 | ||
|
|
cb1db262ca | ||
|
|
101e29b354 | ||
|
|
697fa6fdbc | ||
|
|
9a6cdcd5e2 | ||
|
|
1c60b805bf | ||
|
|
5a96f6dcce | ||
|
|
5dfed7623b | ||
|
|
d24cbf25c7 | ||
|
|
8ceb6f5621 | ||
|
|
0074860cad | ||
|
|
e5159a3ba2 | ||
|
|
7a63ae6315 | ||
|
|
b63e272490 | ||
|
|
004e6285a1 | ||
|
|
a4c4d0157b | ||
|
|
a7fc89a5bb | ||
|
|
236027fc22 | ||
|
|
5a712ebb6b | ||
|
|
3097e7f223 | ||
|
|
50b36bda2a | ||
|
|
91bb3c1e68 | ||
|
|
c519fa43c6 | ||
|
|
1c98e59003 | ||
|
|
982aaf6f76 | ||
|
|
58eae7a220 | ||
|
|
22420f595f | ||
|
|
cf94438150 | ||
|
|
3cad054af1 | ||
|
|
7a6fa5a457 | ||
|
|
9face76417 | ||
|
|
3169c5fb85 | ||
|
|
c189e4aaf5 | ||
|
|
29d6288b27 | ||
|
|
ae1e9a2aed | ||
|
|
6971f0a976 | ||
|
|
03327cb082 | ||
|
|
bd133b936c | ||
|
|
8a36d96e56 | ||
|
|
41f82bb357 | ||
|
|
4bad008085 | ||
|
|
4b751e8cbf | ||
|
|
c30c0c1d2a | ||
|
|
4e6a036f22 | ||
|
|
43d753dcb1 | ||
|
|
87877aeb3d | ||
|
|
f88baba66b | ||
|
|
9022ad2360 | ||
|
|
3c5de319d1 | ||
|
|
5b67abba31 | ||
|
|
e042572cfb | ||
|
|
e88c9b3525 | ||
|
|
9d05105dc0 | ||
|
|
e61a5cc83a | ||
|
|
d74c71c1d0 | ||
|
|
000d1f2f6e | ||
|
|
25a54b16f7 | ||
|
|
14899eb883 | ||
|
|
7454430aa1 | ||
|
|
11cc50af6e | ||
|
|
9081c205b1 | ||
|
|
1c2bccdeff | ||
|
|
2908ddb759 | ||
|
|
5485c12445 | ||
|
|
ccedefb8b8 | ||
|
|
6f4a0c4a61 | ||
|
|
de13932ffe | ||
|
|
ff5659cc0f | ||
|
|
86ce3a86e9 | ||
|
|
88dd977848 | ||
|
|
cfcb24bbc9 | ||
|
|
88463c1263 | ||
|
|
574f7cba67 | ||
|
|
64bf17684a | ||
|
|
b5991776f4 | ||
|
|
531af410f9 | ||
|
|
5816ed6600 | ||
|
|
2b0a639b95 | ||
|
|
a84e5554fb | ||
|
|
43372b7168 | ||
|
|
47237fa88f | ||
|
|
b00e689ff6 | ||
|
|
6dea40c868 | ||
|
|
52ef3ef7a5 | ||
|
|
3f508cd43b | ||
|
|
df61bd5ccd | ||
|
|
abfb3a27b1 | ||
|
|
59e7bc7438 | ||
|
|
d407c164b6 | ||
|
|
db4beb47f7 |
77
.claude/CLAUDE.md
Normal file
77
.claude/CLAUDE.md
Normal file
@ -0,0 +1,77 @@
|
||||
# Bitwarden Server - Claude Code Configuration
|
||||
|
||||
## Project Context Files
|
||||
|
||||
**Read these files before reviewing to ensure that you fully understand the project and contributing guidelines**
|
||||
|
||||
1. @README.md
|
||||
2. @CONTRIBUTING.md
|
||||
3. @.github/PULL_REQUEST_TEMPLATE.md
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- **NEVER** use code regions: If complexity suggests regions, refactor for better readability
|
||||
|
||||
- **NEVER** compromise zero-knowledge principles: User vault data must remain encrypted and inaccessible to Bitwarden
|
||||
|
||||
- **NEVER** log or expose sensitive data: No PII, passwords, keys, or vault data in logs or error messages
|
||||
|
||||
- **ALWAYS** use secure communication channels: Enforce confidentiality, integrity, and authenticity
|
||||
|
||||
- **ALWAYS** encrypt sensitive data: All vault data must be encrypted at rest, in transit, and in use
|
||||
|
||||
- **ALWAYS** prioritize cryptographic integrity and data protection
|
||||
|
||||
- **ALWAYS** add unit tests (with mocking) for any new feature development
|
||||
|
||||
## Project Structure
|
||||
|
||||
- **Source Code**: `/src/` - Services and core infrastructure
|
||||
- **Tests**: `/test/` - Test logic aligning with the source structure, albeit with a `.Test` suffix
|
||||
- **Utilities**: `/util/` - Migration tools, seeders, and setup scripts
|
||||
- **Dev Tools**: `/dev/` - Local development helpers
|
||||
- **Configuration**: `appsettings.{Environment}.json`, `/dev/secrets.json` for local development
|
||||
|
||||
## Security Requirements
|
||||
|
||||
- **Compliance**: SOC 2 Type II, SOC 3, HIPAA, ISO 27001, GDPR, CCPA
|
||||
- **Principles**: Zero-knowledge, end-to-end encryption, secure defaults
|
||||
- **Validation**: Input sanitization, parameterized queries, rate limiting
|
||||
- **Logging**: Structured logs, no PII/sensitive data in logs
|
||||
|
||||
## Common Commands
|
||||
|
||||
- **Build**: `dotnet build`
|
||||
- **Test**: `dotnet test`
|
||||
- **Run locally**: `dotnet run --project src/Api`
|
||||
- **Database update**: `pwsh dev/migrate.ps1`
|
||||
- **Generate OpenAPI**: `pwsh dev/generate_openapi_files.ps1`
|
||||
|
||||
## Development Workflow
|
||||
|
||||
- Security impact assessed
|
||||
- xUnit tests added / updated
|
||||
- Performance impact considered
|
||||
- Error handling implemented
|
||||
- Breaking changes documented
|
||||
- CI passes: build, test, lint
|
||||
- Feature flags considered for new features
|
||||
- CODEOWNERS file respected
|
||||
|
||||
### Key Architectural Decisions
|
||||
|
||||
- Use .NET nullable reference types (ADR 0024)
|
||||
- TryAdd dependency injection pattern (ADR 0026)
|
||||
- Authorization patterns (ADR 0022)
|
||||
- OpenTelemetry for observability (ADR 0020)
|
||||
- Log to standard output (ADR 0021)
|
||||
|
||||
## References
|
||||
|
||||
- [Server architecture](https://contributing.bitwarden.com/architecture/server/)
|
||||
- [Architectural Decision Records (ADRs)](https://contributing.bitwarden.com/architecture/adr/)
|
||||
- [Contributing guidelines](https://contributing.bitwarden.com/contributing/)
|
||||
- [Setup guide](https://contributing.bitwarden.com/getting-started/server/guide/)
|
||||
- [Code style](https://contributing.bitwarden.com/contributing/code-style/)
|
||||
- [Bitwarden security whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper/)
|
||||
- [Bitwarden security definitions](https://contributing.bitwarden.com/architecture/security/definitions)
|
||||
25
.claude/prompts/review-code.md
Normal file
25
.claude/prompts/review-code.md
Normal file
@ -0,0 +1,25 @@
|
||||
Please review this pull request with a focus on:
|
||||
|
||||
- Code quality and best practices
|
||||
- Potential bugs or issues
|
||||
- Security implications
|
||||
- Performance considerations
|
||||
|
||||
Note: The PR branch is already checked out in the current working directory.
|
||||
|
||||
Provide a comprehensive review including:
|
||||
|
||||
- Summary of changes since last review
|
||||
- Critical issues found (be thorough)
|
||||
- Suggested improvements (be thorough)
|
||||
- Good practices observed (be concise - list only the most notable items without elaboration)
|
||||
- Action items for the author
|
||||
- Leverage collapsible <details> sections where appropriate for lengthy explanations or code snippets to enhance human readability
|
||||
|
||||
When reviewing subsequent commits:
|
||||
|
||||
- Track status of previously identified issues (fixed/unfixed/reopened)
|
||||
- Identify NEW problems introduced since last review
|
||||
- Note if fixes introduced new issues
|
||||
|
||||
IMPORTANT: Be comprehensive about issues and improvements. For good practices, be brief - just note what was done well without explaining why or praising excessively.
|
||||
@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"swashbuckle.aspnetcore.cli": {
|
||||
"version": "7.3.2",
|
||||
"version": "9.0.4",
|
||||
"commands": ["swagger"]
|
||||
},
|
||||
"dotnet-ef": {
|
||||
|
||||
@ -123,3 +123,12 @@ 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
|
||||
|
||||
# CA2253: Named placeholders should nto be numeric values
|
||||
dotnet_diagnostic.CA2253.severity = suggestion
|
||||
|
||||
# CA2254: Template should be a static expression
|
||||
dotnet_diagnostic.CA2254.severity = warning
|
||||
|
||||
# CA1727: Use PascalCase for named placeholders
|
||||
dotnet_diagnostic.CA1727.severity = suggestion
|
||||
|
||||
22
.github/CODEOWNERS
vendored
22
.github/CODEOWNERS
vendored
@ -4,11 +4,12 @@
|
||||
#
|
||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
## Docker files have shared ownership ##
|
||||
**/Dockerfile
|
||||
**/*.Dockerfile
|
||||
**/.dockerignore
|
||||
**/entrypoint.sh
|
||||
## Docker-related files
|
||||
**/Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
**/*.Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
|
||||
## BRE team owns these workflows ##
|
||||
.github/workflows/publish.yml @bitwarden/dept-bre
|
||||
@ -35,6 +36,7 @@ util/Setup/** @bitwarden/dept-bre @bitwarden/team-platform-dev
|
||||
|
||||
# UIF
|
||||
src/Core/MailTemplates/Mjml @bitwarden/team-ui-foundation # Teams are expected to own sub-directories of this project
|
||||
src/Core/MailTemplates/Mjml/.mjmlconfig # This change allows teams to add components within their own subdirectories without requiring a code review from UIF.
|
||||
|
||||
# Auth team
|
||||
**/Auth @bitwarden/team-auth-dev
|
||||
@ -92,7 +94,17 @@ src/Admin/Views/Tools @bitwarden/team-billing-dev
|
||||
**/.dockerignore @bitwarden/team-platform-dev
|
||||
**/Dockerfile @bitwarden/team-platform-dev
|
||||
**/entrypoint.sh @bitwarden/team-platform-dev
|
||||
# The PushType enum is expected to be editted by anyone without need for Platform review
|
||||
src/Core/Platform/Push/PushType.cs
|
||||
|
||||
# SDK
|
||||
util/RustSdk @bitwarden/team-sdk-sme
|
||||
|
||||
# Multiple owners - DO NOT REMOVE (BRE)
|
||||
**/packages.lock.json
|
||||
Directory.Build.props
|
||||
|
||||
# Claude related files
|
||||
.claude/ @bitwarden/team-ai-sme
|
||||
.github/workflows/respond.yml @bitwarden/team-ai-sme
|
||||
.github/workflows/review-code.yml @bitwarden/team-ai-sme
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
name: Bitwarden Unified Bug Report
|
||||
name: Bitwarden Unified Deployment Bug Report
|
||||
name: Bitwarden lite Deployment Bug Report
|
||||
description: File a bug report
|
||||
labels: [bug, bw-unified-deploy]
|
||||
labels: [bug, bw-lite-deploy]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@ -75,7 +74,7 @@ body:
|
||||
id: epic-label
|
||||
attributes:
|
||||
label: Issue-Link
|
||||
description: Link to our pinned issue, tracking all Bitwarden Unified
|
||||
description: Link to our pinned issue, tracking all Bitwarden lite
|
||||
value: |
|
||||
https://github.com/bitwarden/server/issues/2480
|
||||
validations:
|
||||
17
.github/renovate.json5
vendored
17
.github/renovate.json5
vendored
@ -2,6 +2,7 @@
|
||||
$schema: "https://docs.renovatebot.com/renovate-schema.json",
|
||||
extends: ["github>bitwarden/renovate-config"], // Extends our default configuration for pinned dependencies
|
||||
enabledManagers: [
|
||||
"cargo",
|
||||
"dockerfile",
|
||||
"docker-compose",
|
||||
"github-actions",
|
||||
@ -9,6 +10,11 @@
|
||||
"nuget",
|
||||
],
|
||||
packageRules: [
|
||||
{
|
||||
groupName: "cargo minor",
|
||||
matchManagers: ["cargo"],
|
||||
matchUpdateTypes: ["minor"],
|
||||
},
|
||||
{
|
||||
groupName: "dockerfile minor",
|
||||
matchManagers: ["dockerfile"],
|
||||
@ -35,6 +41,11 @@
|
||||
matchUpdateTypes: ["patch"],
|
||||
dependencyDashboardApproval: false,
|
||||
},
|
||||
{
|
||||
matchPackageNames: ["https://github.com/bitwarden/sdk-internal.git"],
|
||||
groupName: "sdk-internal",
|
||||
dependencyDashboardApproval: true
|
||||
},
|
||||
{
|
||||
matchManagers: ["dockerfile", "docker-compose"],
|
||||
commitMessagePrefix: "[deps] BRE:",
|
||||
@ -53,7 +64,6 @@
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
|
||||
"DuoUniversal",
|
||||
"Fido2.AspNet",
|
||||
"Duende.IdentityServer",
|
||||
@ -80,11 +90,7 @@
|
||||
"Microsoft.AspNetCore.Mvc.Testing",
|
||||
"Newtonsoft.Json",
|
||||
"NSubstitute",
|
||||
"Sentry.Serilog",
|
||||
"Serilog.AspNetCore",
|
||||
"Serilog.Extensions.Logging",
|
||||
"Serilog.Extensions.Logging.File",
|
||||
"Serilog.Sinks.SyslogMessages",
|
||||
"Stripe.net",
|
||||
"Swashbuckle.AspNetCore",
|
||||
"Swashbuckle.AspNetCore.SwaggerGen",
|
||||
@ -131,6 +137,7 @@
|
||||
"AspNetCoreRateLimit",
|
||||
"AspNetCoreRateLimit.Redis",
|
||||
"Azure.Data.Tables",
|
||||
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
|
||||
"Azure.Messaging.EventGrid",
|
||||
"Azure.Messaging.ServiceBus",
|
||||
"Azure.Storage.Blobs",
|
||||
|
||||
30
.github/workflows/_move_edd_db_scripts.yml
vendored
30
.github/workflows/_move_edd_db_scripts.yml
vendored
@ -41,18 +41,19 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Get script prefix
|
||||
id: prefix
|
||||
run: echo "prefix=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
run: echo "prefix=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check if any files in DB transition or finalization directories
|
||||
id: check-script-existence
|
||||
run: |
|
||||
if [ -f util/Migrator/DbScripts_transition/* -o -f util/Migrator/DbScripts_finalization/* ]; then
|
||||
echo "copy_edd_scripts=true" >> $GITHUB_OUTPUT
|
||||
echo "copy_edd_scripts=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "copy_edd_scripts=false" >> $GITHUB_OUTPUT
|
||||
echo "copy_edd_scripts=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
move-scripts:
|
||||
@ -70,17 +71,18 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Generate branch name
|
||||
id: branch_name
|
||||
env:
|
||||
PREFIX: ${{ needs.setup.outputs.migration_filename_prefix }}
|
||||
run: echo "branch_name=move_edd_db_scripts_$PREFIX" >> $GITHUB_OUTPUT
|
||||
run: echo "branch_name=move_edd_db_scripts_$PREFIX" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: "Create branch"
|
||||
env:
|
||||
BRANCH: ${{ steps.branch_name.outputs.branch_name }}
|
||||
run: git switch -c $BRANCH
|
||||
run: git switch -c "$BRANCH"
|
||||
|
||||
- name: Move scripts and finalization database schema
|
||||
id: move-files
|
||||
@ -120,7 +122,7 @@ jobs:
|
||||
|
||||
# sync finalization schema back to dbo, maintaining structure
|
||||
rsync -r "$src_dir/" "$dest_dir/"
|
||||
rm -rf $src_dir/*
|
||||
rm -rf "${src_dir}"/*
|
||||
|
||||
# Replace any finalization references due to the move
|
||||
find ./src/Sql/dbo -name "*.sql" -type f -exec sed -i \
|
||||
@ -131,7 +133,7 @@ jobs:
|
||||
moved_files="$moved_files \n $file"
|
||||
done
|
||||
|
||||
echo "moved_files=$moved_files" >> $GITHUB_OUTPUT
|
||||
echo "moved_files=$moved_files" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
@ -153,7 +155,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Import GPG keys
|
||||
uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0
|
||||
uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0
|
||||
with:
|
||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
||||
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
||||
@ -162,18 +164,20 @@ jobs:
|
||||
|
||||
- name: Commit and push changes
|
||||
id: commit
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.branch_name.outputs.branch_name }}
|
||||
run: |
|
||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||
git config --local user.name "bitwarden-devops-bot"
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
git add .
|
||||
git commit -m "Move EDD database scripts" -a
|
||||
git push -u origin ${{ steps.branch_name.outputs.branch_name }}
|
||||
echo "pr_needed=true" >> $GITHUB_OUTPUT
|
||||
git push -u origin "${BRANCH_NAME}"
|
||||
echo "pr_needed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No changes to commit!";
|
||||
echo "pr_needed=false" >> $GITHUB_OUTPUT
|
||||
echo "### :mega: No changes to commit! PR was ommited." >> $GITHUB_STEP_SUMMARY
|
||||
echo "pr_needed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "### :mega: No changes to commit! PR was ommited." >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Create PR for ${{ steps.branch_name.outputs.branch_name }}
|
||||
@ -195,7 +199,7 @@ jobs:
|
||||
Files moved:
|
||||
$(echo -e "$MOVED_FILES")
|
||||
")
|
||||
echo "pr_url=${PR_URL}" >> $GITHUB_OUTPUT
|
||||
echo "pr_url=${PR_URL}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Notify Slack about creation of PR
|
||||
if: ${{ steps.commit.outputs.pr_needed == 'true' }}
|
||||
|
||||
214
.github/workflows/build.yml
vendored
214
.github/workflows/build.yml
vendored
@ -22,22 +22,23 @@ env:
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
|
||||
- name: Verify format
|
||||
run: dotnet format --verify-no-changes
|
||||
|
||||
build-artifacts:
|
||||
name: Build Docker images
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- lint
|
||||
outputs:
|
||||
@ -45,6 +46,7 @@ jobs:
|
||||
permissions:
|
||||
security-events: write
|
||||
id-token: write
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -97,30 +99,31 @@ jobs:
|
||||
id: check-secrets
|
||||
run: |
|
||||
has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }}
|
||||
echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
|
||||
echo "has_secrets=$has_secrets" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check branch to publish
|
||||
env:
|
||||
PUBLISH_BRANCHES: "main,rc,hotfix-rc"
|
||||
id: publish-branch-check
|
||||
run: |
|
||||
IFS="," read -a publish_branches <<< $PUBLISH_BRANCHES
|
||||
IFS="," read -a publish_branches <<< "$PUBLISH_BRANCHES"
|
||||
if [[ " ${publish_branches[*]} " =~ " ${GITHUB_REF:11} " ]]; then
|
||||
echo "is_publish_branch=true" >> $GITHUB_ENV
|
||||
echo "is_publish_branch=true" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "is_publish_branch=false" >> $GITHUB_ENV
|
||||
echo "is_publish_branch=false" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
@ -157,7 +160,7 @@ jobs:
|
||||
ls -atlh ../../../
|
||||
|
||||
- name: Upload project artifact
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
if: ${{ matrix.dotnet }}
|
||||
with:
|
||||
name: ${{ matrix.project_name }}.zip
|
||||
@ -166,10 +169,10 @@ jobs:
|
||||
|
||||
########## Set up Docker ##########
|
||||
- name: Set up QEMU emulators
|
||||
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
|
||||
########## ACRs ##########
|
||||
- name: Log in to Azure
|
||||
@ -182,13 +185,6 @@ jobs:
|
||||
- name: Log in to ACR - production subscription
|
||||
run: az acr login -n bitwardenprod
|
||||
|
||||
- name: Retrieve GitHub PAT secrets
|
||||
id: retrieve-secret-pat
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
########## Generate image tag and build Docker image ##########
|
||||
- name: Generate Docker image tag
|
||||
id: tag
|
||||
@ -209,8 +205,8 @@ jobs:
|
||||
IMAGE_TAG=dev
|
||||
fi
|
||||
|
||||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||
echo "### :mega: Docker Image Tag: $IMAGE_TAG" >> $GITHUB_STEP_SUMMARY
|
||||
echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
|
||||
echo "### :mega: Docker Image Tag: $IMAGE_TAG" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Set up project name
|
||||
id: setup
|
||||
@ -218,7 +214,7 @@ jobs:
|
||||
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
|
||||
echo "project_name=$PROJECT_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Generate image tags(s)
|
||||
id: image-tags
|
||||
@ -228,16 +224,16 @@ jobs:
|
||||
SHA: ${{ github.sha }}
|
||||
run: |
|
||||
TAGS="${_AZ_REGISTRY}/${PROJECT_NAME}:${IMAGE_TAG}"
|
||||
echo "primary_tag=$TAGS" >> $GITHUB_OUTPUT
|
||||
echo "primary_tag=$TAGS" >> "$GITHUB_OUTPUT"
|
||||
if [[ "${IMAGE_TAG}" == "dev" ]]; then
|
||||
SHORT_SHA=$(git rev-parse --short ${SHA})
|
||||
SHORT_SHA=$(git rev-parse --short "${SHA}")
|
||||
TAGS=$TAGS",${_AZ_REGISTRY}/${PROJECT_NAME}:dev-${SHORT_SHA}"
|
||||
fi
|
||||
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
||||
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build Docker image
|
||||
id: build-artifacts
|
||||
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
|
||||
@ -247,12 +243,10 @@ jobs:
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.image-tags.outputs.tags }}
|
||||
secrets: |
|
||||
"GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}"
|
||||
|
||||
- name: Install Cosign
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
|
||||
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
|
||||
|
||||
- name: Sign image with Cosign
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
@ -260,23 +254,24 @@ jobs:
|
||||
DIGEST: ${{ steps.build-artifacts.outputs.digest }}
|
||||
TAGS: ${{ steps.image-tags.outputs.tags }}
|
||||
run: |
|
||||
IFS="," read -a tags <<< "${TAGS}"
|
||||
images=""
|
||||
for tag in "${tags[@]}"; do
|
||||
images+="${tag}@${DIGEST} "
|
||||
IFS=',' read -r -a tags_array <<< "${TAGS}"
|
||||
images=()
|
||||
for tag in "${tags_array[@]}"; do
|
||||
images+=("${tag}@${DIGEST}")
|
||||
done
|
||||
cosign sign --yes ${images}
|
||||
cosign sign --yes ${images[@]}
|
||||
echo "images=${images[*]}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Scan Docker image
|
||||
id: container-scan
|
||||
uses: anchore/scan-action@abae793926ec39a78ab18002bc7fc45bbbd94342 # v6.0.0
|
||||
uses: anchore/scan-action@f6601287cdb1efc985d6b765bbf99cb4c0ac29d8 # v7.0.0
|
||||
with:
|
||||
image: ${{ steps.image-tags.outputs.primary_tag }}
|
||||
fail-build: false
|
||||
output-format: sarif
|
||||
|
||||
- name: Upload Grype results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
||||
uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
with:
|
||||
sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
||||
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
|
||||
@ -287,7 +282,7 @@ jobs:
|
||||
|
||||
upload:
|
||||
name: Upload
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build-artifacts
|
||||
permissions:
|
||||
id-token: write
|
||||
@ -297,9 +292,10 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
@ -309,7 +305,7 @@ jobs:
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Log in to ACR - production subscription
|
||||
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
||||
run: az acr login -n "$_AZ_REGISTRY" --only-show-errors
|
||||
|
||||
- name: Make Docker stubs
|
||||
if: |
|
||||
@ -332,26 +328,26 @@ jobs:
|
||||
STUB_OUTPUT=$(pwd)/docker-stub
|
||||
|
||||
# Run setup
|
||||
docker run -i --rm --name setup -v $STUB_OUTPUT/US:/bitwarden $SETUP_IMAGE \
|
||||
docker run -i --rm --name setup -v "$STUB_OUTPUT/US:/bitwarden" "$SETUP_IMAGE" \
|
||||
/app/Setup -stub 1 -install 1 -domain bitwarden.example.com -os lin -cloud-region US
|
||||
docker run -i --rm --name setup -v $STUB_OUTPUT/EU:/bitwarden $SETUP_IMAGE \
|
||||
docker run -i --rm --name setup -v "$STUB_OUTPUT/EU:/bitwarden" "$SETUP_IMAGE" \
|
||||
/app/Setup -stub 1 -install 1 -domain bitwarden.example.com -os lin -cloud-region EU
|
||||
|
||||
sudo chown -R $(whoami):$(whoami) $STUB_OUTPUT
|
||||
sudo chown -R "$(whoami):$(whoami)" "$STUB_OUTPUT"
|
||||
|
||||
# Remove extra directories and files
|
||||
rm -rf $STUB_OUTPUT/US/letsencrypt
|
||||
rm -rf $STUB_OUTPUT/EU/letsencrypt
|
||||
rm $STUB_OUTPUT/US/env/uid.env $STUB_OUTPUT/US/config.yml
|
||||
rm $STUB_OUTPUT/EU/env/uid.env $STUB_OUTPUT/EU/config.yml
|
||||
rm -rf "$STUB_OUTPUT/US/letsencrypt"
|
||||
rm -rf "$STUB_OUTPUT/EU/letsencrypt"
|
||||
rm "$STUB_OUTPUT/US/env/uid.env" "$STUB_OUTPUT/US/config.yml"
|
||||
rm "$STUB_OUTPUT/EU/env/uid.env" "$STUB_OUTPUT/EU/config.yml"
|
||||
|
||||
# Create uid environment files
|
||||
touch $STUB_OUTPUT/US/env/uid.env
|
||||
touch $STUB_OUTPUT/EU/env/uid.env
|
||||
touch "$STUB_OUTPUT/US/env/uid.env"
|
||||
touch "$STUB_OUTPUT/EU/env/uid.env"
|
||||
|
||||
# Zip up the Docker stub files
|
||||
cd docker-stub/US; zip -r ../../docker-stub-US.zip *; cd ../..
|
||||
cd docker-stub/EU; zip -r ../../docker-stub-EU.zip *; cd ../..
|
||||
cd docker-stub/US; zip -r ../../docker-stub-US.zip ./*; cd ../..
|
||||
cd docker-stub/EU; zip -r ../../docker-stub-EU.zip ./*; cd ../..
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
@ -360,7 +356,7 @@ jobs:
|
||||
if: |
|
||||
github.event_name != 'pull_request'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: docker-stub-US.zip
|
||||
path: docker-stub-US.zip
|
||||
@ -370,72 +366,33 @@ jobs:
|
||||
if: |
|
||||
github.event_name != 'pull_request'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: docker-stub-EU.zip
|
||||
path: docker-stub-EU.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Build Public API Swagger
|
||||
- name: Build Swagger files
|
||||
run: |
|
||||
cd ./src/Api
|
||||
echo "Restore tools"
|
||||
dotnet tool restore
|
||||
echo "Publish"
|
||||
dotnet publish -c "Release" -o obj/build-output/publish
|
||||
|
||||
dotnet swagger tofile --output ../../swagger.json --host https://api.bitwarden.com \
|
||||
./obj/build-output/publish/Api.dll public
|
||||
cd ../..
|
||||
env:
|
||||
ASPNETCORE_ENVIRONMENT: Production
|
||||
swaggerGen: "True"
|
||||
DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX: 2
|
||||
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
|
||||
cd ./dev
|
||||
pwsh ./generate_openapi_files.ps1
|
||||
|
||||
- name: Upload Public API Swagger artifact
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: swagger.json
|
||||
path: swagger.json
|
||||
path: api.public.json
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Build Internal API Swagger
|
||||
run: |
|
||||
cd ./src/Api
|
||||
echo "Restore API tools"
|
||||
dotnet tool restore
|
||||
echo "Publish API"
|
||||
dotnet publish -c "Release" -o obj/build-output/publish
|
||||
|
||||
dotnet swagger tofile --output ../../internal.json --host https://api.bitwarden.com \
|
||||
./obj/build-output/publish/Api.dll internal
|
||||
|
||||
cd ../Identity
|
||||
|
||||
echo "Restore Identity tools"
|
||||
dotnet tool restore
|
||||
echo "Publish Identity"
|
||||
dotnet publish -c "Release" -o obj/build-output/publish
|
||||
|
||||
dotnet swagger tofile --output ../../identity.json --host https://identity.bitwarden.com \
|
||||
./obj/build-output/publish/Identity.dll v1
|
||||
cd ../..
|
||||
env:
|
||||
ASPNETCORE_ENVIRONMENT: Development
|
||||
swaggerGen: "True"
|
||||
DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX: 2
|
||||
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
|
||||
|
||||
- name: Upload Internal API Swagger artifact
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: internal.json
|
||||
path: internal.json
|
||||
path: api.json
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Identity Swagger artifact
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: identity.json
|
||||
path: identity.json
|
||||
@ -443,7 +400,7 @@ jobs:
|
||||
|
||||
build-mssqlmigratorutility:
|
||||
name: Build MSSQL migrator utility
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- lint
|
||||
defaults:
|
||||
@ -462,9 +419,10 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
@ -480,7 +438,7 @@ jobs:
|
||||
|
||||
- name: Upload project artifact for Windows
|
||||
if: ${{ contains(matrix.target, 'win') == true }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: MsSqlMigratorUtility-${{ matrix.target }}
|
||||
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility.exe
|
||||
@ -488,7 +446,7 @@ jobs:
|
||||
|
||||
- name: Upload project artifact
|
||||
if: ${{ contains(matrix.target, 'win') == false }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: MsSqlMigratorUtility-${{ matrix.target }}
|
||||
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility
|
||||
@ -499,7 +457,7 @@ jobs:
|
||||
if: |
|
||||
github.event_name != 'pull_request'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- build-artifacts
|
||||
permissions:
|
||||
@ -512,25 +470,34 @@ jobs:
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Retrieve GitHub PAT secrets
|
||||
id: retrieve-secret-pat
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Trigger self-host build
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
id: app-token
|
||||
with:
|
||||
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: self-host
|
||||
|
||||
- name: Trigger Bitwarden lite build
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'bitwarden',
|
||||
repo: 'self-host',
|
||||
workflow_id: 'build-unified.yml',
|
||||
workflow_id: 'build-bitwarden-lite.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
server_branch: process.env.GITHUB_REF
|
||||
@ -553,20 +520,29 @@ jobs:
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Retrieve GitHub PAT secrets
|
||||
id: retrieve-secret-pat
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Trigger k8s deploy
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
id: app-token
|
||||
with:
|
||||
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: devops
|
||||
|
||||
- name: Trigger k8s deploy
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'bitwarden',
|
||||
|
||||
1
.github/workflows/build_target.yml
vendored
1
.github/workflows/build_target.yml
vendored
@ -26,5 +26,6 @@ jobs:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
id-token: write
|
||||
security-events: write
|
||||
|
||||
12
.github/workflows/cleanup-after-pr.yml
vendored
12
.github/workflows/cleanup-after-pr.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Log in to Azure ACR
|
||||
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
||||
run: az acr login -n "$_AZ_REGISTRY" --only-show-errors
|
||||
|
||||
########## Remove Docker images ##########
|
||||
- name: Remove the Docker image from ACR
|
||||
@ -45,20 +45,20 @@ jobs:
|
||||
- Setup
|
||||
- Sso
|
||||
run: |
|
||||
for SERVICE in $(echo "${{ env.SERVICES }}" | yq e ".services[]" - )
|
||||
for SERVICE in $(echo "${SERVICES}" | yq e ".services[]" - )
|
||||
do
|
||||
SERVICE_NAME=$(echo $SERVICE | awk '{print tolower($0)}')
|
||||
SERVICE_NAME=$(echo "$SERVICE" | awk '{print tolower($0)}')
|
||||
IMAGE_TAG=$(echo "${REF}" | sed "s#/#-#g") # slash safe branch name
|
||||
|
||||
echo "[*] Checking if remote exists: $_AZ_REGISTRY/$SERVICE_NAME:$IMAGE_TAG"
|
||||
TAG_EXISTS=$(
|
||||
az acr repository show-tags --name $_AZ_REGISTRY --repository $SERVICE_NAME \
|
||||
| jq --arg $TAG "$IMAGE_TAG" -e '. | any(. == "$TAG")'
|
||||
az acr repository show-tags --name "$_AZ_REGISTRY" --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 $_AZ_REGISTRY --image $SERVICE_NAME:$IMAGE_TAG --yes
|
||||
az acr repository delete --name "$_AZ_REGISTRY" --image "$SERVICE_NAME:$IMAGE_TAG" --yes
|
||||
else
|
||||
echo "[*] Tag does not exist. No action needed"
|
||||
fi
|
||||
|
||||
14
.github/workflows/cleanup-rc-branch.yml
vendored
14
.github/workflows/cleanup-rc-branch.yml
vendored
@ -35,6 +35,8 @@ jobs:
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if a RC branch exists
|
||||
id: branch-check
|
||||
@ -43,11 +45,11 @@ jobs:
|
||||
rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
|
||||
|
||||
if [[ "${hotfix_rc_branch_check}" -gt 0 ]]; then
|
||||
echo "hotfix-rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "name=hotfix-rc" >> $GITHUB_OUTPUT
|
||||
echo "hotfix-rc branch exists." | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
echo "name=hotfix-rc" >> "$GITHUB_OUTPUT"
|
||||
elif [[ "${rc_branch_check}" -gt 0 ]]; then
|
||||
echo "rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "name=rc" >> $GITHUB_OUTPUT
|
||||
echo "rc branch exists." | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
echo "name=rc" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Delete RC branch
|
||||
@ -55,6 +57,6 @@ jobs:
|
||||
BRANCH_NAME: ${{ steps.branch-check.outputs.name }}
|
||||
run: |
|
||||
if ! [[ -z "$BRANCH_NAME" ]]; then
|
||||
git push --quiet origin --delete $BRANCH_NAME
|
||||
echo "Deleted $BRANCH_NAME branch." | tee -a $GITHUB_STEP_SUMMARY
|
||||
git push --quiet origin --delete "$BRANCH_NAME"
|
||||
echo "Deleted $BRANCH_NAME branch." | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
10
.github/workflows/code-references.yml
vendored
10
.github/workflows/code-references.yml
vendored
@ -19,9 +19,9 @@ jobs:
|
||||
id: check-secret-access
|
||||
run: |
|
||||
if [ "${{ secrets.AZURE_CLIENT_ID }}" != '' ]; then
|
||||
echo "available=true" >> $GITHUB_OUTPUT;
|
||||
echo "available=true" >> "$GITHUB_OUTPUT";
|
||||
else
|
||||
echo "available=false" >> $GITHUB_OUTPUT;
|
||||
echo "available=false" >> "$GITHUB_OUTPUT";
|
||||
fi
|
||||
|
||||
refs:
|
||||
@ -37,6 +37,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
@ -65,14 +67,14 @@ jobs:
|
||||
|
||||
- name: Add label
|
||||
if: steps.collect.outputs.any-changed == 'true'
|
||||
run: gh pr edit $PR_NUMBER --add-label feature-flag
|
||||
run: gh pr edit "$PR_NUMBER" --add-label feature-flag
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
- name: Remove label
|
||||
if: steps.collect.outputs.any-changed == 'false'
|
||||
run: gh pr edit $PR_NUMBER --remove-label feature-flag
|
||||
run: gh pr edit "$PR_NUMBER" --remove-label feature-flag
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
2
.github/workflows/enforce-labels.yml
vendored
2
.github/workflows/enforce-labels.yml
vendored
@ -17,5 +17,5 @@ jobs:
|
||||
- name: Check for label
|
||||
run: |
|
||||
echo "PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged"
|
||||
echo "### :x: PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### :x: PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
|
||||
2
.github/workflows/ephemeral-environment.yml
vendored
2
.github/workflows/ephemeral-environment.yml
vendored
@ -16,5 +16,5 @@ jobs:
|
||||
with:
|
||||
project: server
|
||||
pull_request_number: ${{ github.event.number }}
|
||||
sync_environment: true
|
||||
sync_environment: false
|
||||
secrets: inherit
|
||||
|
||||
114
.github/workflows/load-test.yml
vendored
Normal file
114
.github/workflows/load-test.yml
vendored
Normal file
@ -0,0 +1,114 @@
|
||||
name: Load test
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 1" # Run every Monday at 00:00
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test-id:
|
||||
type: string
|
||||
description: "Identifier label for Datadog metrics"
|
||||
default: "server-load-test"
|
||||
k6-test-path:
|
||||
type: string
|
||||
description: "Path to load test files"
|
||||
default: "perf/load/*.js"
|
||||
k6-flags:
|
||||
type: string
|
||||
description: "Additional k6 flags"
|
||||
api-env-url:
|
||||
type: string
|
||||
description: "URL of the API environment"
|
||||
default: "https://api.qa.bitwarden.pw"
|
||||
identity-env-url:
|
||||
type: string
|
||||
description: "URL of the Identity environment"
|
||||
default: "https://identity.qa.bitwarden.pw"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
# Secret configuration
|
||||
AZURE_KEY_VAULT_NAME: gh-server
|
||||
AZURE_KEY_VAULT_SECRETS: DD-API-KEY, K6-CLIENT-ID, K6-AUTH-USER-EMAIL, K6-AUTH-USER-PASSWORD-HASH
|
||||
# Specify defaults for scheduled runs
|
||||
TEST_ID: ${{ inputs.test-id || 'server-load-test' }}
|
||||
K6_TEST_PATH: ${{ inputs.k6-test-path || 'perf/load/*.js' }}
|
||||
API_ENV_URL: ${{ inputs.api-env-url || 'https://api.qa.bitwarden.pw' }}
|
||||
IDENTITY_ENV_URL: ${{ inputs.identity-env-url || 'https://identity.qa.bitwarden.pw' }}
|
||||
|
||||
jobs:
|
||||
run-tests:
|
||||
name: Run load tests
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: ${{ env.AZURE_KEY_VAULT_NAME }}
|
||||
secrets: ${{ env.AZURE_KEY_VAULT_SECRETS }}
|
||||
|
||||
- name: Log out of Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
# Datadog agent for collecting OTEL metrics from k6
|
||||
- name: Start Datadog agent
|
||||
env:
|
||||
DD_API_KEY: ${{ steps.get-kv-secrets.outputs.DD-API-KEY }}
|
||||
run: |
|
||||
docker run --detach \
|
||||
--name datadog-agent \
|
||||
-p 4317:4317 \
|
||||
-p 5555:5555 \
|
||||
-e DD_SITE=us3.datadoghq.com \
|
||||
-e DD_API_KEY="${DD_API_KEY}" \
|
||||
-e DD_DOGSTATSD_NON_LOCAL_TRAFFIC=1 \
|
||||
-e DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT=0.0.0.0:4317 \
|
||||
-e DD_HEALTH_PORT=5555 \
|
||||
-e HOST_PROC=/proc \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||
--volume /sys/fs/cgroup/:/host/sys/fs/cgroup:ro \
|
||||
--health-cmd "curl -f http://localhost:5555/health || exit 1" \
|
||||
--health-interval 10s \
|
||||
--health-timeout 5s \
|
||||
--health-retries 10 \
|
||||
--health-start-period 30s \
|
||||
--pid host \
|
||||
datadog/agent:7-full@sha256:7ea933dec3b8baa8c19683b1c3f6f801dbf3291f748d9ed59234accdaac4e479
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up k6
|
||||
uses: grafana/setup-k6-action@ffe7d7290dfa715e48c2ccc924d068444c94bde2 # v1.1.0
|
||||
|
||||
- name: Run k6 tests
|
||||
uses: grafana/run-k6-action@c6b79182b9b666aa4f630f4a6be9158ead62536e # v1.2.0
|
||||
continue-on-error: false
|
||||
env:
|
||||
K6_OTEL_METRIC_PREFIX: k6_
|
||||
K6_OTEL_GRPC_EXPORTER_INSECURE: true
|
||||
# Load test specific environment variables
|
||||
API_URL: ${{ env.API_ENV_URL }}
|
||||
IDENTITY_URL: ${{ env.IDENTITY_ENV_URL }}
|
||||
CLIENT_ID: ${{ steps.get-kv-secrets.outputs.K6-CLIENT-ID }}
|
||||
AUTH_USER_EMAIL: ${{ steps.get-kv-secrets.outputs.K6-AUTH-USER-EMAIL }}
|
||||
AUTH_USER_PASSWORD_HASH: ${{ steps.get-kv-secrets.outputs.K6-AUTH-USER-PASSWORD-HASH }}
|
||||
with:
|
||||
flags: >-
|
||||
--tag test-id=${{ env.TEST_ID }}
|
||||
-o experimental-opentelemetry
|
||||
${{ inputs.k6-flags }}
|
||||
path: ${{ env.K6_TEST_PATH }}
|
||||
5
.github/workflows/protect-files.yml
vendored
5
.github/workflows/protect-files.yml
vendored
@ -34,6 +34,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check for file changes
|
||||
id: check-changes
|
||||
@ -43,9 +44,9 @@ jobs:
|
||||
for file in $MODIFIED_FILES
|
||||
do
|
||||
if [[ $file == *"${{ matrix.path }}"* ]]; then
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
echo "changes_detected=true" >> "$GITHUB_OUTPUT"
|
||||
break
|
||||
else echo "changes_detected=false" >> $GITHUB_OUTPUT
|
||||
else echo "changes_detected=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
37
.github/workflows/publish.yml
vendored
37
.github/workflows/publish.yml
vendored
@ -36,21 +36,23 @@ jobs:
|
||||
steps:
|
||||
- name: Version output
|
||||
id: version-output
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
if [[ "${{ inputs.version }}" == "latest" || "${{ inputs.version }}" == "" ]]; then
|
||||
if [[ "${INPUT_VERSION}" == "latest" || "${INPUT_VERSION}" == "" ]]; then
|
||||
VERSION=$(curl "https://api.github.com/repos/bitwarden/server/releases" | jq -c '.[] | select(.tag_name) | .tag_name' | head -1 | grep -ohE '20[0-9]{2}\.([1-9]|1[0-2])\.[0-9]+')
|
||||
echo "Latest Released Version: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Release Version: ${{ inputs.version }}"
|
||||
echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
|
||||
echo "Release Version: ${INPUT_VERSION}"
|
||||
echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Get branch name
|
||||
id: branch
|
||||
run: |
|
||||
BRANCH_NAME=$(basename ${{ github.ref }})
|
||||
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
BRANCH_NAME=$(basename "${GITHUB_REF}")
|
||||
echo "branch-name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create GitHub deployment
|
||||
uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7
|
||||
@ -105,6 +107,9 @@ jobs:
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up project name
|
||||
id: setup
|
||||
@ -112,7 +117,7 @@ jobs:
|
||||
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
|
||||
echo "project_name=$PROJECT_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
########## ACR PROD ##########
|
||||
- name: Log in to Azure
|
||||
@ -123,16 +128,16 @@ jobs:
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Log in to Azure ACR
|
||||
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
||||
run: az acr login -n "$_AZ_REGISTRY" --only-show-errors
|
||||
|
||||
- name: Pull latest project image
|
||||
env:
|
||||
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||
run: |
|
||||
if [[ "${{ inputs.publish_type }}" == "Dry Run" ]]; then
|
||||
docker pull $_AZ_REGISTRY/$PROJECT_NAME:latest
|
||||
docker pull "$_AZ_REGISTRY/$PROJECT_NAME:latest"
|
||||
else
|
||||
docker pull $_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME
|
||||
docker pull "$_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME"
|
||||
fi
|
||||
|
||||
- name: Tag version and latest
|
||||
@ -140,10 +145,10 @@ jobs:
|
||||
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||
run: |
|
||||
if [[ "${{ inputs.publish_type }}" == "Dry Run" ]]; then
|
||||
docker tag $_AZ_REGISTRY/$PROJECT_NAME:latest $_AZ_REGISTRY/$PROJECT_NAME:dryrun
|
||||
docker tag "$_AZ_REGISTRY/$PROJECT_NAME:latest" "$_AZ_REGISTRY/$PROJECT_NAME:dryrun"
|
||||
else
|
||||
docker tag $_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $_AZ_REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION
|
||||
docker tag $_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $_AZ_REGISTRY/$PROJECT_NAME:latest
|
||||
docker tag "$_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME" "$_AZ_REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION"
|
||||
docker tag "$_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME" "$_AZ_REGISTRY/$PROJECT_NAME:latest"
|
||||
fi
|
||||
|
||||
- name: Push version and latest image
|
||||
@ -151,10 +156,10 @@ jobs:
|
||||
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
|
||||
run: |
|
||||
if [[ "${{ inputs.publish_type }}" == "Dry Run" ]]; then
|
||||
docker push $_AZ_REGISTRY/$PROJECT_NAME:dryrun
|
||||
docker push "$_AZ_REGISTRY/$PROJECT_NAME:dryrun"
|
||||
else
|
||||
docker push $_AZ_REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION
|
||||
docker push $_AZ_REGISTRY/$PROJECT_NAME:latest
|
||||
docker push "$_AZ_REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION"
|
||||
docker push "$_AZ_REGISTRY/$PROJECT_NAME:latest"
|
||||
fi
|
||||
|
||||
- name: Log out of Docker
|
||||
|
||||
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
@ -40,6 +40,9 @@ jobs:
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check release version
|
||||
id: version
|
||||
@ -52,8 +55,8 @@ jobs:
|
||||
- name: Get branch name
|
||||
id: branch
|
||||
run: |
|
||||
BRANCH_NAME=$(basename ${{ github.ref }})
|
||||
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
BRANCH_NAME=$(basename "${GITHUB_REF}")
|
||||
echo "branch-name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
release:
|
||||
name: Create GitHub release
|
||||
@ -86,7 +89,7 @@ jobs:
|
||||
|
||||
- name: Create release
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0
|
||||
uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1.16.0
|
||||
with:
|
||||
artifacts: "docker-stub-US.zip,
|
||||
docker-stub-EU.zip,
|
||||
|
||||
50
.github/workflows/repository-management.yml
vendored
50
.github/workflows/repository-management.yml
vendored
@ -22,7 +22,9 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions: {}
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
@ -44,7 +46,7 @@ jobs:
|
||||
BRANCH="hotfix-rc"
|
||||
fi
|
||||
|
||||
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
||||
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
bump_version:
|
||||
name: Bump Version
|
||||
@ -82,7 +84,7 @@ jobs:
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
@ -93,6 +95,7 @@ jobs:
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
persist-credentials: true
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
@ -108,7 +111,7 @@ jobs:
|
||||
id: current-version
|
||||
run: |
|
||||
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
||||
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Verify input version
|
||||
if: ${{ inputs.version_number_override != '' }}
|
||||
@ -118,16 +121,15 @@ jobs:
|
||||
run: |
|
||||
# Error if version has not changed.
|
||||
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
||||
echo "Specified override version is the same as the current version." >> $GITHUB_STEP_SUMMARY
|
||||
echo "Specified override version is the same as the current version." >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if version is newer.
|
||||
printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V
|
||||
if [ $? -eq 0 ]; then
|
||||
if printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V; then
|
||||
echo "Version is newer than the current version."
|
||||
else
|
||||
echo "Version is older than the current version." >> $GITHUB_STEP_SUMMARY
|
||||
echo "Version is older than the current version." >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -158,15 +160,20 @@ jobs:
|
||||
id: set-final-version-output
|
||||
env:
|
||||
VERSION: ${{ inputs.version_number_override }}
|
||||
BUMP_VERSION_OVERRIDE_OUTCOME: ${{ steps.bump-version-override.outcome }}
|
||||
BUMP_VERSION_AUTOMATIC_OUTCOME: ${{ steps.bump-version-automatic.outcome }}
|
||||
CALCULATE_NEXT_VERSION: ${{ steps.calculate-next-version.outputs.version }}
|
||||
run: |
|
||||
if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ steps.bump-version-automatic.outcome }}" = "success" ]]; then
|
||||
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
if [[ "${BUMP_VERSION_OVERRIDE_OUTCOME}" = "success" ]]; then
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
elif [[ "${BUMP_VERSION_AUTOMATIC_OUTCOME}" = "success" ]]; then
|
||||
echo "version=${CALCULATE_NEXT_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Commit files
|
||||
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
|
||||
env:
|
||||
FINAL_VERSION: ${{ steps.set-final-version-output.outputs.version }}
|
||||
run: git commit -m "Bumped version to $FINAL_VERSION" -a
|
||||
|
||||
- name: Push changes
|
||||
run: git push
|
||||
@ -200,7 +207,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
@ -211,13 +218,15 @@ jobs:
|
||||
with:
|
||||
ref: ${{ inputs.target_ref }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
persist-credentials: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if ${{ needs.setup.outputs.branch }} branch exists
|
||||
env:
|
||||
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||
run: |
|
||||
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
|
||||
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
|
||||
if [[ $(git ls-remote --heads origin "$BRANCH_NAME") ]]; then
|
||||
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -225,11 +234,16 @@ jobs:
|
||||
env:
|
||||
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||
run: |
|
||||
git switch --quiet --create $BRANCH_NAME
|
||||
git push --quiet --set-upstream origin $BRANCH_NAME
|
||||
git switch --quiet --create "$BRANCH_NAME"
|
||||
git push --quiet --set-upstream origin "$BRANCH_NAME"
|
||||
|
||||
move_edd_db_scripts:
|
||||
name: Move EDD database scripts
|
||||
needs: cut_branch
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
uses: ./.github/workflows/_move_edd_db_scripts.yml
|
||||
secrets: inherit
|
||||
|
||||
28
.github/workflows/respond.yml
vendored
Normal file
28
.github/workflows/respond.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: Respond
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
respond:
|
||||
name: Respond
|
||||
uses: bitwarden/gh-actions/.github/workflows/_respond.yml@main
|
||||
secrets:
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
id-token: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
21
.github/workflows/review-code.yml
vendored
Normal file
21
.github/workflows/review-code.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
name: Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
review:
|
||||
name: Review
|
||||
uses: bitwarden/gh-actions/.github/workflows/_review-code.yml@main
|
||||
secrets:
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
113
.github/workflows/scan.yml
vendored
113
.github/workflows/scan.yml
vendored
@ -16,6 +16,8 @@ on:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check-run:
|
||||
name: Check PR run
|
||||
@ -24,113 +26,30 @@ jobs:
|
||||
contents: read
|
||||
|
||||
sast:
|
||||
name: SAST scan
|
||||
runs-on: ubuntu-22.04
|
||||
name: Checkmarx
|
||||
uses: bitwarden/gh-actions/.github/workflows/_checkmarx.yml@main
|
||||
needs: check-run
|
||||
secrets:
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "CHECKMARX-TENANT,CHECKMARX-CLIENT-ID,CHECKMARX-SECRET"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
|
||||
env:
|
||||
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
|
||||
with:
|
||||
project_name: ${{ github.repository }}
|
||||
cx_tenant: ${{ steps.get-kv-secrets.outputs.CHECKMARX-TENANT }}
|
||||
base_uri: https://ast.checkmarx.net/
|
||||
cx_client_id: ${{ steps.get-kv-secrets.outputs.CHECKMARX-CLIENT-ID }}
|
||||
cx_client_secret: ${{ steps.get-kv-secrets.outputs.CHECKMARX-SECRET }}
|
||||
additional_params: |
|
||||
--report-format sarif \
|
||||
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
|
||||
--output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
||||
|
||||
quality:
|
||||
name: Quality scan
|
||||
runs-on: ubuntu-22.04
|
||||
name: Sonar
|
||||
uses: bitwarden/gh-actions/.github/workflows/_sonar.yml@main
|
||||
needs: check-run
|
||||
secrets:
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: "zulu"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Install SonarCloud scanner
|
||||
run: dotnet tool install dotnet-sonarscanner -g
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "SONAR-TOKEN"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
env:
|
||||
SONAR_TOKEN: ${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }}
|
||||
run: |
|
||||
dotnet-sonarscanner begin /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" \
|
||||
/d:sonar.test.inclusions=test/,bitwarden_license/test/ \
|
||||
/d:sonar.exclusions=test/,bitwarden_license/test/ \
|
||||
/o:"${{ github.repository_owner }}" /d:sonar.token="${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }}" \
|
||||
/d:sonar.host.url="https://sonarcloud.io" ${{ contains(github.event_name, 'pull_request') && format('/d:sonar.pullrequest.key={0}', github.event.pull_request.number) || '' }}
|
||||
dotnet build
|
||||
dotnet-sonarscanner end /d:sonar.token="${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }}"
|
||||
with:
|
||||
sonar-config: "dotnet"
|
||||
|
||||
53
.github/workflows/test-database.yml
vendored
53
.github/workflows/test-database.yml
vendored
@ -45,9 +45,11 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
|
||||
- name: Restore tools
|
||||
run: dotnet tool restore
|
||||
@ -60,7 +62,7 @@ jobs:
|
||||
docker compose --profile mssql --profile postgres --profile mysql up -d
|
||||
shell: pwsh
|
||||
|
||||
- name: Add MariaDB for unified
|
||||
- name: Add MariaDB for Bitwarden lite
|
||||
# Use a different port than MySQL
|
||||
run: |
|
||||
docker run --detach --name mariadb --env MARIADB_ROOT_PASSWORD=mariadb-password -p 4306:3306 mariadb:10
|
||||
@ -131,7 +133,7 @@ jobs:
|
||||
# Default Sqlite
|
||||
BW_TEST_DATABASES__3__TYPE: "Sqlite"
|
||||
BW_TEST_DATABASES__3__CONNECTIONSTRING: "Data Source=${{ runner.temp }}/test.db"
|
||||
# Unified MariaDB
|
||||
# Bitwarden lite MariaDB
|
||||
BW_TEST_DATABASES__4__TYPE: "MySql"
|
||||
BW_TEST_DATABASES__4__CONNECTIONSTRING: "server=localhost;port=4306;uid=root;pwd=mariadb-password;database=vault_dev;Allow User Variables=true"
|
||||
run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
||||
@ -139,31 +141,31 @@ jobs:
|
||||
|
||||
- name: Print MySQL Logs
|
||||
if: failure()
|
||||
run: 'docker logs $(docker ps --quiet --filter "name=mysql")'
|
||||
run: 'docker logs "$(docker ps --quiet --filter "name=mysql")"'
|
||||
|
||||
- name: Print MariaDB Logs
|
||||
if: failure()
|
||||
run: 'docker logs $(docker ps --quiet --filter "name=mariadb")'
|
||||
run: 'docker logs "$(docker ps --quiet --filter "name=mariadb")"'
|
||||
|
||||
- name: Print Postgres Logs
|
||||
if: failure()
|
||||
run: 'docker logs $(docker ps --quiet --filter "name=postgres")'
|
||||
run: 'docker logs "$(docker ps --quiet --filter "name=postgres")"'
|
||||
|
||||
- name: Print MSSQL Logs
|
||||
if: failure()
|
||||
run: 'docker logs $(docker ps --quiet --filter "name=mssql")'
|
||||
run: 'docker logs "$(docker ps --quiet --filter "name=mssql")"'
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0
|
||||
uses: dorny/test-reporter@890a17cecf52a379fc869ab770a71657660be727 # v2.1.0
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
path: "**/*-test-results.trx"
|
||||
path: "./**/*-test-results.trx"
|
||||
reporter: dotnet-trx
|
||||
fail-on-error: true
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
|
||||
- name: Docker Compose down
|
||||
if: always()
|
||||
@ -177,9 +179,11 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
@ -193,7 +197,7 @@ jobs:
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload DACPAC
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: sql.dacpac
|
||||
path: Sql.dacpac
|
||||
@ -219,7 +223,7 @@ jobs:
|
||||
shell: pwsh
|
||||
|
||||
- name: Report validation results
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: report.xml
|
||||
path: |
|
||||
@ -258,3 +262,26 @@ jobs:
|
||||
working-directory: "dev"
|
||||
run: docker compose down
|
||||
shell: pwsh
|
||||
|
||||
validate-migration-naming:
|
||||
name: Validate new migration naming and order
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate new migrations for pull request
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
git fetch origin main:main
|
||||
pwsh dev/verify_migrations.ps1 -BaseRef main
|
||||
shell: pwsh
|
||||
|
||||
- name: Validate new migrations for push
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
run: pwsh dev/verify_migrations.ps1 -BaseRef HEAD~1
|
||||
shell: pwsh
|
||||
|
||||
16
.github/workflows/test.yml
vendored
16
.github/workflows/test.yml
vendored
@ -28,9 +28,19 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
|
||||
|
||||
- name: Install rust
|
||||
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2.7.7
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
@ -49,7 +59,7 @@ jobs:
|
||||
run: dotnet test ./bitwarden_license/test --configuration Debug --logger "trx;LogFileName=bw-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0
|
||||
uses: dorny/test-reporter@890a17cecf52a379fc869ab770a71657660be727 # v2.1.0
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
@ -58,4 +68,4 @@ jobs:
|
||||
fail-on-error: true
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@ -215,6 +215,9 @@ bitwarden_license/src/Sso/wwwroot/assets
|
||||
**/**.swp
|
||||
.mono
|
||||
src/Core/MailTemplates/Mjml/out
|
||||
src/Core/MailTemplates/Mjml/out-hbs
|
||||
NativeMethods.g.cs
|
||||
util/RustSdk/rust/target
|
||||
|
||||
src/Admin/Admin.zip
|
||||
src/Api/Api.zip
|
||||
@ -226,3 +229,11 @@ src/Notifications/Notifications.zip
|
||||
bitwarden_license/src/Portal/Portal.zip
|
||||
bitwarden_license/src/Sso/Sso.zip
|
||||
**/src/**/flags.json
|
||||
|
||||
# Generated swagger specs
|
||||
/identity.json
|
||||
/api.json
|
||||
/api.public.json
|
||||
|
||||
# Serena
|
||||
.serena/
|
||||
|
||||
@ -3,69 +3,31 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2025.7.2</Version>
|
||||
<Version>2025.12.0</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<!-- Treat it as a test project if the project hasn't set their own value and it follows our test project conventions -->
|
||||
<IsTestProject Condition="'$(IsTestProject)' == '' and ($(MSBuildProjectName.EndsWith('.Test')) or $(MSBuildProjectName.EndsWith('.IntegrationTest')))">true</IsTestProject>
|
||||
<Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' == 'true'">annotations</Nullable>
|
||||
<Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' != 'true'">enable</Nullable>
|
||||
<TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == ''">true</TreatWarningsAsErrors>
|
||||
</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.8.0</MicrosoftNetTestSdkVersion>
|
||||
<!--
|
||||
NuGet: https://www.nuget.org/packages/xunit
|
||||
-->
|
||||
|
||||
<MicrosoftNetTestSdkVersion>18.0.1</MicrosoftNetTestSdkVersion>
|
||||
|
||||
<XUnitVersion>2.6.6</XUnitVersion>
|
||||
<!--
|
||||
NuGet: https://www.nuget.org/packages/xunit.runner.visualstudio
|
||||
-->
|
||||
|
||||
<XUnitRunnerVisualStudioVersion>2.5.6</XUnitRunnerVisualStudioVersion>
|
||||
<!--
|
||||
NuGet: https://www.nuget.org/packages/coverlet.collector
|
||||
-->
|
||||
|
||||
<CoverletCollectorVersion>6.0.0</CoverletCollectorVersion>
|
||||
<!--
|
||||
NuGet: https://www.nuget.org/packages/NSubstitute
|
||||
-->
|
||||
|
||||
<NSubstituteVersion>5.1.0</NSubstituteVersion>
|
||||
<!--
|
||||
NuGet: https://www.nuget.org/packages/AutoFixture.Xunit2
|
||||
-->
|
||||
|
||||
<AutoFixtureXUnit2Version>4.18.1</AutoFixtureXUnit2Version>
|
||||
<!--
|
||||
NuGet: https://www.nuget.org/packages/AutoFixture.AutoNSubstitute
|
||||
-->
|
||||
|
||||
<AutoFixtureAutoNSubstituteVersion>4.18.1</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>
|
||||
@ -133,6 +133,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seeder", "util\Seeder\Seede
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RustSdk", "util\RustSdk\RustSdk.csproj", "{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedWeb.Test", "test\SharedWeb.Test\SharedWeb.Test.csproj", "{AD59537D-5259-4B7A-948F-0CF58E80B359}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SSO.Test", "bitwarden_license\test\SSO.Test\SSO.Test.csproj", "{7D98784C-C253-43FB-9873-25B65C6250D6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -337,6 +342,18 @@ Global
|
||||
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7D98784C-C253-43FB-9873-25B65C6250D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7D98784C-C253-43FB-9873-25B65C6250D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7D98784C-C253-43FB-9873-25B65C6250D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7D98784C-C253-43FB-9873-25B65C6250D6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -391,6 +408,9 @@ Global
|
||||
{3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||
{17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||
{AD59537D-5259-4B7A-948F-0CF58E80B359} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{7D98784C-C253-43FB-9873-25B65C6250D6} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
@ -137,20 +136,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }]
|
||||
};
|
||||
|
||||
var setNonUSBusinessUseToReverseCharge = _featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||
|
||||
if (setNonUSBusinessUseToReverseCharge)
|
||||
{
|
||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
else if (customer.HasRecognizedTaxLocation())
|
||||
{
|
||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||
{
|
||||
Enabled = customer.Address.Country == "US" ||
|
||||
customer.TaxIds.Any()
|
||||
};
|
||||
}
|
||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
|
||||
var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||
|
||||
@ -162,22 +148,30 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
}
|
||||
else if (organization.IsStripeEnabled())
|
||||
{
|
||||
var subscription = await _stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId);
|
||||
var subscription = await _stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId, new SubscriptionGetOptions
|
||||
{
|
||||
Expand = ["customer"]
|
||||
});
|
||||
|
||||
if (subscription.Status is StripeConstants.SubscriptionStatus.Canceled or StripeConstants.SubscriptionStatus.IncompleteExpired)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||
await _stripeAdapter.CustomerUpdateAsync(subscription.CustomerId, new CustomerUpdateOptions
|
||||
{
|
||||
Coupon = string.Empty,
|
||||
Email = organization.BillingEmail
|
||||
});
|
||||
|
||||
if (subscription.Customer.Discount?.Coupon != null)
|
||||
{
|
||||
await _stripeAdapter.CustomerDeleteDiscountAsync(subscription.CustomerId);
|
||||
}
|
||||
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, new SubscriptionUpdateOptions
|
||||
{
|
||||
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
|
||||
DaysUntilDue = 30
|
||||
DaysUntilDue = 30,
|
||||
});
|
||||
|
||||
await _subscriberService.RemovePaymentSource(organization);
|
||||
|
||||
@ -12,7 +12,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Context;
|
||||
@ -35,8 +35,9 @@ public class ProviderService : IProviderService
|
||||
{
|
||||
private static readonly PlanType[] _resellerDisallowedOrganizationTypes = [
|
||||
PlanType.Free,
|
||||
PlanType.FamiliesAnnually,
|
||||
PlanType.FamiliesAnnually2019
|
||||
PlanType.FamiliesAnnually2025,
|
||||
PlanType.FamiliesAnnually2019,
|
||||
PlanType.FamiliesAnnually
|
||||
];
|
||||
|
||||
private readonly IDataProtector _dataProtector;
|
||||
@ -90,7 +91,7 @@ public class ProviderService : IProviderService
|
||||
_providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand;
|
||||
}
|
||||
|
||||
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null)
|
||||
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress)
|
||||
{
|
||||
var owner = await _userService.GetUserByIdAsync(ownerUserId);
|
||||
if (owner == null)
|
||||
@ -115,24 +116,7 @@ public class ProviderService : IProviderService
|
||||
throw new BadRequestException("Invalid owner.");
|
||||
}
|
||||
|
||||
if (taxInfo == null || string.IsNullOrEmpty(taxInfo.BillingAddressCountry) || string.IsNullOrEmpty(taxInfo.BillingAddressPostalCode))
|
||||
{
|
||||
throw new BadRequestException("Both address and postal code are required to set up your provider.");
|
||||
}
|
||||
|
||||
var requireProviderPaymentMethodDuringSetup =
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup);
|
||||
|
||||
if (requireProviderPaymentMethodDuringSetup && tokenizedPaymentSource is not
|
||||
{
|
||||
Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal,
|
||||
Token: not null and not ""
|
||||
})
|
||||
{
|
||||
throw new BadRequestException("A payment method is required to set up your provider.");
|
||||
}
|
||||
|
||||
var customer = await _providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
|
||||
var customer = await _providerBillingService.SetupCustomer(provider, paymentMethod, billingAddress);
|
||||
provider.GatewayCustomerId = customer.Id;
|
||||
var subscription = await _providerBillingService.SetupSubscription(provider);
|
||||
provider.GatewaySubscriptionId = subscription.Id;
|
||||
|
||||
@ -0,0 +1,107 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Providers.Models;
|
||||
using Bit.Core.Billing.Providers.Queries;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Services;
|
||||
using Stripe;
|
||||
using Stripe.Tax;
|
||||
|
||||
namespace Bit.Commercial.Core.Billing.Providers.Queries;
|
||||
|
||||
using static Bit.Core.Constants;
|
||||
using static StripeConstants;
|
||||
using SuspensionWarning = ProviderWarnings.SuspensionWarning;
|
||||
using TaxIdWarning = ProviderWarnings.TaxIdWarning;
|
||||
|
||||
public class GetProviderWarningsQuery(
|
||||
ICurrentContext currentContext,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ISubscriberService subscriberService) : IGetProviderWarningsQuery
|
||||
{
|
||||
public async Task<ProviderWarnings?> Run(Provider provider)
|
||||
{
|
||||
var warnings = new ProviderWarnings();
|
||||
|
||||
var subscription =
|
||||
await subscriberService.GetSubscription(provider,
|
||||
new SubscriptionGetOptions { Expand = ["customer.tax_ids"] });
|
||||
|
||||
if (subscription == null)
|
||||
{
|
||||
return warnings;
|
||||
}
|
||||
|
||||
warnings.Suspension = GetSuspensionWarning(provider, subscription);
|
||||
|
||||
warnings.TaxId = await GetTaxIdWarningAsync(provider, subscription.Customer);
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
private SuspensionWarning? GetSuspensionWarning(
|
||||
Provider provider,
|
||||
Subscription subscription)
|
||||
{
|
||||
if (provider.Enabled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return subscription.Status switch
|
||||
{
|
||||
SubscriptionStatus.Unpaid => currentContext.ProviderProviderAdmin(provider.Id)
|
||||
? new SuspensionWarning { Resolution = "add_payment_method", SubscriptionCancelsAt = subscription.CancelAt }
|
||||
: new SuspensionWarning { Resolution = "contact_administrator" },
|
||||
_ => new SuspensionWarning { Resolution = "contact_support" }
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<TaxIdWarning?> GetTaxIdWarningAsync(
|
||||
Provider provider,
|
||||
Customer customer)
|
||||
{
|
||||
if (customer.Address?.Country == CountryAbbreviations.UnitedStates)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!currentContext.ProviderProviderAdmin(provider.Id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: Potentially DRY this out with the GetOrganizationWarningsQuery
|
||||
|
||||
// Get active and scheduled registrations
|
||||
var registrations = (await Task.WhenAll(
|
||||
stripeAdapter.TaxRegistrationsListAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Active }),
|
||||
stripeAdapter.TaxRegistrationsListAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Scheduled })))
|
||||
.SelectMany(registrations => registrations.Data);
|
||||
|
||||
// Find the matching registration for the customer
|
||||
var registration = registrations.FirstOrDefault(registration => registration.Country == customer.Address?.Country);
|
||||
|
||||
// If we're not registered in their country, we don't need a warning
|
||||
if (registration == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var taxId = customer.TaxIds.FirstOrDefault();
|
||||
|
||||
return taxId switch
|
||||
{
|
||||
// Customer's tax ID is missing
|
||||
null => new TaxIdWarning { Type = "tax_id_missing" },
|
||||
// Not sure if this case is valid, but Stripe says this property is nullable
|
||||
not null when taxId.Verification == null => null,
|
||||
// Customer's tax ID is pending verification
|
||||
not null when taxId.Verification.Status == TaxIdVerificationStatus.Pending => new TaxIdWarning { Type = "tax_id_pending_verification" },
|
||||
// Customer's tax ID failed verification
|
||||
not null when taxId.Verification.Status == TaxIdVerificationStatus.Unverified => new TaxIdWarning { Type = "tax_id_failed_verification" },
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Entities;
|
||||
using Bit.Core.Billing.Providers.Models;
|
||||
@ -21,10 +22,8 @@ using Bit.Core.Billing.Providers.Repositories;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -38,10 +37,12 @@ using Subscription = Stripe.Subscription;
|
||||
|
||||
namespace Bit.Commercial.Core.Billing.Providers.Services;
|
||||
|
||||
using static Constants;
|
||||
using static StripeConstants;
|
||||
|
||||
public class ProviderBillingService(
|
||||
IBraintreeGateway braintreeGateway,
|
||||
IEventService eventService,
|
||||
IFeatureService featureService,
|
||||
IGlobalSettings globalSettings,
|
||||
ILogger<ProviderBillingService> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -52,8 +53,7 @@ public class ProviderBillingService(
|
||||
IProviderUserRepository providerUserRepository,
|
||||
ISetupIntentCache setupIntentCache,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ISubscriberService subscriberService,
|
||||
ITaxService taxService)
|
||||
ISubscriberService subscriberService)
|
||||
: IProviderBillingService
|
||||
{
|
||||
public async Task AddExistingOrganization(
|
||||
@ -62,10 +62,7 @@ public class ProviderBillingService(
|
||||
string key)
|
||||
{
|
||||
await stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
|
||||
new SubscriptionUpdateOptions
|
||||
{
|
||||
CancelAtPeriodEnd = false
|
||||
});
|
||||
new SubscriptionUpdateOptions { CancelAtPeriodEnd = false });
|
||||
|
||||
var subscription =
|
||||
await stripeAdapter.SubscriptionCancelAsync(organization.GatewaySubscriptionId,
|
||||
@ -84,7 +81,7 @@ public class ProviderBillingService(
|
||||
|
||||
var wasTrialing = subscription.TrialEnd.HasValue && subscription.TrialEnd.Value > now;
|
||||
|
||||
if (!wasTrialing && subscription.LatestInvoice.Status == StripeConstants.InvoiceStatus.Draft)
|
||||
if (!wasTrialing && subscription.LatestInvoice.Status == InvoiceStatus.Draft)
|
||||
{
|
||||
await stripeAdapter.InvoiceFinalizeInvoiceAsync(subscription.LatestInvoiceId,
|
||||
new InvoiceFinalizeOptions { AutoAdvance = true });
|
||||
@ -185,16 +182,8 @@ public class ProviderBillingService(
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new SubscriptionItemOptions
|
||||
{
|
||||
Price = newPriceId,
|
||||
Quantity = oldSubscriptionItem!.Quantity
|
||||
},
|
||||
new SubscriptionItemOptions
|
||||
{
|
||||
Id = oldSubscriptionItem.Id,
|
||||
Deleted = true
|
||||
}
|
||||
new SubscriptionItemOptions { Price = newPriceId, Quantity = oldSubscriptionItem!.Quantity },
|
||||
new SubscriptionItemOptions { Id = oldSubscriptionItem.Id, Deleted = true }
|
||||
]
|
||||
};
|
||||
|
||||
@ -203,7 +192,8 @@ public class ProviderBillingService(
|
||||
// Refactor later to ?ChangeClientPlanCommand? (ProviderPlanId, ProviderId, OrganizationId)
|
||||
// 1. Retrieve PlanType and PlanName for ProviderPlan
|
||||
// 2. Assign PlanType & PlanName to Organization
|
||||
var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerPlan.ProviderId);
|
||||
var providerOrganizations =
|
||||
await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerPlan.ProviderId);
|
||||
|
||||
var newPlan = await pricingClient.GetPlanOrThrow(newPlanType);
|
||||
|
||||
@ -214,6 +204,7 @@ public class ProviderBillingService(
|
||||
{
|
||||
throw new ConflictException($"Organization '{providerOrganization.Id}' not found.");
|
||||
}
|
||||
|
||||
organization.PlanType = newPlanType;
|
||||
organization.Plan = newPlan.Name;
|
||||
await organizationRepository.ReplaceAsync(organization);
|
||||
@ -229,15 +220,15 @@ public class ProviderBillingService(
|
||||
|
||||
if (!string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||
{
|
||||
logger.LogWarning("Client organization ({ID}) already has a populated {FieldName}", organization.Id, nameof(organization.GatewayCustomerId));
|
||||
logger.LogWarning("Client organization ({ID}) already has a populated {FieldName}", organization.Id,
|
||||
nameof(organization.GatewayCustomerId));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions
|
||||
{
|
||||
Expand = ["tax", "tax_ids"]
|
||||
});
|
||||
var providerCustomer =
|
||||
await subscriberService.GetCustomerOrThrow(provider,
|
||||
new CustomerGetOptions { Expand = ["tax", "tax_ids"] });
|
||||
|
||||
var providerTaxId = providerCustomer.TaxIds.FirstOrDefault();
|
||||
|
||||
@ -270,25 +261,18 @@ public class ProviderBillingService(
|
||||
}
|
||||
]
|
||||
},
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "region", globalSettings.BaseServiceUri.CloudRegion }
|
||||
},
|
||||
TaxIdData = providerTaxId == null ? null :
|
||||
[
|
||||
new CustomerTaxIdDataOptions
|
||||
{
|
||||
Type = providerTaxId.Type,
|
||||
Value = providerTaxId.Value
|
||||
}
|
||||
]
|
||||
Metadata = new Dictionary<string, string> { { "region", globalSettings.BaseServiceUri.CloudRegion } },
|
||||
TaxIdData = providerTaxId == null
|
||||
? null
|
||||
:
|
||||
[
|
||||
new CustomerTaxIdDataOptions { Type = providerTaxId.Type, Value = providerTaxId.Value }
|
||||
]
|
||||
};
|
||||
|
||||
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||
|
||||
if (setNonUSBusinessUseToReverseCharge && providerCustomer.Address is not { Country: "US" })
|
||||
if (providerCustomer.Address is not { Country: CountryAbbreviations.UnitedStates })
|
||||
{
|
||||
customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
||||
customerCreateOptions.TaxExempt = TaxExempt.Reverse;
|
||||
}
|
||||
|
||||
var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||
@ -350,9 +334,9 @@ public class ProviderBillingService(
|
||||
.Where(pair => pair.subscription is
|
||||
{
|
||||
Status:
|
||||
StripeConstants.SubscriptionStatus.Active or
|
||||
StripeConstants.SubscriptionStatus.Trialing or
|
||||
StripeConstants.SubscriptionStatus.PastDue
|
||||
SubscriptionStatus.Active or
|
||||
SubscriptionStatus.Trialing or
|
||||
SubscriptionStatus.PastDue
|
||||
}).ToList();
|
||||
|
||||
if (active.Count == 0)
|
||||
@ -477,34 +461,25 @@ public class ProviderBillingService(
|
||||
// Below the limit to above the limit
|
||||
(currentlyAssignedSeatTotal <= seatMinimum && newlyAssignedSeatTotal > seatMinimum) ||
|
||||
// Above the limit to further above the limit
|
||||
(currentlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > currentlyAssignedSeatTotal);
|
||||
(currentlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > seatMinimum &&
|
||||
newlyAssignedSeatTotal > currentlyAssignedSeatTotal);
|
||||
}
|
||||
|
||||
public async Task<Customer> SetupCustomer(
|
||||
Provider provider,
|
||||
TaxInfo taxInfo,
|
||||
TokenizedPaymentSource tokenizedPaymentSource = null)
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
if (taxInfo is not
|
||||
{
|
||||
BillingAddressCountry: not null and not "",
|
||||
BillingAddressPostalCode: not null and not ""
|
||||
})
|
||||
{
|
||||
logger.LogError("Cannot create customer for provider ({ProviderID}) without both a country and postal code", provider.Id);
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
var options = new CustomerCreateOptions
|
||||
{
|
||||
Address = new AddressOptions
|
||||
{
|
||||
Country = taxInfo.BillingAddressCountry,
|
||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||
Line1 = taxInfo.BillingAddressLine1,
|
||||
Line2 = taxInfo.BillingAddressLine2,
|
||||
City = taxInfo.BillingAddressCity,
|
||||
State = taxInfo.BillingAddressState
|
||||
Country = billingAddress.Country,
|
||||
PostalCode = billingAddress.PostalCode,
|
||||
Line1 = billingAddress.Line1,
|
||||
Line2 = billingAddress.Line2,
|
||||
City = billingAddress.City,
|
||||
State = billingAddress.State
|
||||
},
|
||||
Description = provider.DisplayBusinessName(),
|
||||
Email = provider.BillingEmail,
|
||||
@ -521,112 +496,71 @@ public class ProviderBillingService(
|
||||
}
|
||||
]
|
||||
},
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "region", globalSettings.BaseServiceUri.CloudRegion }
|
||||
}
|
||||
Metadata = new Dictionary<string, string> { { "region", globalSettings.BaseServiceUri.CloudRegion } },
|
||||
TaxExempt = billingAddress.Country != CountryAbbreviations.UnitedStates ? TaxExempt.Reverse : TaxExempt.None
|
||||
};
|
||||
|
||||
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||
|
||||
if (setNonUSBusinessUseToReverseCharge && taxInfo.BillingAddressCountry != "US")
|
||||
if (billingAddress.TaxId != null)
|
||||
{
|
||||
options.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber))
|
||||
{
|
||||
var taxIdType = taxService.GetStripeTaxCode(
|
||||
taxInfo.BillingAddressCountry,
|
||||
taxInfo.TaxIdNumber);
|
||||
|
||||
if (taxIdType == null)
|
||||
{
|
||||
logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
|
||||
taxInfo.BillingAddressCountry,
|
||||
taxInfo.TaxIdNumber);
|
||||
|
||||
throw new BadRequestException("billingTaxIdTypeInferenceError");
|
||||
}
|
||||
|
||||
options.TaxIdData =
|
||||
[
|
||||
new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber }
|
||||
new CustomerTaxIdDataOptions { Type = billingAddress.TaxId.Code, Value = billingAddress.TaxId.Value }
|
||||
];
|
||||
|
||||
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
|
||||
if (billingAddress.TaxId.Code == TaxIdType.SpanishNIF)
|
||||
{
|
||||
options.TaxIdData.Add(new CustomerTaxIdDataOptions
|
||||
{
|
||||
Type = StripeConstants.TaxIdType.EUVAT,
|
||||
Value = $"ES{taxInfo.TaxIdNumber}"
|
||||
Type = TaxIdType.EUVAT,
|
||||
Value = $"ES{billingAddress.TaxId.Value}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(provider.DiscountId))
|
||||
{
|
||||
options.Coupon = provider.DiscountId;
|
||||
}
|
||||
|
||||
var requireProviderPaymentMethodDuringSetup =
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup);
|
||||
|
||||
var braintreeCustomerId = "";
|
||||
|
||||
if (requireProviderPaymentMethodDuringSetup)
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (paymentMethod.Type)
|
||||
{
|
||||
if (tokenizedPaymentSource is not
|
||||
case TokenizablePaymentMethodType.BankAccount:
|
||||
{
|
||||
Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal,
|
||||
Token: not null and not ""
|
||||
})
|
||||
{
|
||||
logger.LogError("Cannot create customer for provider ({ProviderID}) without a payment method", provider.Id);
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
var (type, token) = tokenizedPaymentSource;
|
||||
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (type)
|
||||
{
|
||||
case PaymentMethodType.BankAccount:
|
||||
{
|
||||
var setupIntent =
|
||||
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = token }))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (setupIntent == null)
|
||||
var setupIntent =
|
||||
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions
|
||||
{
|
||||
logger.LogError("Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account", provider.Id);
|
||||
throw new BillingException();
|
||||
}
|
||||
PaymentMethod = paymentMethod.Token
|
||||
}))
|
||||
.FirstOrDefault();
|
||||
|
||||
await setupIntentCache.Set(provider.Id, setupIntent.Id);
|
||||
break;
|
||||
}
|
||||
case PaymentMethodType.Card:
|
||||
if (setupIntent == null)
|
||||
{
|
||||
options.PaymentMethod = token;
|
||||
options.InvoiceSettings.DefaultPaymentMethod = token;
|
||||
break;
|
||||
logger.LogError(
|
||||
"Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account",
|
||||
provider.Id);
|
||||
throw new BillingException();
|
||||
}
|
||||
case PaymentMethodType.PayPal:
|
||||
{
|
||||
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(provider, token);
|
||||
options.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await setupIntentCache.Set(provider.Id, setupIntent.Id);
|
||||
break;
|
||||
}
|
||||
case TokenizablePaymentMethodType.Card:
|
||||
{
|
||||
options.PaymentMethod = paymentMethod.Token;
|
||||
options.InvoiceSettings.DefaultPaymentMethod = paymentMethod.Token;
|
||||
break;
|
||||
}
|
||||
case TokenizablePaymentMethodType.PayPal:
|
||||
{
|
||||
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(provider, paymentMethod.Token);
|
||||
options.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await stripeAdapter.CustomerCreateAsync(options);
|
||||
}
|
||||
catch (StripeException stripeException) when (stripeException.StripeError?.Code ==
|
||||
StripeConstants.ErrorCodes.TaxIdInvalid)
|
||||
catch (StripeException stripeException) when (stripeException.StripeError?.Code == ErrorCodes.TaxIdInvalid)
|
||||
{
|
||||
await Revert();
|
||||
throw new BadRequestException(
|
||||
@ -640,25 +574,22 @@ public class ProviderBillingService(
|
||||
|
||||
async Task Revert()
|
||||
{
|
||||
if (requireProviderPaymentMethodDuringSetup && tokenizedPaymentSource != null)
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (paymentMethod.Type)
|
||||
{
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (tokenizedPaymentSource.Type)
|
||||
{
|
||||
case PaymentMethodType.BankAccount:
|
||||
{
|
||||
var setupIntentId = await setupIntentCache.Get(provider.Id);
|
||||
await stripeAdapter.SetupIntentCancel(setupIntentId,
|
||||
new SetupIntentCancelOptions { CancellationReason = "abandoned" });
|
||||
await setupIntentCache.Remove(provider.Id);
|
||||
break;
|
||||
}
|
||||
case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
|
||||
{
|
||||
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
case TokenizablePaymentMethodType.BankAccount:
|
||||
{
|
||||
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id);
|
||||
await stripeAdapter.SetupIntentCancel(setupIntentId,
|
||||
new SetupIntentCancelOptions { CancellationReason = "abandoned" });
|
||||
await setupIntentCache.RemoveSetupIntentForSubscriber(provider.Id);
|
||||
break;
|
||||
}
|
||||
case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
|
||||
{
|
||||
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -673,9 +604,10 @@ public class ProviderBillingService(
|
||||
|
||||
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
||||
|
||||
if (providerPlans == null || providerPlans.Count == 0)
|
||||
if (providerPlans.Count == 0)
|
||||
{
|
||||
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured plans", provider.Id);
|
||||
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured plans",
|
||||
provider.Id);
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
@ -688,7 +620,9 @@ public class ProviderBillingService(
|
||||
|
||||
if (!providerPlan.IsConfigured())
|
||||
{
|
||||
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan", provider.Id, plan.Name);
|
||||
logger.LogError(
|
||||
"Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan",
|
||||
provider.Id, plan.Name);
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
@ -701,23 +635,17 @@ public class ProviderBillingService(
|
||||
});
|
||||
}
|
||||
|
||||
var requireProviderPaymentMethodDuringSetup =
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup);
|
||||
|
||||
var setupIntentId = await setupIntentCache.Get(provider.Id);
|
||||
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id);
|
||||
|
||||
var setupIntent = !string.IsNullOrEmpty(setupIntentId)
|
||||
? await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions
|
||||
{
|
||||
Expand = ["payment_method"]
|
||||
})
|
||||
? await stripeAdapter.SetupIntentGet(setupIntentId,
|
||||
new SetupIntentGetOptions { Expand = ["payment_method"] })
|
||||
: null;
|
||||
|
||||
var usePaymentMethod =
|
||||
requireProviderPaymentMethodDuringSetup &&
|
||||
(!string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) ||
|
||||
customer.Metadata.ContainsKey(BraintreeCustomerIdKey) ||
|
||||
setupIntent.IsUnverifiedBankAccount());
|
||||
!string.IsNullOrEmpty(customer.InvoiceSettings?.DefaultPaymentMethodId) ||
|
||||
customer.Metadata?.ContainsKey(BraintreeCustomerIdKey) == true ||
|
||||
setupIntent?.IsUnverifiedBankAccount() == true;
|
||||
|
||||
int? trialPeriodDays = provider.Type switch
|
||||
{
|
||||
@ -728,43 +656,28 @@ public class ProviderBillingService(
|
||||
|
||||
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
||||
{
|
||||
CollectionMethod = usePaymentMethod ?
|
||||
StripeConstants.CollectionMethod.ChargeAutomatically : StripeConstants.CollectionMethod.SendInvoice,
|
||||
CollectionMethod =
|
||||
usePaymentMethod
|
||||
? CollectionMethod.ChargeAutomatically
|
||||
: CollectionMethod.SendInvoice,
|
||||
Customer = customer.Id,
|
||||
DaysUntilDue = usePaymentMethod ? null : 30,
|
||||
Discounts = !string.IsNullOrEmpty(provider.DiscountId) ? [new SubscriptionDiscountOptions { Coupon = provider.DiscountId }] : null,
|
||||
Items = subscriptionItemOptionsList,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "providerId", provider.Id.ToString() }
|
||||
},
|
||||
Metadata = new Dictionary<string, string> { { "providerId", provider.Id.ToString() } },
|
||||
OffSession = true,
|
||||
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
|
||||
TrialPeriodDays = trialPeriodDays
|
||||
ProrationBehavior = ProrationBehavior.CreateProrations,
|
||||
TrialPeriodDays = trialPeriodDays,
|
||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||
};
|
||||
|
||||
var setNonUSBusinessUseToReverseCharge =
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||
|
||||
if (setNonUSBusinessUseToReverseCharge)
|
||||
{
|
||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
else if (customer.HasRecognizedTaxLocation())
|
||||
{
|
||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||
{
|
||||
Enabled = customer.Address.Country == "US" ||
|
||||
customer.TaxIds.Any()
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||
|
||||
if (subscription is
|
||||
{
|
||||
Status: StripeConstants.SubscriptionStatus.Active or StripeConstants.SubscriptionStatus.Trialing
|
||||
Status: SubscriptionStatus.Active or SubscriptionStatus.Trialing
|
||||
})
|
||||
{
|
||||
return subscription;
|
||||
@ -778,9 +691,11 @@ public class ProviderBillingService(
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
catch (StripeException stripeException) when (stripeException.StripeError?.Code == StripeConstants.ErrorCodes.CustomerTaxLocationInvalid)
|
||||
catch (StripeException stripeException) when (stripeException.StripeError?.Code ==
|
||||
ErrorCodes.CustomerTaxLocationInvalid)
|
||||
{
|
||||
throw new BadRequestException("Your location wasn't recognized. Please ensure your country and postal code are valid.");
|
||||
throw new BadRequestException(
|
||||
"Your location wasn't recognized. Please ensure your country and postal code are valid.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -794,7 +709,7 @@ public class ProviderBillingService(
|
||||
subscriberService.UpdateTaxInformation(provider, taxInformation));
|
||||
|
||||
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
||||
new SubscriptionUpdateOptions { CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically });
|
||||
new SubscriptionUpdateOptions { CollectionMethod = CollectionMethod.ChargeAutomatically });
|
||||
}
|
||||
|
||||
public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)
|
||||
@ -894,13 +809,9 @@ public class ProviderBillingService(
|
||||
|
||||
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId, new SubscriptionUpdateOptions
|
||||
{
|
||||
Items = [
|
||||
new SubscriptionItemOptions
|
||||
{
|
||||
Id = item.Id,
|
||||
Price = priceId,
|
||||
Quantity = newlySubscribedSeats
|
||||
}
|
||||
Items =
|
||||
[
|
||||
new SubscriptionItemOptions { Id = item.Id, Price = priceId, Quantity = newlySubscribedSeats }
|
||||
]
|
||||
});
|
||||
|
||||
@ -923,7 +834,8 @@ public class ProviderBillingService(
|
||||
var plan = await pricingClient.GetPlanOrThrow(planType);
|
||||
|
||||
return providerOrganizations
|
||||
.Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed)
|
||||
.Where(providerOrganization => providerOrganization.Plan == plan.Name &&
|
||||
providerOrganization.Status == OrganizationStatusType.Managed)
|
||||
.Sum(providerOrganization => providerOrganization.Seats ?? 0);
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
||||
<PackageReference Include="CsvHelper" Version="33.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
|
||||
|
||||
@ -13,15 +16,21 @@ public class CreateServiceAccountCommand : ICreateServiceAccountCommand
|
||||
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
public CreateServiceAccountCommand(
|
||||
IAccessPolicyRepository accessPolicyRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IServiceAccountRepository serviceAccountRepository)
|
||||
IServiceAccountRepository serviceAccountRepository,
|
||||
IEventService eventService,
|
||||
ICurrentContext currentContext)
|
||||
{
|
||||
_accessPolicyRepository = accessPolicyRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_serviceAccountRepository = serviceAccountRepository;
|
||||
_eventService = eventService;
|
||||
_currentContext = currentContext;
|
||||
}
|
||||
|
||||
public async Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount, Guid userId)
|
||||
@ -38,6 +47,7 @@ public class CreateServiceAccountCommand : ICreateServiceAccountCommand
|
||||
Write = true,
|
||||
};
|
||||
await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> { accessPolicy });
|
||||
await _eventService.LogServiceAccountPeopleEventAsync(user.Id, accessPolicy, EventType.ServiceAccount_UserAdded, _currentContext.IdentityClientType);
|
||||
return createdServiceAccount;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||
using Bit.Commercial.Core.AdminConsole.Services;
|
||||
using Bit.Commercial.Core.Billing.Providers.Queries;
|
||||
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Providers.Queries;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@ -17,5 +19,6 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IRemoveOrganizationFromProviderCommand, RemoveOrganizationFromProviderCommand>();
|
||||
services.AddTransient<IProviderBillingService, ProviderBillingService>();
|
||||
services.AddTransient<IBusinessUnitConverter, BusinessUnitConverter>();
|
||||
services.AddTransient<IGetProviderWarningsQuery, GetProviderWarningsQuery>();
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,7 +28,10 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ProjectPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
|
||||
public async Task<IEnumerable<ProjectPermissionDetails>> GetManyByOrganizationIdAsync(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
AccessClientType accessType)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
@ -45,6 +45,19 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyTrashedSecretsByIds(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)
|
||||
.Include(c => c.Projects)
|
||||
.ToListAsync();
|
||||
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdAsync(
|
||||
Guid organizationId, Guid userId, AccessClientType accessType)
|
||||
{
|
||||
@ -66,10 +79,14 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
||||
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
|
||||
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
AccessClientType accessType)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var query = dbContext.Secret
|
||||
.Include(c => c.Projects)
|
||||
.Where(c => c.OrganizationId == organizationId && c.DeletedDate == null)
|
||||
|
||||
@ -0,0 +1,94 @@
|
||||
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 SecretVersionRepository : Repository<Core.SecretsManager.Entities.SecretVersion, SecretVersion, Guid>, ISecretVersionRepository
|
||||
{
|
||||
public SecretVersionRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
|
||||
: base(serviceScopeFactory, mapper, db => db.SecretVersion)
|
||||
{ }
|
||||
|
||||
public override async Task<Core.SecretsManager.Entities.SecretVersion?> GetByIdAsync(Guid id)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var secretVersion = await dbContext.SecretVersion
|
||||
.Where(sv => sv.Id == id)
|
||||
.FirstOrDefaultAsync();
|
||||
return Mapper.Map<Core.SecretsManager.Entities.SecretVersion>(secretVersion);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.SecretVersion>> GetManyBySecretIdAsync(Guid secretId)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var secretVersions = await dbContext.SecretVersion
|
||||
.Where(sv => sv.SecretId == secretId)
|
||||
.OrderByDescending(sv => sv.VersionDate)
|
||||
.ToListAsync();
|
||||
return Mapper.Map<List<Core.SecretsManager.Entities.SecretVersion>>(secretVersions);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.SecretVersion>> GetManyByIdsAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var versionIds = ids.ToList();
|
||||
var secretVersions = await dbContext.SecretVersion
|
||||
.Where(sv => versionIds.Contains(sv.Id))
|
||||
.OrderByDescending(sv => sv.VersionDate)
|
||||
.ToListAsync();
|
||||
return Mapper.Map<List<Core.SecretsManager.Entities.SecretVersion>>(secretVersions);
|
||||
}
|
||||
|
||||
public override async Task<Core.SecretsManager.Entities.SecretVersion> CreateAsync(Core.SecretsManager.Entities.SecretVersion secretVersion)
|
||||
{
|
||||
const int maxVersionsToKeep = 10;
|
||||
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||
|
||||
// Get the IDs of the most recent (maxVersionsToKeep - 1) versions to keep
|
||||
var versionsToKeepIds = await dbContext.SecretVersion
|
||||
.Where(sv => sv.SecretId == secretVersion.SecretId)
|
||||
.OrderByDescending(sv => sv.VersionDate)
|
||||
.Take(maxVersionsToKeep - 1)
|
||||
.Select(sv => sv.Id)
|
||||
.ToListAsync();
|
||||
|
||||
// Delete all versions for this secret that are not in the "keep" list
|
||||
if (versionsToKeepIds.Any())
|
||||
{
|
||||
await dbContext.SecretVersion
|
||||
.Where(sv => sv.SecretId == secretVersion.SecretId && !versionsToKeepIds.Contains(sv.Id))
|
||||
.ExecuteDeleteAsync();
|
||||
}
|
||||
|
||||
secretVersion.SetNewId();
|
||||
var entity = Mapper.Map<SecretVersion>(secretVersion);
|
||||
|
||||
await dbContext.AddAsync(entity);
|
||||
await dbContext.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
return secretVersion;
|
||||
}
|
||||
|
||||
public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var secretVersionIds = ids.ToList();
|
||||
await dbContext.SecretVersion
|
||||
.Where(sv => secretVersionIds.Contains(sv.Id))
|
||||
.ExecuteDeleteAsync();
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ public static class SecretsManagerEfServiceCollectionExtensions
|
||||
{
|
||||
services.AddSingleton<IAccessPolicyRepository, AccessPolicyRepository>();
|
||||
services.AddSingleton<ISecretRepository, SecretRepository>();
|
||||
services.AddSingleton<ISecretVersionRepository, SecretVersionRepository>();
|
||||
services.AddSingleton<IProjectRepository, ProjectRepository>();
|
||||
services.AddSingleton<IServiceAccountRepository, ServiceAccountRepository>();
|
||||
}
|
||||
|
||||
@ -61,17 +61,15 @@ public class GroupsController : Controller
|
||||
[HttpGet("")]
|
||||
public async Task<IActionResult> Get(
|
||||
Guid organizationId,
|
||||
[FromQuery] string filter,
|
||||
[FromQuery] int? count,
|
||||
[FromQuery] int? startIndex)
|
||||
[FromQuery] GetGroupsQueryParamModel model)
|
||||
{
|
||||
var groupsListQueryResult = await _getGroupsListQuery.GetGroupsListAsync(organizationId, filter, count, startIndex);
|
||||
var groupsListQueryResult = await _getGroupsListQuery.GetGroupsListAsync(organizationId, model);
|
||||
var scimListResponseModel = new ScimListResponseModel<ScimGroupResponseModel>
|
||||
{
|
||||
Resources = groupsListQueryResult.groupList.Select(g => new ScimGroupResponseModel(g)).ToList(),
|
||||
ItemsPerPage = count.GetValueOrDefault(groupsListQueryResult.groupList.Count()),
|
||||
ItemsPerPage = model.Count,
|
||||
TotalResults = groupsListQueryResult.totalResults,
|
||||
StartIndex = startIndex.GetValueOrDefault(1),
|
||||
StartIndex = model.StartIndex,
|
||||
};
|
||||
return Ok(scimListResponseModel);
|
||||
}
|
||||
|
||||
@ -3,10 +3,10 @@
|
||||
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||
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;
|
||||
using Bit.Scim.Utilities;
|
||||
@ -22,29 +22,28 @@ namespace Bit.Scim.Controllers.v2;
|
||||
public class UsersController : Controller
|
||||
{
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IGetUsersListQuery _getUsersListQuery;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IPatchUserCommand _patchUserCommand;
|
||||
private readonly IPostUserCommand _postUserCommand;
|
||||
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||
private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommand;
|
||||
|
||||
public UsersController(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationService organizationService,
|
||||
public UsersController(IOrganizationUserRepository organizationUserRepository,
|
||||
IGetUsersListQuery getUsersListQuery,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IPatchUserCommand patchUserCommand,
|
||||
IPostUserCommand postUserCommand,
|
||||
IRestoreOrganizationUserCommand restoreOrganizationUserCommand)
|
||||
IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
|
||||
IRevokeOrganizationUserCommand revokeOrganizationUserCommand)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationService = organizationService;
|
||||
_getUsersListQuery = getUsersListQuery;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_patchUserCommand = patchUserCommand;
|
||||
_postUserCommand = postUserCommand;
|
||||
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||
_revokeOrganizationUserCommand = revokeOrganizationUserCommand;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -101,7 +100,7 @@ public class UsersController : Controller
|
||||
}
|
||||
else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
await _organizationService.RevokeUserAsync(orgUser, EventSystemUser.SCIM);
|
||||
await _revokeOrganizationUserCommand.RevokeUserAsync(orgUser, EventSystemUser.SCIM);
|
||||
}
|
||||
|
||||
// Have to get full details object for response model
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
###############################################
|
||||
# Build stage #
|
||||
###############################################
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
|
||||
|
||||
# Docker buildx supplies the value for this arg
|
||||
ARG TARGETPLATFORM
|
||||
@ -9,11 +9,11 @@ ARG TARGETPLATFORM
|
||||
# Determine proper runtime value for .NET
|
||||
# We put the value in a file to be read by later layers.
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||
RID=linux-x64 ; \
|
||||
RID=linux-musl-x64 ; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||
RID=linux-arm64 ; \
|
||||
RID=linux-musl-arm64 ; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
||||
RID=linux-arm ; \
|
||||
RID=linux-musl-arm ; \
|
||||
fi \
|
||||
&& echo "RID=$RID" > /tmp/rid.txt
|
||||
|
||||
@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \
|
||||
###############################################
|
||||
# App stage #
|
||||
###############################################
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
LABEL com.bitwarden.product="bitwarden"
|
||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||
ENV ASPNETCORE_URLS=http://+:5000
|
||||
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
|
||||
EXPOSE 5000
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
gosu \
|
||||
curl \
|
||||
krb5-user \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN apk add --no-cache curl \
|
||||
krb5 \
|
||||
icu-libs \
|
||||
shadow \
|
||||
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
|
||||
|
||||
# Copy app from the build stage
|
||||
WORKDIR /app
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Scim.Groups.Interfaces;
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Groups;
|
||||
|
||||
@ -16,10 +17,16 @@ public class GetGroupsListQuery : IGetGroupsListQuery
|
||||
_groupRepository = groupRepository;
|
||||
}
|
||||
|
||||
public async Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(Guid organizationId, string filter, int? count, int? startIndex)
|
||||
public async Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(
|
||||
Guid organizationId, GetGroupsQueryParamModel groupQueryParams)
|
||||
{
|
||||
string nameFilter = null;
|
||||
string externalIdFilter = null;
|
||||
|
||||
int count = groupQueryParams.Count;
|
||||
int startIndex = groupQueryParams.StartIndex;
|
||||
string filter = groupQueryParams.Filter;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
if (filter.StartsWith("displayName eq "))
|
||||
@ -53,11 +60,11 @@ public class GetGroupsListQuery : IGetGroupsListQuery
|
||||
}
|
||||
totalResults = groupList.Count;
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(filter) && startIndex.HasValue && count.HasValue)
|
||||
else if (string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
groupList = groups.OrderBy(g => g.Name)
|
||||
.Skip(startIndex.Value - 1)
|
||||
.Take(count.Value)
|
||||
.Skip(startIndex - 1)
|
||||
.Take(count)
|
||||
.ToList();
|
||||
totalResults = groups.Count;
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Groups.Interfaces;
|
||||
|
||||
public interface IGetGroupsListQuery
|
||||
{
|
||||
Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(Guid organizationId, string filter, int? count, int? startIndex);
|
||||
Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(Guid organizationId, GetGroupsQueryParamModel model);
|
||||
}
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Scim.Models;
|
||||
|
||||
public class GetGroupsQueryParamModel
|
||||
{
|
||||
public string Filter { get; init; } = string.Empty;
|
||||
|
||||
[Range(1, int.MaxValue)]
|
||||
public int Count { get; init; } = 50;
|
||||
|
||||
[Range(1, int.MaxValue)]
|
||||
public int StartIndex { get; init; } = 1;
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Scim.Models;
|
||||
|
||||
public class GetUsersQueryParamModel
|
||||
{
|
||||
public string Filter { get; init; } = string.Empty;
|
||||
@ -11,21 +11,8 @@ public class Program
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
|
||||
if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
|
||||
!string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
|
||||
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return e.Level >= globalSettings.MinLogLevel.ScimSettings.Default;
|
||||
}));
|
||||
})
|
||||
.AddSerilogFileLogging()
|
||||
.Build()
|
||||
.Run();
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ using Bit.Core.Utilities;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using IdentityModel;
|
||||
using Duende.IdentityModel;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Stripe;
|
||||
|
||||
@ -94,11 +94,8 @@ public class Startup
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IWebHostEnvironment env,
|
||||
IHostApplicationLifetime appLifetime,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
app.UseSerilog(env, appLifetime, globalSettings);
|
||||
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Users.Interfaces;
|
||||
|
||||
namespace Bit.Scim.Users;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Users.Interfaces;
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||
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;
|
||||
|
||||
@ -11,20 +11,19 @@ namespace Bit.Scim.Users;
|
||||
public class PatchUserCommand : IPatchUserCommand
|
||||
{
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||
private readonly ILogger<PatchUserCommand> _logger;
|
||||
private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommand;
|
||||
|
||||
public PatchUserCommand(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationService organizationService,
|
||||
public PatchUserCommand(IOrganizationUserRepository organizationUserRepository,
|
||||
IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
|
||||
ILogger<PatchUserCommand> logger)
|
||||
ILogger<PatchUserCommand> logger,
|
||||
IRevokeOrganizationUserCommand revokeOrganizationUserCommand)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationService = organizationService;
|
||||
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||
_logger = logger;
|
||||
_revokeOrganizationUserCommand = revokeOrganizationUserCommand;
|
||||
}
|
||||
|
||||
public async Task PatchUserAsync(Guid organizationId, Guid id, ScimPatchModel model)
|
||||
@ -80,7 +79,7 @@ public class PatchUserCommand : IPatchUserCommand
|
||||
}
|
||||
else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
await _organizationService.RevokeUserAsync(orgUser, EventSystemUser.SCIM);
|
||||
await _revokeOrganizationUserCommand.RevokeUserAsync(orgUser, EventSystemUser.SCIM);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@ -3,7 +3,7 @@ using System.Text.Encodings.Web;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Context;
|
||||
using IdentityModel;
|
||||
using Duende.IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
},
|
||||
"storage": {
|
||||
"connectionString": "UseDevelopmentStorage=true"
|
||||
}
|
||||
},
|
||||
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,9 +30,6 @@
|
||||
"connectionString": "SECRET",
|
||||
"applicationCacheTopicName": "SECRET"
|
||||
},
|
||||
"sentry": {
|
||||
"dsn": "SECRET"
|
||||
},
|
||||
"notificationHub": {
|
||||
"connectionString": "SECRET",
|
||||
"hubName": "SECRET"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/sh
|
||||
|
||||
# Setup
|
||||
|
||||
@ -37,7 +37,7 @@ then
|
||||
mkdir -p /etc/bitwarden/ca-certificates
|
||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
||||
|
||||
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
||||
if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then
|
||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos
|
||||
fi
|
||||
|
||||
@ -46,13 +46,13 @@ else
|
||||
gosu_cmd=""
|
||||
fi
|
||||
|
||||
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
||||
if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then
|
||||
cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf
|
||||
$gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab
|
||||
fi
|
||||
|
||||
if [[ $globalSettings__selfHosted == "true" ]]; then
|
||||
if [[ -z $globalSettings__identityServer__certificateLocation ]]; then
|
||||
if [ "$globalSettings__selfHosted" = "true" ]; then
|
||||
if [ -z "$globalSettings__identityServer__certificateLocation" ]; then
|
||||
export globalSettings__identityServer__certificateLocation=/etc/bitwarden/identity/identity.pfx
|
||||
fi
|
||||
fi
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.Security.Claims;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Entities;
|
||||
@ -22,10 +20,10 @@ using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Sso.Models;
|
||||
using Bit.Sso.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -56,6 +54,7 @@ public class AccountController : Controller
|
||||
private readonly IDataProtectorTokenFactory<SsoTokenable> _dataProtector;
|
||||
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
||||
private readonly IRegisterUserCommand _registerUserCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public AccountController(
|
||||
IAuthenticationSchemeProvider schemeProvider,
|
||||
@ -76,7 +75,8 @@ public class AccountController : Controller
|
||||
Core.Services.IEventService eventService,
|
||||
IDataProtectorTokenFactory<SsoTokenable> dataProtector,
|
||||
IOrganizationDomainRepository organizationDomainRepository,
|
||||
IRegisterUserCommand registerUserCommand)
|
||||
IRegisterUserCommand registerUserCommand,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_schemeProvider = schemeProvider;
|
||||
_clientStore = clientStore;
|
||||
@ -97,46 +97,43 @@ public class AccountController : Controller
|
||||
_dataProtector = dataProtector;
|
||||
_organizationDomainRepository = organizationDomainRepository;
|
||||
_registerUserCommand = registerUserCommand;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> PreValidate(string domainHint)
|
||||
public async Task<IActionResult> PreValidateAsync(string domainHint)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Validate domain_hint provided
|
||||
if (string.IsNullOrWhiteSpace(domainHint))
|
||||
{
|
||||
return InvalidJson("NoOrganizationIdentifierProvidedError");
|
||||
_logger.LogError(new ArgumentException("domainHint is required."), "domainHint not specified.");
|
||||
return InvalidJson("SsoInvalidIdentifierError");
|
||||
}
|
||||
|
||||
// Validate organization exists from domain_hint
|
||||
var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);
|
||||
if (organization == null)
|
||||
if (organization is not { UseSso: true })
|
||||
{
|
||||
return InvalidJson("OrganizationNotFoundByIdentifierError");
|
||||
}
|
||||
if (!organization.UseSso)
|
||||
{
|
||||
return InvalidJson("SsoNotAllowedForOrganizationError");
|
||||
_logger.LogError("Organization not configured to use SSO.");
|
||||
return InvalidJson("SsoInvalidIdentifierError");
|
||||
}
|
||||
|
||||
// Validate SsoConfig exists and is Enabled
|
||||
var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(domainHint);
|
||||
if (ssoConfig == null)
|
||||
if (ssoConfig is not { Enabled: true })
|
||||
{
|
||||
return InvalidJson("SsoConfigurationNotFoundForOrganizationError");
|
||||
}
|
||||
if (!ssoConfig.Enabled)
|
||||
{
|
||||
return InvalidJson("SsoNotEnabledForOrganizationError");
|
||||
_logger.LogError("SsoConfig not enabled.");
|
||||
return InvalidJson("SsoInvalidIdentifierError");
|
||||
}
|
||||
|
||||
// Validate Authentication Scheme exists and is loaded (cache)
|
||||
var scheme = await _schemeProvider.GetSchemeAsync(organization.Id.ToString());
|
||||
if (scheme == null || !(scheme is IDynamicAuthenticationScheme dynamicScheme))
|
||||
if (scheme is not IDynamicAuthenticationScheme dynamicScheme)
|
||||
{
|
||||
return InvalidJson("NoSchemeOrHandlerForSsoConfigurationFoundError");
|
||||
_logger.LogError("Invalid authentication scheme for organization.");
|
||||
return InvalidJson("SsoInvalidIdentifierError");
|
||||
}
|
||||
|
||||
// Run scheme validation
|
||||
@ -146,13 +143,8 @@ public class AccountController : Controller
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var translatedException = _i18nService.GetLocalizedHtmlString(ex.Message);
|
||||
var errorKey = "InvalidSchemeConfigurationError";
|
||||
if (!translatedException.ResourceNotFound)
|
||||
{
|
||||
errorKey = ex.Message;
|
||||
}
|
||||
return InvalidJson(errorKey, translatedException.ResourceNotFound ? ex : null);
|
||||
_logger.LogError(ex, "An error occurred while validating SSO dynamic scheme.");
|
||||
return InvalidJson("SsoInvalidIdentifierError");
|
||||
}
|
||||
|
||||
var tokenable = new SsoTokenable(organization, _globalSettings.Sso.SsoTokenLifetimeInSeconds);
|
||||
@ -162,15 +154,18 @@ public class AccountController : Controller
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return InvalidJson("PreValidationError", ex);
|
||||
_logger.LogError(ex, "An error occurred during SSO prevalidation.");
|
||||
return InvalidJson("SsoInvalidIdentifierError");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Login(string returnUrl)
|
||||
public async Task<IActionResult> LoginAsync(string returnUrl)
|
||||
{
|
||||
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
if (!context.Parameters.AllKeys.Contains("domain_hint") ||
|
||||
string.IsNullOrWhiteSpace(context.Parameters["domain_hint"]))
|
||||
{
|
||||
@ -186,6 +181,7 @@ public class AccountController : Controller
|
||||
|
||||
var domainHint = context.Parameters["domain_hint"];
|
||||
var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);
|
||||
#nullable restore
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
@ -242,32 +238,73 @@ public class AccountController : Controller
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ExternalCallback()
|
||||
{
|
||||
// Feature flag (PM-24579): Prevent SSO on existing non-compliant users.
|
||||
var preventOrgUserLoginIfStatusInvalid =
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM24579_PreventSsoOnExistingNonCompliantUsers);
|
||||
|
||||
// Read external identity from the temporary cookie
|
||||
var result = await HttpContext.AuthenticateAsync(
|
||||
AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
|
||||
if (result?.Succeeded != true)
|
||||
|
||||
if (preventOrgUserLoginIfStatusInvalid)
|
||||
{
|
||||
throw new Exception(_i18nService.T("ExternalAuthenticationError"));
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
throw new Exception(_i18nService.T("ExternalAuthenticationError"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (result?.Succeeded != true)
|
||||
{
|
||||
throw new Exception(_i18nService.T("ExternalAuthenticationError"));
|
||||
}
|
||||
}
|
||||
|
||||
// Debugging
|
||||
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
|
||||
_logger.LogDebug("External claims: {@claims}", externalClaims);
|
||||
// See if the user has logged in with this SSO provider before and has already been provisioned.
|
||||
// This is signified by the user existing in the User table and the SSOUser table for the SSO provider they're using.
|
||||
var (possibleSsoLinkedUser, provider, providerUserId, claims, ssoConfigData) = await FindUserFromExternalProviderAsync(result);
|
||||
|
||||
// Lookup our user and external provider info
|
||||
var (user, provider, providerUserId, claims, ssoConfigData) = await FindUserFromExternalProviderAsync(result);
|
||||
if (user == null)
|
||||
// We will look these up as required (lazy resolution) to avoid multiple DB hits.
|
||||
Organization? organization = null;
|
||||
OrganizationUser? orgUser = null;
|
||||
|
||||
// The user has not authenticated with this SSO provider before.
|
||||
// They could have an existing Bitwarden account in the User table though.
|
||||
if (possibleSsoLinkedUser == null)
|
||||
{
|
||||
// This might be where you might initiate a custom workflow for user registration
|
||||
// in this sample we don't show how that would be done, as our sample implementation
|
||||
// simply auto-provisions new external user
|
||||
var userIdentifier = result.Properties.Items.Keys.Contains("user_identifier") ?
|
||||
result.Properties.Items["user_identifier"] : null;
|
||||
user = await AutoProvisionUserAsync(provider, providerUserId, claims, userIdentifier, ssoConfigData);
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
// If we're manually linking to SSO, the user's external identifier will be passed as query string parameter.
|
||||
var userIdentifier = result.Properties.Items.Keys.Contains("user_identifier")
|
||||
? result.Properties.Items["user_identifier"]
|
||||
: null;
|
||||
|
||||
var (resolvedUser, foundOrganization, foundOrCreatedOrgUser) =
|
||||
await CreateUserAndOrgUserConditionallyAsync(
|
||||
provider,
|
||||
providerUserId,
|
||||
claims,
|
||||
userIdentifier,
|
||||
ssoConfigData);
|
||||
#nullable restore
|
||||
|
||||
possibleSsoLinkedUser = resolvedUser;
|
||||
|
||||
if (preventOrgUserLoginIfStatusInvalid)
|
||||
{
|
||||
organization = foundOrganization;
|
||||
orgUser = foundOrCreatedOrgUser;
|
||||
}
|
||||
}
|
||||
|
||||
if (user != null)
|
||||
if (preventOrgUserLoginIfStatusInvalid)
|
||||
{
|
||||
User resolvedSsoLinkedUser = possibleSsoLinkedUser
|
||||
?? throw new Exception(_i18nService.T("UserShouldBeFound"));
|
||||
|
||||
await PreventOrgUserLoginIfStatusInvalidAsync(organization, provider, orgUser, resolvedSsoLinkedUser);
|
||||
|
||||
// This allows us to collect any additional claims or properties
|
||||
// for the specific protocols used and store them in the local auth cookie.
|
||||
// this is typically used to store data needed for signout from those protocols.
|
||||
@ -280,19 +317,52 @@ public class AccountController : Controller
|
||||
ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);
|
||||
|
||||
// Issue authentication cookie for user
|
||||
await HttpContext.SignInAsync(new IdentityServerUser(user.Id.ToString())
|
||||
await HttpContext.SignInAsync(
|
||||
new IdentityServerUser(resolvedSsoLinkedUser.Id.ToString())
|
||||
{
|
||||
DisplayName = resolvedSsoLinkedUser.Email,
|
||||
IdentityProvider = provider,
|
||||
AdditionalClaims = additionalLocalClaims.ToArray()
|
||||
}, localSignInProps);
|
||||
}
|
||||
else
|
||||
{
|
||||
// PM-24579: remove this else block with feature flag removal.
|
||||
// Either the user already authenticated with the SSO provider, or we've just provisioned them.
|
||||
// Either way, we have associated the SSO login with a Bitwarden user.
|
||||
// We will now sign the Bitwarden user in.
|
||||
if (possibleSsoLinkedUser != null)
|
||||
{
|
||||
DisplayName = user.Email,
|
||||
IdentityProvider = provider,
|
||||
AdditionalClaims = additionalLocalClaims.ToArray()
|
||||
}, localSignInProps);
|
||||
// This allows us to collect any additional claims or properties
|
||||
// for the specific protocols used and store them in the local auth cookie.
|
||||
// this is typically used to store data needed for signout from those protocols.
|
||||
var additionalLocalClaims = new List<Claim>();
|
||||
var localSignInProps = new AuthenticationProperties
|
||||
{
|
||||
IsPersistent = true,
|
||||
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(1)
|
||||
};
|
||||
ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);
|
||||
|
||||
// Issue authentication cookie for user
|
||||
await HttpContext.SignInAsync(
|
||||
new IdentityServerUser(possibleSsoLinkedUser.Id.ToString())
|
||||
{
|
||||
DisplayName = possibleSsoLinkedUser.Email,
|
||||
IdentityProvider = provider,
|
||||
AdditionalClaims = additionalLocalClaims.ToArray()
|
||||
}, localSignInProps);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete temporary cookie used during external authentication
|
||||
await HttpContext.SignOutAsync(AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
|
||||
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
// Retrieve return URL
|
||||
var returnUrl = result.Properties.Items["return_url"] ?? "~/";
|
||||
#nullable restore
|
||||
|
||||
// Check if external login is in the context of an OIDC request
|
||||
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||
@ -311,8 +381,10 @@ public class AccountController : Controller
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Logout(string logoutId)
|
||||
public async Task<IActionResult> LogoutAsync(string logoutId)
|
||||
{
|
||||
// Build a model so the logged out page knows what to display
|
||||
var (updatedLogoutId, redirectUri, externalAuthenticationScheme) = await GetLoggedOutDataAsync(logoutId);
|
||||
@ -335,6 +407,7 @@ public class AccountController : Controller
|
||||
// This triggers a redirect to the external provider for sign-out
|
||||
return SignOut(new AuthenticationProperties { RedirectUri = url }, externalAuthenticationScheme);
|
||||
}
|
||||
|
||||
if (redirectUri != null)
|
||||
{
|
||||
return View("Redirect", new RedirectViewModel { RedirectUrl = redirectUri });
|
||||
@ -344,10 +417,22 @@ public class AccountController : Controller
|
||||
return Redirect("~/");
|
||||
}
|
||||
}
|
||||
#nullable restore
|
||||
|
||||
private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims, SsoConfigurationData config)>
|
||||
FindUserFromExternalProviderAsync(AuthenticateResult result)
|
||||
/// <summary>
|
||||
/// Attempts to map the external identity to a Bitwarden user, through the SsoUser table, which holds the `externalId`.
|
||||
/// The claims on the external identity are used to determine an `externalId`, and that is used to find the appropriate `SsoUser` and `User` records.
|
||||
/// </summary>
|
||||
private async Task<(
|
||||
User? possibleSsoUser,
|
||||
string provider,
|
||||
string providerUserId,
|
||||
IEnumerable<Claim> claims,
|
||||
SsoConfigurationData config
|
||||
)> FindUserFromExternalProviderAsync(AuthenticateResult result)
|
||||
{
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
var provider = result.Properties.Items["scheme"];
|
||||
var orgId = new Guid(provider);
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgId);
|
||||
@ -372,9 +457,10 @@ public class AccountController : Controller
|
||||
// Ensure the NameIdentifier used is not a transient name ID, if so, we need a different attribute
|
||||
// for the user identifier.
|
||||
static bool nameIdIsNotTransient(Claim c) => c.Type == ClaimTypes.NameIdentifier
|
||||
&& (c.Properties == null
|
||||
|| !c.Properties.TryGetValue(SamlPropertyKeys.ClaimFormat, out var claimFormat)
|
||||
|| claimFormat != SamlNameIdFormats.Transient);
|
||||
&& (c.Properties == null
|
||||
|| !c.Properties.TryGetValue(SamlPropertyKeys.ClaimFormat,
|
||||
out var claimFormat)
|
||||
|| claimFormat != SamlNameIdFormats.Transient);
|
||||
|
||||
// Try to determine the unique id of the external user (issued by the provider)
|
||||
// the most common claim type for that are the sub claim and the NameIdentifier
|
||||
@ -389,6 +475,7 @@ public class AccountController : Controller
|
||||
externalUser.FindFirst("upn") ??
|
||||
externalUser.FindFirst("eppn") ??
|
||||
throw new Exception(_i18nService.T("UnknownUserId"));
|
||||
#nullable restore
|
||||
|
||||
// Remove the user id claim so we don't include it as an extra claim if/when we provision the user
|
||||
var claims = externalUser.Claims.ToList();
|
||||
@ -397,112 +484,121 @@ public class AccountController : Controller
|
||||
// find external user
|
||||
var providerUserId = userIdClaim.Value;
|
||||
|
||||
var user = await _userRepository.GetBySsoUserAsync(providerUserId, orgId);
|
||||
var possibleSsoUser = await _userRepository.GetBySsoUserAsync(providerUserId, orgId);
|
||||
|
||||
return (user, provider, providerUserId, claims, ssoConfigData);
|
||||
return (possibleSsoUser, provider, providerUserId, claims, ssoConfigData);
|
||||
}
|
||||
|
||||
private async Task<User> AutoProvisionUserAsync(string provider, string providerUserId,
|
||||
IEnumerable<Claim> claims, string userIdentifier, SsoConfigurationData config)
|
||||
/// <summary>
|
||||
/// This function seeks to set up the org user record or create a new user record based on the conditions
|
||||
/// below.
|
||||
///
|
||||
/// This handles three different scenarios:
|
||||
/// 1. Creating an SsoUser link for an existing User and OrganizationUser
|
||||
/// - User is a member of the organization, but hasn't authenticated with the org's SSO provider before.
|
||||
/// 2. Creating a new User and a new OrganizationUser, then establishing an SsoUser link
|
||||
/// - User is joining the organization through JIT provisioning, without a pending invitation
|
||||
/// 3. Creating a new User for an existing OrganizationUser (created by invitation), then establishing an SsoUser link
|
||||
/// - User is signing in with a pending invitation.
|
||||
/// </summary>
|
||||
/// <param name="provider">The external identity provider.</param>
|
||||
/// <param name="providerUserId">The external identity provider's user identifier.</param>
|
||||
/// <param name="claims">The claims from the external IdP.</param>
|
||||
/// <param name="userIdentifier">The user identifier used for manual SSO linking.</param>
|
||||
/// <param name="ssoConfigData">The SSO configuration for the organization.</param>
|
||||
/// <returns>Guaranteed to return the user to sign in as well as the found organization and org user.</returns>
|
||||
/// <exception cref="Exception">An exception if the user cannot be provisioned as requested.</exception>
|
||||
private async Task<(User resolvedUser, Organization foundOrganization, OrganizationUser foundOrgUser)> CreateUserAndOrgUserConditionallyAsync(
|
||||
string provider,
|
||||
string providerUserId,
|
||||
IEnumerable<Claim> claims,
|
||||
string userIdentifier,
|
||||
SsoConfigurationData ssoConfigData
|
||||
)
|
||||
{
|
||||
var name = GetName(claims, config.GetAdditionalNameClaimTypes());
|
||||
var email = GetEmailAddress(claims, config.GetAdditionalEmailClaimTypes());
|
||||
if (string.IsNullOrWhiteSpace(email) && providerUserId.Contains("@"))
|
||||
{
|
||||
email = providerUserId;
|
||||
}
|
||||
// Try to get the email from the claims as we don't know if we have a user record yet.
|
||||
var name = GetName(claims, ssoConfigData.GetAdditionalNameClaimTypes());
|
||||
var email = TryGetEmailAddress(claims, ssoConfigData, providerUserId);
|
||||
|
||||
if (!Guid.TryParse(provider, out var orgId))
|
||||
{
|
||||
// TODO: support non-org (server-wide) SSO in the future?
|
||||
throw new Exception(_i18nService.T("SSOProviderIsNotAnOrgId", provider));
|
||||
}
|
||||
|
||||
User existingUser = null;
|
||||
User? possibleExistingUser;
|
||||
if (string.IsNullOrWhiteSpace(userIdentifier))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
throw new Exception(_i18nService.T("CannotFindEmailClaim"));
|
||||
}
|
||||
existingUser = await _userRepository.GetByEmailAsync(email);
|
||||
|
||||
possibleExistingUser = await _userRepository.GetByEmailAsync(email);
|
||||
}
|
||||
else
|
||||
{
|
||||
var split = userIdentifier.Split(",");
|
||||
if (split.Length < 2)
|
||||
{
|
||||
throw new Exception(_i18nService.T("InvalidUserIdentifier"));
|
||||
}
|
||||
var userId = split[0];
|
||||
var token = split[1];
|
||||
|
||||
var tokenOptions = new TokenOptions();
|
||||
|
||||
var claimedUser = await _userService.GetUserByIdAsync(userId);
|
||||
if (claimedUser != null)
|
||||
{
|
||||
var tokenIsValid = await _userManager.VerifyUserTokenAsync(
|
||||
claimedUser, tokenOptions.PasswordResetTokenProvider, TokenPurposes.LinkSso, token);
|
||||
if (tokenIsValid)
|
||||
{
|
||||
existingUser = claimedUser;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception(_i18nService.T("UserIdAndTokenMismatch"));
|
||||
}
|
||||
}
|
||||
possibleExistingUser = await GetUserFromManualLinkingDataAsync(userIdentifier);
|
||||
}
|
||||
|
||||
OrganizationUser orgUser = null;
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgId);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new Exception(_i18nService.T("CouldNotFindOrganization", orgId));
|
||||
}
|
||||
// Find the org (we error if we can't find an org because no org is not valid)
|
||||
var organization = await GetOrganizationByProviderAsync(provider);
|
||||
|
||||
// Try to find OrgUser via existing User Id (accepted/confirmed user)
|
||||
if (existingUser != null)
|
||||
{
|
||||
var orgUsersByUserId = await _organizationUserRepository.GetManyByUserAsync(existingUser.Id);
|
||||
orgUser = orgUsersByUserId.SingleOrDefault(u => u.OrganizationId == orgId);
|
||||
}
|
||||
// Try to find an org user (null org user possible and valid here)
|
||||
var possibleOrgUser = await GetOrganizationUserByUserAndOrgIdOrEmailAsync(possibleExistingUser, organization.Id, email);
|
||||
|
||||
// If no Org User found by Existing User Id - search all organization users via email
|
||||
orgUser ??= await _organizationUserRepository.GetByOrganizationEmailAsync(orgId, email);
|
||||
|
||||
// All Existing User flows handled below
|
||||
if (existingUser != null)
|
||||
//----------------------------------------------------
|
||||
// Scenario 1: We've found the user in the User table
|
||||
//----------------------------------------------------
|
||||
if (possibleExistingUser != null)
|
||||
{
|
||||
if (existingUser.UsesKeyConnector &&
|
||||
(orgUser == null || orgUser.Status == OrganizationUserStatusType.Invited))
|
||||
User guaranteedExistingUser = possibleExistingUser;
|
||||
|
||||
if (guaranteedExistingUser.UsesKeyConnector &&
|
||||
(possibleOrgUser == null || possibleOrgUser.Status == OrganizationUserStatusType.Invited))
|
||||
{
|
||||
throw new Exception(_i18nService.T("UserAlreadyExistsKeyConnector"));
|
||||
}
|
||||
|
||||
if (orgUser == null)
|
||||
OrganizationUser guaranteedOrgUser = possibleOrgUser ?? throw new Exception(_i18nService.T("UserAlreadyExistsInviteProcess"));
|
||||
|
||||
/*
|
||||
* ----------------------------------------------------
|
||||
* Critical Code Check Here
|
||||
*
|
||||
* We want to ensure a user is not in the invited state
|
||||
* explicitly. User's in the invited state should not
|
||||
* be able to authenticate via SSO.
|
||||
*
|
||||
* See internal doc called "Added Context for SSO Login
|
||||
* Flows" for further details.
|
||||
* ----------------------------------------------------
|
||||
*/
|
||||
if (guaranteedOrgUser.Status == OrganizationUserStatusType.Invited)
|
||||
{
|
||||
// Org User is not created - no invite has been sent
|
||||
throw new Exception(_i18nService.T("UserAlreadyExistsInviteProcess"));
|
||||
// Org User is invited – must accept via email first
|
||||
throw new Exception(
|
||||
_i18nService.T("AcceptInviteBeforeUsingSSO", organization.DisplayName()));
|
||||
}
|
||||
|
||||
if (orgUser.Status == OrganizationUserStatusType.Invited)
|
||||
{
|
||||
// Org User is invited - they must manually accept the invite via email and authenticate with MP
|
||||
// This allows us to enroll them in MP reset if required
|
||||
throw new Exception(_i18nService.T("AcceptInviteBeforeUsingSSO", organization.DisplayName()));
|
||||
}
|
||||
// If the user already exists in Bitwarden, we require that the user already be in the org,
|
||||
// and that they are either Accepted or Confirmed.
|
||||
EnforceAllowedOrgUserStatus(
|
||||
guaranteedOrgUser.Status,
|
||||
allowedStatuses: [
|
||||
OrganizationUserStatusType.Accepted,
|
||||
OrganizationUserStatusType.Confirmed
|
||||
],
|
||||
organization.DisplayName());
|
||||
|
||||
// Accepted or Confirmed - create SSO link and return;
|
||||
await CreateSsoUserRecord(providerUserId, existingUser.Id, orgId, orgUser);
|
||||
return existingUser;
|
||||
// Since we're in the auto-provisioning logic, this means that the user exists, but they have not
|
||||
// authenticated with the org's SSO provider before now (otherwise we wouldn't be auto-provisioning them).
|
||||
// We've verified that the user is Accepted or Confnirmed, so we can create an SsoUser link and proceed
|
||||
// with authentication.
|
||||
await CreateSsoUserRecordAsync(providerUserId, guaranteedExistingUser.Id, organization.Id, guaranteedOrgUser);
|
||||
|
||||
return (guaranteedExistingUser, organization, guaranteedOrgUser);
|
||||
}
|
||||
|
||||
// Before any user creation - if Org User doesn't exist at this point - make sure there are enough seats to add one
|
||||
if (orgUser == null && organization.Seats.HasValue)
|
||||
if (possibleOrgUser == null && organization.Seats.HasValue)
|
||||
{
|
||||
var occupiedSeats = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var occupiedSeats =
|
||||
await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var initialSeatCount = organization.Seats.Value;
|
||||
var availableSeats = initialSeatCount - occupiedSeats.Total;
|
||||
if (availableSeats < 1)
|
||||
@ -520,8 +616,10 @@ public class AccountController : Controller
|
||||
{
|
||||
if (organization.Seats.Value != initialSeatCount)
|
||||
{
|
||||
await _organizationService.AdjustSeatsAsync(orgId, initialSeatCount - organization.Seats.Value);
|
||||
await _organizationService.AdjustSeatsAsync(organization.Id,
|
||||
initialSeatCount - organization.Seats.Value);
|
||||
}
|
||||
|
||||
_logger.LogInformation(e, "SSO auto provisioning failed");
|
||||
throw new Exception(_i18nService.T("NoSeatsAvailable", organization.DisplayName()));
|
||||
}
|
||||
@ -529,65 +627,257 @@ public class AccountController : Controller
|
||||
}
|
||||
|
||||
// If the email domain is verified, we can mark the email as verified
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
throw new Exception(_i18nService.T("CannotFindEmailClaim"));
|
||||
}
|
||||
|
||||
var emailVerified = false;
|
||||
var emailDomain = CoreHelpers.GetEmailDomain(email);
|
||||
if (!string.IsNullOrWhiteSpace(emailDomain))
|
||||
{
|
||||
var organizationDomain = await _organizationDomainRepository.GetDomainByOrgIdAndDomainNameAsync(orgId, emailDomain);
|
||||
var organizationDomain =
|
||||
await _organizationDomainRepository.GetDomainByOrgIdAndDomainNameAsync(organization.Id, emailDomain);
|
||||
emailVerified = organizationDomain?.VerifiedDate.HasValue ?? false;
|
||||
}
|
||||
|
||||
// Create user record - all existing user flows are handled above
|
||||
var user = new User
|
||||
//--------------------------------------------------
|
||||
// Scenarios 2 and 3: We need to register a new user
|
||||
//--------------------------------------------------
|
||||
var newUser = new User
|
||||
{
|
||||
Name = name,
|
||||
Email = email,
|
||||
EmailVerified = emailVerified,
|
||||
ApiKey = CoreHelpers.SecureRandomString(30)
|
||||
};
|
||||
await _registerUserCommand.RegisterUser(user);
|
||||
|
||||
// If the organization has 2fa policy enabled, make sure to default jit user 2fa to email
|
||||
var twoFactorPolicy =
|
||||
await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.TwoFactorAuthentication);
|
||||
if (twoFactorPolicy != null && twoFactorPolicy.Enabled)
|
||||
/*
|
||||
The feature flag is checked here so that we can send the new MJML welcome email templates.
|
||||
The other organization invites flows have an OrganizationUser allowing the RegisterUserCommand the ability
|
||||
to fetch the Organization. The old method RegisterUser(User) here does not have that context, so we need
|
||||
to use a new method RegisterSSOAutoProvisionedUserAsync(User, Organization) to send the correct email.
|
||||
[PM-28057]: Prefer RegisterSSOAutoProvisionedUserAsync for SSO auto-provisioned users.
|
||||
TODO: Remove Feature flag: PM-28221
|
||||
*/
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates))
|
||||
{
|
||||
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
[TwoFactorProviderType.Email] = new TwoFactorProvider
|
||||
{
|
||||
MetaData = new Dictionary<string, object> { ["Email"] = user.Email.ToLowerInvariant() },
|
||||
Enabled = true
|
||||
}
|
||||
});
|
||||
await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
|
||||
}
|
||||
|
||||
// Create Org User if null or else update existing Org User
|
||||
if (orgUser == null)
|
||||
{
|
||||
orgUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
UserId = user.Id,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Invited
|
||||
};
|
||||
await _organizationUserRepository.CreateAsync(orgUser);
|
||||
await _registerUserCommand.RegisterSSOAutoProvisionedUserAsync(newUser, organization);
|
||||
}
|
||||
else
|
||||
{
|
||||
orgUser.UserId = user.Id;
|
||||
await _organizationUserRepository.ReplaceAsync(orgUser);
|
||||
await _registerUserCommand.RegisterUser(newUser);
|
||||
}
|
||||
|
||||
// Create sso user record
|
||||
await CreateSsoUserRecord(providerUserId, user.Id, orgId, orgUser);
|
||||
// If the organization has 2fa policy enabled, make sure to default jit user 2fa to email
|
||||
var twoFactorPolicy =
|
||||
await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.TwoFactorAuthentication);
|
||||
if (twoFactorPolicy != null && twoFactorPolicy.Enabled)
|
||||
{
|
||||
newUser.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
[TwoFactorProviderType.Email] = new TwoFactorProvider
|
||||
{
|
||||
MetaData = new Dictionary<string, object> { ["Email"] = newUser.Email.ToLowerInvariant() },
|
||||
Enabled = true
|
||||
}
|
||||
});
|
||||
await _userService.UpdateTwoFactorProviderAsync(newUser, TwoFactorProviderType.Email);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------
|
||||
// Scenario 2: We also need to create an OrganizationUser
|
||||
// This means that an invitation was not sent for this user and we
|
||||
// need to establish their invited status now.
|
||||
//-----------------------------------------------------------------
|
||||
if (possibleOrgUser == null)
|
||||
{
|
||||
possibleOrgUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = newUser.Id,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Invited
|
||||
};
|
||||
await _organizationUserRepository.CreateAsync(possibleOrgUser);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------
|
||||
// Scenario 3: There is already an existing OrganizationUser
|
||||
// That was established through an invitation. We just need to
|
||||
// update the UserId now that we have created a User record.
|
||||
//-----------------------------------------------------------------
|
||||
else
|
||||
{
|
||||
possibleOrgUser.UserId = newUser.Id;
|
||||
await _organizationUserRepository.ReplaceAsync(possibleOrgUser);
|
||||
}
|
||||
|
||||
// Create the SsoUser record to link the user to the SSO provider.
|
||||
await CreateSsoUserRecordAsync(providerUserId, newUser.Id, organization.Id, possibleOrgUser);
|
||||
|
||||
return (newUser, organization, possibleOrgUser);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates an organization user is allowed to log in via SSO and blocks invalid statuses.
|
||||
/// Lazily resolves the organization and organization user if not provided.
|
||||
/// </summary>
|
||||
/// <param name="organization">The target organization; if null, resolved from provider.</param>
|
||||
/// <param name="provider">The SSO scheme provider value (organization id as a GUID string).</param>
|
||||
/// <param name="orgUser">The organization-user record; if null, looked up by user/org or user email for invited users.</param>
|
||||
/// <param name="user">The user attempting to sign in (existing or newly provisioned).</param>
|
||||
/// <exception cref="Exception">Thrown if the organization cannot be resolved from provider;
|
||||
/// the organization user cannot be found; or the organization user status is not allowed.</exception>
|
||||
private async Task PreventOrgUserLoginIfStatusInvalidAsync(
|
||||
Organization? organization,
|
||||
string provider,
|
||||
OrganizationUser? orgUser,
|
||||
User user)
|
||||
{
|
||||
// Lazily get organization if not already known
|
||||
organization ??= await GetOrganizationByProviderAsync(provider);
|
||||
|
||||
// Lazily get the org user if not already known
|
||||
orgUser ??= await GetOrganizationUserByUserAndOrgIdOrEmailAsync(
|
||||
user,
|
||||
organization.Id,
|
||||
user.Email);
|
||||
|
||||
if (orgUser != null)
|
||||
{
|
||||
// Invited is allowed at this point because we know the user is trying to accept an org invite.
|
||||
EnforceAllowedOrgUserStatus(
|
||||
orgUser.Status,
|
||||
allowedStatuses: [
|
||||
OrganizationUserStatusType.Invited,
|
||||
OrganizationUserStatusType.Accepted,
|
||||
OrganizationUserStatusType.Confirmed,
|
||||
],
|
||||
organization.DisplayName());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception(_i18nService.T("CouldNotFindOrganizationUser", user.Id, organization.Id));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<User?> GetUserFromManualLinkingDataAsync(string userIdentifier)
|
||||
{
|
||||
User? user = null;
|
||||
var split = userIdentifier.Split(",");
|
||||
if (split.Length < 2)
|
||||
{
|
||||
throw new Exception(_i18nService.T("InvalidUserIdentifier"));
|
||||
}
|
||||
|
||||
var userId = split[0];
|
||||
var token = split[1];
|
||||
|
||||
var tokenOptions = new TokenOptions();
|
||||
|
||||
var claimedUser = await _userService.GetUserByIdAsync(userId);
|
||||
if (claimedUser != null)
|
||||
{
|
||||
var tokenIsValid = await _userManager.VerifyUserTokenAsync(
|
||||
claimedUser, tokenOptions.PasswordResetTokenProvider, TokenPurposes.LinkSso, token);
|
||||
if (tokenIsValid)
|
||||
{
|
||||
user = claimedUser;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception(_i18nService.T("UserIdAndTokenMismatch"));
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private IActionResult InvalidJson(string errorMessageKey, Exception ex = null)
|
||||
/// <summary>
|
||||
/// Tries to get the organization by the provider which is org id for us as we use the scheme
|
||||
/// to identify organizations - not identity providers.
|
||||
/// </summary>
|
||||
/// <param name="provider">Org id string from SSO scheme property</param>
|
||||
/// <exception cref="Exception">Errors if the provider string is not a valid org id guid or if the org cannot be found by the id.</exception>
|
||||
private async Task<Organization> GetOrganizationByProviderAsync(string provider)
|
||||
{
|
||||
if (!Guid.TryParse(provider, out var organizationId))
|
||||
{
|
||||
// TODO: support non-org (server-wide) SSO in the future?
|
||||
throw new Exception(_i18nService.T("SSOProviderIsNotAnOrgId", provider));
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
throw new Exception(_i18nService.T("CouldNotFindOrganization", organizationId));
|
||||
}
|
||||
|
||||
return organization;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get an <see cref="OrganizationUser"/> for a given organization
|
||||
/// by first checking for an existing user relationship, and if none is found,
|
||||
/// by looking up an invited user via their email address.
|
||||
/// </summary>
|
||||
/// <param name="user">The existing user entity to be looked up in OrganizationUsers table.</param>
|
||||
/// <param name="organizationId">Organization id from the provider data.</param>
|
||||
/// <param name="email">Email to use as a fallback in case of an invited user not in the Org Users
|
||||
/// table yet.</param>
|
||||
private async Task<OrganizationUser?> GetOrganizationUserByUserAndOrgIdOrEmailAsync(
|
||||
User? user,
|
||||
Guid organizationId,
|
||||
string? email)
|
||||
{
|
||||
OrganizationUser? orgUser = null;
|
||||
|
||||
// Try to find OrgUser via existing User Id.
|
||||
// This covers any OrganizationUser state after they have accepted an invite.
|
||||
if (user != null)
|
||||
{
|
||||
var orgUsersByUserId = await _organizationUserRepository.GetManyByUserAsync(user.Id);
|
||||
orgUser = orgUsersByUserId.SingleOrDefault(u => u.OrganizationId == organizationId);
|
||||
}
|
||||
|
||||
// If no Org User found by Existing User Id - search all the organization's users via email.
|
||||
// This covers users who are Invited but haven't accepted their invite yet.
|
||||
if (email != null)
|
||||
{
|
||||
orgUser ??= await _organizationUserRepository.GetByOrganizationEmailAsync(organizationId, email);
|
||||
}
|
||||
|
||||
return orgUser;
|
||||
}
|
||||
|
||||
private void EnforceAllowedOrgUserStatus(
|
||||
OrganizationUserStatusType statusToCheckAgainst,
|
||||
OrganizationUserStatusType[] allowedStatuses,
|
||||
string organizationDisplayNameForLogging)
|
||||
{
|
||||
// if this status is one of the allowed ones, just return
|
||||
if (allowedStatuses.Contains(statusToCheckAgainst))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise throw the appropriate exception
|
||||
switch (statusToCheckAgainst)
|
||||
{
|
||||
case OrganizationUserStatusType.Revoked:
|
||||
// Revoked users may not be (auto)‑provisioned
|
||||
throw new Exception(
|
||||
_i18nService.T("OrganizationUserAccessRevoked", organizationDisplayNameForLogging));
|
||||
default:
|
||||
// anything else is “unknown”
|
||||
throw new Exception(
|
||||
_i18nService.T("OrganizationUserUnknownStatus", organizationDisplayNameForLogging));
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult InvalidJson(string errorMessageKey, Exception? ex = null)
|
||||
{
|
||||
Response.StatusCode = ex == null ? 400 : 500;
|
||||
return Json(new ErrorResponseModel(_i18nService.T(errorMessageKey))
|
||||
@ -598,13 +888,13 @@ public class AccountController : Controller
|
||||
});
|
||||
}
|
||||
|
||||
private string GetEmailAddress(IEnumerable<Claim> claims, IEnumerable<string> additionalClaimTypes)
|
||||
private string? TryGetEmailAddressFromClaims(IEnumerable<Claim> claims, IEnumerable<string> additionalClaimTypes)
|
||||
{
|
||||
var filteredClaims = claims.Where(c => !string.IsNullOrWhiteSpace(c.Value) && c.Value.Contains("@"));
|
||||
|
||||
var email = filteredClaims.GetFirstMatch(additionalClaimTypes.ToArray()) ??
|
||||
filteredClaims.GetFirstMatch(JwtClaimTypes.Email, ClaimTypes.Email,
|
||||
SamlClaimTypes.Email, "mail", "emailaddress");
|
||||
filteredClaims.GetFirstMatch(JwtClaimTypes.Email, ClaimTypes.Email,
|
||||
SamlClaimTypes.Email, "mail", "emailaddress");
|
||||
if (!string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
return email;
|
||||
@ -620,13 +910,15 @@ public class AccountController : Controller
|
||||
return null;
|
||||
}
|
||||
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
private string GetName(IEnumerable<Claim> claims, IEnumerable<string> additionalClaimTypes)
|
||||
{
|
||||
var filteredClaims = claims.Where(c => !string.IsNullOrWhiteSpace(c.Value));
|
||||
|
||||
var name = filteredClaims.GetFirstMatch(additionalClaimTypes.ToArray()) ??
|
||||
filteredClaims.GetFirstMatch(JwtClaimTypes.Name, ClaimTypes.Name,
|
||||
SamlClaimTypes.DisplayName, SamlClaimTypes.CommonName, "displayname", "cn");
|
||||
filteredClaims.GetFirstMatch(JwtClaimTypes.Name, ClaimTypes.Name,
|
||||
SamlClaimTypes.DisplayName, SamlClaimTypes.CommonName, "displayname", "cn");
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return name;
|
||||
@ -643,8 +935,10 @@ public class AccountController : Controller
|
||||
|
||||
return null;
|
||||
}
|
||||
#nullable restore
|
||||
|
||||
private async Task CreateSsoUserRecord(string providerUserId, Guid userId, Guid orgId, OrganizationUser orgUser)
|
||||
private async Task CreateSsoUserRecordAsync(string providerUserId, Guid userId, Guid orgId,
|
||||
OrganizationUser orgUser)
|
||||
{
|
||||
// Delete existing SsoUser (if any) - avoids error if providerId has changed and the sso link is stale
|
||||
var existingSsoUser = await _ssoUserRepository.GetByUserIdOrganizationIdAsync(orgId, userId);
|
||||
@ -659,15 +953,12 @@ public class AccountController : Controller
|
||||
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_FirstSsoLogin);
|
||||
}
|
||||
|
||||
var ssoUser = new SsoUser
|
||||
{
|
||||
ExternalId = providerUserId,
|
||||
UserId = userId,
|
||||
OrganizationId = orgId,
|
||||
};
|
||||
var ssoUser = new SsoUser { ExternalId = providerUserId, UserId = userId, OrganizationId = orgId, };
|
||||
await _ssoUserRepository.CreateAsync(ssoUser);
|
||||
}
|
||||
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
private void ProcessLoginCallback(AuthenticateResult externalResult,
|
||||
List<Claim> localClaims, AuthenticationProperties localSignInProps)
|
||||
{
|
||||
@ -688,18 +979,6 @@ public class AccountController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> GetProviderAsync(string returnUrl)
|
||||
{
|
||||
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||
if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null)
|
||||
{
|
||||
return context.IdP;
|
||||
}
|
||||
var schemes = await _schemeProvider.GetAllSchemesAsync();
|
||||
var providers = schemes.Select(x => x.Name).ToList();
|
||||
return providers.FirstOrDefault();
|
||||
}
|
||||
|
||||
private async Task<(string, string, string)> GetLoggedOutDataAsync(string logoutId)
|
||||
{
|
||||
// Get context information (client name, post logout redirect URI and iframe for federated signout)
|
||||
@ -730,10 +1009,31 @@ public class AccountController : Controller
|
||||
|
||||
return (logoutId, logout?.PostLogoutRedirectUri, externalAuthenticationScheme);
|
||||
}
|
||||
#nullable restore
|
||||
|
||||
/**
|
||||
* Tries to get a user's email from the claims and SSO configuration data or the provider user id if
|
||||
* the claims email extraction returns null.
|
||||
*/
|
||||
private string? TryGetEmailAddress(
|
||||
IEnumerable<Claim> claims,
|
||||
SsoConfigurationData config,
|
||||
string providerUserId)
|
||||
{
|
||||
var email = TryGetEmailAddressFromClaims(claims, config.GetAdditionalEmailClaimTypes());
|
||||
|
||||
// If email isn't populated from claims and providerUserId has @, assume it is the email.
|
||||
if (string.IsNullOrWhiteSpace(email) && providerUserId.Contains("@"))
|
||||
{
|
||||
email = providerUserId;
|
||||
}
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
public bool IsNativeClient(DIM.AuthorizationRequest context)
|
||||
{
|
||||
return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal)
|
||||
&& !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);
|
||||
&& !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
###############################################
|
||||
# Build stage #
|
||||
###############################################
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
|
||||
|
||||
# Docker buildx supplies the value for this arg
|
||||
ARG TARGETPLATFORM
|
||||
@ -9,11 +9,11 @@ ARG TARGETPLATFORM
|
||||
# Determine proper runtime value for .NET
|
||||
# We put the value in a file to be read by later layers.
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||
RID=linux-x64 ; \
|
||||
RID=linux-musl-x64 ; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||
RID=linux-arm64 ; \
|
||||
RID=linux-musl-arm64 ; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
||||
RID=linux-arm ; \
|
||||
RID=linux-musl-arm ; \
|
||||
fi \
|
||||
&& echo "RID=$RID" > /tmp/rid.txt
|
||||
|
||||
@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \
|
||||
###############################################
|
||||
# App stage #
|
||||
###############################################
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
LABEL com.bitwarden.product="bitwarden"
|
||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||
ENV ASPNETCORE_URLS=http://+:5000
|
||||
ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
|
||||
EXPOSE 5000
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
gosu \
|
||||
curl \
|
||||
krb5-user \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN apk add --no-cache curl \
|
||||
krb5 \
|
||||
icu-libs \
|
||||
shadow \
|
||||
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
|
||||
|
||||
# Copy app from the build stage
|
||||
WORKDIR /app
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Serilog;
|
||||
|
||||
namespace Bit.Sso;
|
||||
|
||||
@ -13,19 +12,8 @@ public class Program
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
|
||||
!string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
|
||||
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return e.Level >= globalSettings.MinLogLevel.SsoSettings.Default;
|
||||
}));
|
||||
})
|
||||
.AddSerilogFileLogging()
|
||||
.Build()
|
||||
.Run();
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<!-- This is a transitive dependency to Sustainsys.Saml2.AspNetCore2 -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
|
||||
|
||||
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.10.0" />
|
||||
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.11.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -100,8 +100,6 @@ public class Startup
|
||||
IdentityModelEventSource.ShowPII = true;
|
||||
}
|
||||
|
||||
app.UseSerilog(env, appLifetime, globalSettings);
|
||||
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
|
||||
@ -157,6 +155,6 @@ public class Startup
|
||||
app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
|
||||
|
||||
// Log startup
|
||||
logger.LogInformation(Constants.BypassFiltersEventId, globalSettings.ProjectName + " started.");
|
||||
logger.LogInformation(Constants.BypassFiltersEventId, "{Project} started.", globalSettings.ProjectName);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,15 +4,16 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Sso.Models;
|
||||
using Bit.Sso.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Infrastructure;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
using Microsoft.Extensions.Options;
|
||||
@ -416,7 +417,7 @@ public class DynamicAuthenticationSchemeProvider : AuthenticationSchemeProvider
|
||||
SPOptions = spOptions,
|
||||
SignInScheme = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme,
|
||||
SignOutScheme = IdentityServerConstants.DefaultCookieAuthenticationScheme,
|
||||
CookieManager = new IdentityServer.DistributedCacheCookieManager(),
|
||||
CookieManager = new DistributedCacheCookieManager(),
|
||||
};
|
||||
options.IdentityProviders.Add(idp);
|
||||
|
||||
|
||||
@ -24,6 +24,13 @@
|
||||
"storage": {
|
||||
"connectionString": "UseDevelopmentStorage=true"
|
||||
},
|
||||
"developmentDirectory": "../../../dev"
|
||||
"developmentDirectory": "../../../dev",
|
||||
"pricingUri": "https://billingpricing.qa.bitwarden.pw",
|
||||
"mail": {
|
||||
"smtp": {
|
||||
"host": "localhost",
|
||||
"port": 10250
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,11 @@
|
||||
"mail": {
|
||||
"sendGridApiKey": "SECRET",
|
||||
"amazonConfigSetName": "Email",
|
||||
"replyToEmail": "no-reply@bitwarden.com"
|
||||
"replyToEmail": "no-reply@bitwarden.com",
|
||||
"smtp": {
|
||||
"host": "localhost",
|
||||
"port": 10250
|
||||
}
|
||||
},
|
||||
"identityServer": {
|
||||
"certificateThumbprint": "SECRET"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/sh
|
||||
|
||||
# Setup
|
||||
|
||||
@ -37,7 +37,7 @@ then
|
||||
mkdir -p /etc/bitwarden/ca-certificates
|
||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
|
||||
|
||||
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
||||
if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then
|
||||
chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos
|
||||
fi
|
||||
|
||||
@ -46,13 +46,13 @@ else
|
||||
gosu_cmd=""
|
||||
fi
|
||||
|
||||
if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then
|
||||
if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then
|
||||
cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf
|
||||
$gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab
|
||||
fi
|
||||
|
||||
if [[ $globalSettings__selfHosted == "true" ]]; then
|
||||
if [[ -z $globalSettings__identityServer__certificateLocation ]]; then
|
||||
if [ "$globalSettings__selfHosted" = "true" ]; then
|
||||
if [ -z "$globalSettings__identityServer__certificateLocation" ]; then
|
||||
export globalSettings__identityServer__certificateLocation=/etc/bitwarden/identity/identity.pfx
|
||||
fi
|
||||
fi
|
||||
|
||||
216
bitwarden_license/src/Sso/package-lock.json
generated
216
bitwarden_license/src/Sso/package-lock.json
generated
@ -17,9 +17,9 @@
|
||||
"css-loader": "7.1.2",
|
||||
"expose-loader": "5.0.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"sass": "1.89.2",
|
||||
"sass": "1.93.2",
|
||||
"sass-loader": "16.0.5",
|
||||
"webpack": "5.99.8",
|
||||
"webpack": "5.102.1",
|
||||
"webpack-cli": "5.1.4"
|
||||
}
|
||||
},
|
||||
@ -34,18 +34,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
@ -58,20 +54,10 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
|
||||
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -80,16 +66,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"version": "0.3.30",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
|
||||
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -441,9 +427,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -455,13 +441,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
|
||||
"integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
|
||||
"version": "24.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz",
|
||||
"integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
@ -687,11 +673,12 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -699,12 +686,26 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-import-phases": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
|
||||
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@ -747,6 +748,16 @@
|
||||
"ajv": "^8.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.18",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.18.tgz",
|
||||
"integrity": "sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "5.3.6",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz",
|
||||
@ -781,9 +792,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.24.5",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
|
||||
"integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
|
||||
"version": "4.26.3",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
|
||||
"integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -800,10 +811,12 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001716",
|
||||
"electron-to-chromium": "^1.5.149",
|
||||
"node-releases": "^2.0.19",
|
||||
"baseline-browser-mapping": "^2.8.9",
|
||||
"caniuse-lite": "^1.0.30001746",
|
||||
"electron-to-chromium": "^1.5.227",
|
||||
"node-releases": "^2.0.21",
|
||||
"update-browserslist-db": "^1.1.3"
|
||||
},
|
||||
"bin": {
|
||||
@ -821,9 +834,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001718",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
|
||||
"integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
|
||||
"version": "1.0.30001751",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
|
||||
"integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -975,16 +988,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.155",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
|
||||
"integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
|
||||
"version": "1.5.237",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz",
|
||||
"integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
||||
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1107,9 +1120,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -1241,9 +1254,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
|
||||
"integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
|
||||
"integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -1528,9 +1541,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
||||
"version": "2.0.26",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz",
|
||||
"integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -1635,9 +1648,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -1654,8 +1667,9 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@ -1860,11 +1874,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.89.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz",
|
||||
"integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
|
||||
"integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
@ -1922,9 +1937,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
|
||||
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
|
||||
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -2061,24 +2076,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
|
||||
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.39.2",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
|
||||
"integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
|
||||
"version": "5.44.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
|
||||
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.14.0",
|
||||
"acorn": "^8.15.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
@ -2139,9 +2158,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"version": "7.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
||||
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -2198,22 +2217,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.99.8",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
|
||||
"integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
|
||||
"version": "5.102.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
|
||||
"integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/estree": "^1.0.8",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@webassemblyjs/ast": "^1.14.1",
|
||||
"@webassemblyjs/wasm-edit": "^1.14.1",
|
||||
"@webassemblyjs/wasm-parser": "^1.14.1",
|
||||
"acorn": "^8.14.0",
|
||||
"browserslist": "^4.24.0",
|
||||
"acorn": "^8.15.0",
|
||||
"acorn-import-phases": "^1.0.3",
|
||||
"browserslist": "^4.26.3",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
"enhanced-resolve": "^5.17.1",
|
||||
"enhanced-resolve": "^5.17.3",
|
||||
"es-module-lexer": "^1.2.1",
|
||||
"eslint-scope": "5.1.1",
|
||||
"events": "^3.2.0",
|
||||
@ -2223,11 +2244,11 @@
|
||||
"loader-runner": "^4.2.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"neo-async": "^2.6.2",
|
||||
"schema-utils": "^4.3.2",
|
||||
"tapable": "^2.1.1",
|
||||
"schema-utils": "^4.3.3",
|
||||
"tapable": "^2.3.0",
|
||||
"terser-webpack-plugin": "^5.3.11",
|
||||
"watchpack": "^2.4.1",
|
||||
"webpack-sources": "^3.2.3"
|
||||
"watchpack": "^2.4.4",
|
||||
"webpack-sources": "^3.3.3"
|
||||
},
|
||||
"bin": {
|
||||
"webpack": "bin/webpack.js"
|
||||
@ -2251,6 +2272,7 @@
|
||||
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "^0.5.0",
|
||||
"@webpack-cli/configtest": "^2.1.1",
|
||||
@ -2317,9 +2339,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-sources": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
|
||||
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
|
||||
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@ -16,9 +16,9 @@
|
||||
"css-loader": "7.1.2",
|
||||
"expose-loader": "5.0.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"sass": "1.89.2",
|
||||
"sass": "1.93.2",
|
||||
"sass-loader": "16.0.5",
|
||||
"webpack": "5.99.8",
|
||||
"webpack": "5.102.1",
|
||||
"webpack-cli": "5.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@ -14,7 +13,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@ -157,16 +156,18 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
"b@example.com"
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
|
||||
.Returns(GetSubscription(organization.GatewaySubscriptionId));
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId, Arg.Is<SubscriptionGetOptions>(
|
||||
options => options.Expand.Contains("customer")))
|
||||
.Returns(GetSubscription(organization.GatewaySubscriptionId, organization.GatewayCustomerId));
|
||||
|
||||
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
await stripeAdapter.Received(1).CustomerUpdateAsync(organization.GatewayCustomerId,
|
||||
Arg.Is<CustomerUpdateOptions>(options =>
|
||||
options.Coupon == string.Empty && options.Email == "a@example.com"));
|
||||
Arg.Is<CustomerUpdateOptions>(options => options.Email == "a@example.com"));
|
||||
|
||||
await stripeAdapter.Received(1).CustomerDeleteDiscountAsync(organization.GatewayCustomerId);
|
||||
|
||||
await stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
|
||||
Arg.Is<SubscriptionUpdateOptions>(options =>
|
||||
@ -206,7 +207,7 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
|
||||
organization.PlanType = PlanType.TeamsMonthly;
|
||||
|
||||
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
|
||||
|
||||
@ -295,7 +296,7 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
|
||||
organization.PlanType = PlanType.TeamsMonthly;
|
||||
|
||||
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
|
||||
|
||||
@ -332,9 +333,6 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
Id = "subscription_id"
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
||||
|
||||
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||
|
||||
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||
@ -372,10 +370,21 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == "a@example.com"));
|
||||
}
|
||||
|
||||
private static Subscription GetSubscription(string subscriptionId) =>
|
||||
private static Subscription GetSubscription(string subscriptionId, string customerId) =>
|
||||
new()
|
||||
{
|
||||
Id = subscriptionId,
|
||||
CustomerId = customerId,
|
||||
Customer = new Customer
|
||||
{
|
||||
Discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = "coupon-id"
|
||||
}
|
||||
}
|
||||
},
|
||||
Status = StripeConstants.SubscriptionStatus.Active,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
@ -407,7 +416,7 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
organization.PlanType = PlanType.TeamsMonthly;
|
||||
organization.Enabled = false; // Start with a disabled organization
|
||||
|
||||
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
using Bit.Commercial.Core.AdminConsole.Services;
|
||||
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@ -10,7 +9,7 @@ using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Context;
|
||||
@ -21,6 +20,7 @@ using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@ -42,7 +42,7 @@ public class ProviderServiceTests
|
||||
public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.CompleteSetupAsync(default, default, default, default, null));
|
||||
() => sutProvider.Sut.CompleteSetupAsync(default, default, default, default, null, null));
|
||||
Assert.Contains("Invalid owner.", exception.Message);
|
||||
}
|
||||
|
||||
@ -54,85 +54,12 @@ public class ProviderServiceTests
|
||||
userService.GetUserByIdAsync(user.Id).Returns(user);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default, null));
|
||||
() => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default, null, null));
|
||||
Assert.Contains("Invalid token.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteSetupAsync_InvalidTaxInfo_ThrowsBadRequestException(
|
||||
User user,
|
||||
Provider provider,
|
||||
string key,
|
||||
TaxInfo taxInfo,
|
||||
TokenizedPaymentSource tokenizedPaymentSource,
|
||||
[ProviderUser] ProviderUser providerUser,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
providerUser.ProviderId = provider.Id;
|
||||
providerUser.UserId = user.Id;
|
||||
var userService = sutProvider.GetDependency<IUserService>();
|
||||
userService.GetUserByIdAsync(user.Id).Returns(user);
|
||||
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
|
||||
|
||||
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
|
||||
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
||||
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(protector);
|
||||
|
||||
sutProvider.Create();
|
||||
|
||||
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
|
||||
taxInfo.BillingAddressCountry = null;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource));
|
||||
|
||||
Assert.Equal("Both address and postal code are required to set up your provider.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteSetupAsync_InvalidTokenizedPaymentSource_ThrowsBadRequestException(
|
||||
User user,
|
||||
Provider provider,
|
||||
string key,
|
||||
TaxInfo taxInfo,
|
||||
TokenizedPaymentSource tokenizedPaymentSource,
|
||||
[ProviderUser] ProviderUser providerUser,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
providerUser.ProviderId = provider.Id;
|
||||
providerUser.UserId = user.Id;
|
||||
var userService = sutProvider.GetDependency<IUserService>();
|
||||
userService.GetUserByIdAsync(user.Id).Returns(user);
|
||||
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
|
||||
|
||||
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
|
||||
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
||||
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(protector);
|
||||
|
||||
sutProvider.Create();
|
||||
|
||||
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
|
||||
tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay };
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource));
|
||||
|
||||
Assert.Equal("A payment method is required to set up your provider.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource,
|
||||
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TokenizedPaymentMethod tokenizedPaymentMethod, BillingAddress billingAddress,
|
||||
[ProviderUser] ProviderUser providerUser,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
@ -152,7 +79,7 @@ public class ProviderServiceTests
|
||||
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
|
||||
|
||||
var customer = new Customer { Id = "customer_id" };
|
||||
providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource).Returns(customer);
|
||||
providerBillingService.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress).Returns(customer);
|
||||
|
||||
var subscription = new Subscription { Id = "subscription_id" };
|
||||
providerBillingService.SetupSubscription(provider).Returns(subscription);
|
||||
@ -161,7 +88,7 @@ public class ProviderServiceTests
|
||||
|
||||
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
|
||||
await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource);
|
||||
await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, tokenizedPaymentMethod, billingAddress);
|
||||
|
||||
await sutProvider.GetDependency<IProviderRepository>().Received().UpsertAsync(Arg.Is<Provider>(
|
||||
p =>
|
||||
@ -885,12 +812,12 @@ public class ProviderServiceTests
|
||||
organization.Plan = "Enterprise (Monthly)";
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
var expectedPlanType = PlanType.EnterpriseMonthly2020;
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(expectedPlanType)
|
||||
.Returns(StaticStore.GetPlan(expectedPlanType));
|
||||
.Returns(MockPlans.Get(expectedPlanType));
|
||||
|
||||
var expectedPlanId = "2020-enterprise-org-seat-monthly";
|
||||
|
||||
@ -1194,7 +1121,7 @@ public class ProviderServiceTests
|
||||
private static SubscriptionUpdateOptions SubscriptionUpdateRequest(string expectedPlanId, Subscription subscriptionItem) =>
|
||||
new()
|
||||
{
|
||||
Items = new List<Stripe.SubscriptionItemOptions>
|
||||
Items = new List<SubscriptionItemOptions>
|
||||
{
|
||||
new() { Id = subscriptionItem.Id, Price = expectedPlanId },
|
||||
}
|
||||
|
||||
@ -0,0 +1,556 @@
|
||||
using Bit.Commercial.Core.Billing.Providers.Queries;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Stripe;
|
||||
using Stripe.Tax;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.Billing.Providers.Queries;
|
||||
|
||||
using static StripeConstants;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class GetProviderWarningsQueryTests
|
||||
{
|
||||
private static readonly string[] _requiredExpansions = ["customer.tax_ids"];
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NoSubscription_NoWarnings(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.ReturnsNull();
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
Suspension: null,
|
||||
TaxId: null
|
||||
});
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_ProviderEnabled_NoSuspensionWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Unpaid,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration> { Data = [] });
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.Null(response!.Suspension);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_Has_SuspensionWarning_AddPaymentMethod(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = false;
|
||||
var cancelAt = DateTime.UtcNow.AddDays(7);
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Unpaid,
|
||||
CancelAt = cancelAt,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration> { Data = [] });
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
Suspension.Resolution: "add_payment_method"
|
||||
});
|
||||
Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_Has_SuspensionWarning_ContactAdministrator(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = false;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Unpaid,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(false);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration> { Data = [] });
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
Suspension.Resolution: "contact_administrator"
|
||||
});
|
||||
Assert.Null(response.Suspension.SubscriptionCancelsAt);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_Has_SuspensionWarning_ContactSupport(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = false;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Canceled,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration> { Data = [] });
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
Suspension.Resolution: "contact_support"
|
||||
});
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NotProviderAdmin_NoTaxIdWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(false);
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.Null(response!.TaxId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NoTaxRegistrationForCountry_NoTaxIdWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "GB" }]
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.Null(response!.TaxId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_Has_TaxIdMissingWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "CA" }]
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
TaxId.Type: "tax_id_missing"
|
||||
});
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_TaxIdVerificationIsNull_NoTaxIdWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = [new TaxId { Verification = null }]
|
||||
},
|
||||
Address = new Address { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "CA" }]
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.Null(response!.TaxId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_Has_TaxIdPendingVerificationWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = [new TaxId
|
||||
{
|
||||
Verification = new TaxIdVerification
|
||||
{
|
||||
Status = TaxIdVerificationStatus.Pending
|
||||
}
|
||||
}]
|
||||
},
|
||||
Address = new Address { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "CA" }]
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
TaxId.Type: "tax_id_pending_verification"
|
||||
});
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_Has_TaxIdFailedVerificationWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = [new TaxId
|
||||
{
|
||||
Verification = new TaxIdVerification
|
||||
{
|
||||
Status = TaxIdVerificationStatus.Unverified
|
||||
}
|
||||
}]
|
||||
},
|
||||
Address = new Address { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "CA" }]
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
TaxId.Type: "tax_id_failed_verification"
|
||||
});
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_TaxIdVerified_NoTaxIdWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = [new TaxId
|
||||
{
|
||||
Verification = new TaxIdVerification
|
||||
{
|
||||
Status = TaxIdVerificationStatus.Verified
|
||||
}
|
||||
}]
|
||||
},
|
||||
Address = new Address { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "CA" }]
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.Null(response!.TaxId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_MultipleRegistrations_MatchesCorrectCountry(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "DE" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Is<RegistrationListOptions>(opt => opt.Status == TaxRegistrationStatus.Active))
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [
|
||||
new Registration { Country = "US" },
|
||||
new Registration { Country = "DE" },
|
||||
new Registration { Country = "FR" }
|
||||
]
|
||||
});
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Is<RegistrationListOptions>(opt => opt.Status == TaxRegistrationStatus.Scheduled))
|
||||
.Returns(new StripeList<Registration> { Data = [] });
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
TaxId.Type: "tax_id_missing"
|
||||
});
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_CombinesBothWarningTypes(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = false;
|
||||
var cancelAt = DateTime.UtcNow.AddDays(5);
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Unpaid,
|
||||
CancelAt = cancelAt,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "CA" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "CA" }]
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
Suspension.Resolution: "add_payment_method",
|
||||
TaxId.Type: "tax_id_missing"
|
||||
});
|
||||
Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_USCustomer_NoTaxIdWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "US" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "US" }]
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.Null(response!.TaxId);
|
||||
}
|
||||
}
|
||||
@ -11,12 +11,14 @@ using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Entities;
|
||||
using Bit.Core.Billing.Providers.Repositories;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
@ -71,7 +73,7 @@ public class BusinessUnitConverterTests
|
||||
{
|
||||
organization.PlanType = PlanType.EnterpriseAnnually2020;
|
||||
|
||||
var enterpriseAnnually2020 = StaticStore.GetPlan(PlanType.EnterpriseAnnually2020);
|
||||
var enterpriseAnnually2020 = MockPlans.Get(PlanType.EnterpriseAnnually2020);
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
@ -133,7 +135,7 @@ public class BusinessUnitConverterTests
|
||||
_pricingClient.GetPlanOrThrow(PlanType.EnterpriseAnnually2020)
|
||||
.Returns(enterpriseAnnually2020);
|
||||
|
||||
var enterpriseAnnually = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
var enterpriseAnnually = MockPlans.Get(PlanType.EnterpriseAnnually);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(PlanType.EnterpriseAnnually)
|
||||
.Returns(enterpriseAnnually);
|
||||
@ -241,7 +243,7 @@ public class BusinessUnitConverterTests
|
||||
argument.Status == ProviderStatusType.Pending &&
|
||||
argument.Type == ProviderType.BusinessUnit)).Returns(provider);
|
||||
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = MockPlans.Get(organization.PlanType);
|
||||
|
||||
_pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan);
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using Bit.Commercial.Core.Billing.Providers.Models;
|
||||
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@ -11,21 +9,20 @@ using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Entities;
|
||||
using Bit.Core.Billing.Providers.Models;
|
||||
using Bit.Core.Billing.Providers.Repositories;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Braintree;
|
||||
@ -143,7 +140,7 @@ public class ProviderBillingServiceTests
|
||||
.Returns(existingPlan);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(existingPlan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(existingPlan.PlanType));
|
||||
.Returns(MockPlans.Get(existingPlan.PlanType));
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider)
|
||||
.Returns(new Subscription
|
||||
@ -158,7 +155,7 @@ public class ProviderBillingServiceTests
|
||||
Id = "si_ent_annual",
|
||||
Price = new Price
|
||||
{
|
||||
Id = StaticStore.GetPlan(PlanType.EnterpriseAnnually).PasswordManager
|
||||
Id = MockPlans.Get(PlanType.EnterpriseAnnually).PasswordManager
|
||||
.StripeProviderPortalSeatPlanId
|
||||
},
|
||||
Quantity = 10
|
||||
@ -171,7 +168,7 @@ public class ProviderBillingServiceTests
|
||||
new ChangeProviderPlanCommand(provider, providerPlanId, PlanType.EnterpriseMonthly);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(command.NewPlan)
|
||||
.Returns(StaticStore.GetPlan(command.NewPlan));
|
||||
.Returns(MockPlans.Get(command.NewPlan));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ChangePlan(command);
|
||||
@ -188,7 +185,7 @@ public class ProviderBillingServiceTests
|
||||
Arg.Is<SubscriptionUpdateOptions>(p =>
|
||||
p.Items.Count(si => si.Id == "si_ent_annual" && si.Deleted == true) == 1));
|
||||
|
||||
var newPlanCfg = StaticStore.GetPlan(command.NewPlan);
|
||||
var newPlanCfg = MockPlans.Get(command.NewPlan);
|
||||
await stripeAdapter.Received(1)
|
||||
.SubscriptionUpdateAsync(
|
||||
Arg.Is(provider.GatewaySubscriptionId),
|
||||
@ -352,9 +349,6 @@ public class ProviderBillingServiceTests
|
||||
CloudRegion = "US"
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
|
||||
options =>
|
||||
options.Address.Country == providerCustomer.Address.Country &&
|
||||
@ -497,7 +491,7 @@ public class ProviderBillingServiceTests
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
.Returns(MockPlans.Get(plan.PlanType));
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
@ -520,7 +514,7 @@ public class ProviderBillingServiceTests
|
||||
sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);
|
||||
|
||||
// 50 seats currently assigned with a seat minimum of 100
|
||||
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
|
||||
|
||||
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
|
||||
[
|
||||
@ -579,7 +573,7 @@ public class ProviderBillingServiceTests
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
.Returns(MockPlans.Get(plan.PlanType));
|
||||
}
|
||||
|
||||
var providerPlan = providerPlans.First();
|
||||
@ -604,7 +598,7 @@ public class ProviderBillingServiceTests
|
||||
sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);
|
||||
|
||||
// 95 seats currently assigned with a seat minimum of 100
|
||||
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
|
||||
|
||||
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
|
||||
[
|
||||
@ -667,7 +661,7 @@ public class ProviderBillingServiceTests
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
.Returns(MockPlans.Get(plan.PlanType));
|
||||
}
|
||||
|
||||
var providerPlan = providerPlans.First();
|
||||
@ -692,7 +686,7 @@ public class ProviderBillingServiceTests
|
||||
sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);
|
||||
|
||||
// 110 seats currently assigned with a seat minimum of 100
|
||||
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
|
||||
|
||||
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
|
||||
[
|
||||
@ -755,7 +749,7 @@ public class ProviderBillingServiceTests
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
.Returns(MockPlans.Get(plan.PlanType));
|
||||
}
|
||||
|
||||
var providerPlan = providerPlans.First();
|
||||
@ -780,7 +774,7 @@ public class ProviderBillingServiceTests
|
||||
sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);
|
||||
|
||||
// 110 seats currently assigned with a seat minimum of 100
|
||||
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
|
||||
|
||||
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
|
||||
[
|
||||
@ -833,13 +827,13 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(planType).Returns(StaticStore.GetPlan(planType));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(planType).Returns(MockPlans.Get(planType));
|
||||
|
||||
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
|
||||
[
|
||||
new ProviderOrganizationOrganizationDetails
|
||||
{
|
||||
Plan = StaticStore.GetPlan(planType).Name,
|
||||
Plan = MockPlans.Get(planType).Name,
|
||||
Status = OrganizationStatusType.Managed,
|
||||
Seats = 5
|
||||
}
|
||||
@ -871,13 +865,13 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(planType).Returns(StaticStore.GetPlan(planType));
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(planType).Returns(MockPlans.Get(planType));
|
||||
|
||||
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
|
||||
[
|
||||
new ProviderOrganizationOrganizationDetails
|
||||
{
|
||||
Plan = StaticStore.GetPlan(planType).Name,
|
||||
Plan = MockPlans.Get(planType).Name,
|
||||
Status = OrganizationStatusType.Managed,
|
||||
Seats = 15
|
||||
}
|
||||
@ -898,208 +892,97 @@ public class ProviderBillingServiceTests
|
||||
#region SetupCustomer
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetupCustomer_MissingCountry_ContactSupport(
|
||||
public async Task SetupCustomer_NullPaymentMethod_ThrowsNullReferenceException(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
taxInfo.BillingAddressCountry = null;
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo));
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.CustomerGetAsync(Arg.Any<string>(), Arg.Any<CustomerGetOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetupCustomer_MissingPostalCode_ContactSupport(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
{
|
||||
taxInfo.BillingAddressCountry = null;
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo));
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.CustomerGetAsync(Arg.Any<string>(), Arg.Any<CustomerGetOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetupCustomer_NoPaymentMethod_Success(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
{
|
||||
provider.Name = "MSP";
|
||||
|
||||
sutProvider.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(
|
||||
p => p == taxInfo.BillingAddressCountry),
|
||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns(taxInfo.TaxIdType);
|
||||
|
||||
taxInfo.BillingAddressCountry = "AD";
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
var expected = new Customer
|
||||
{
|
||||
Id = "customer_id",
|
||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||
};
|
||||
|
||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
o.Address.Country == taxInfo.BillingAddressCountry &&
|
||||
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||
o.Address.City == taxInfo.BillingAddressCity &&
|
||||
o.Address.State == taxInfo.BillingAddressState &&
|
||||
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
|
||||
o.Email == provider.BillingEmail &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
||||
o.Metadata["region"] == "" &&
|
||||
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
||||
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
||||
.Returns(expected);
|
||||
|
||||
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo);
|
||||
|
||||
Assert.Equivalent(expected, actual);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetupCustomer_InvalidRequiredPaymentMethod_ThrowsBillingException(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider,
|
||||
TaxInfo taxInfo,
|
||||
TokenizedPaymentSource tokenizedPaymentSource)
|
||||
{
|
||||
provider.Name = "MSP";
|
||||
|
||||
sutProvider.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(
|
||||
p => p == taxInfo.BillingAddressCountry),
|
||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns(taxInfo.TaxIdType);
|
||||
|
||||
taxInfo.BillingAddressCountry = "AD";
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
|
||||
tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay };
|
||||
|
||||
await ThrowsBillingExceptionAsync(() =>
|
||||
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
|
||||
await Assert.ThrowsAsync<NullReferenceException>(() =>
|
||||
sutProvider.Sut.SetupCustomer(provider, null, billingAddress));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetupCustomer_WithBankAccount_Error_Reverts(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
provider.Name = "MSP";
|
||||
|
||||
sutProvider.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(
|
||||
p => p == taxInfo.BillingAddressCountry),
|
||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns(taxInfo.TaxIdType);
|
||||
|
||||
taxInfo.BillingAddressCountry = "AD";
|
||||
billingAddress.Country = "AD";
|
||||
billingAddress.TaxId = new TaxID("es_nif", "12345678Z");
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token");
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = "token" };
|
||||
|
||||
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
|
||||
options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([
|
||||
options.PaymentMethod == tokenizedPaymentMethod.Token)).Returns([
|
||||
new SetupIntent { Id = "setup_intent_id" }
|
||||
]);
|
||||
|
||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
o.Address.Country == taxInfo.BillingAddressCountry &&
|
||||
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||
o.Address.City == taxInfo.BillingAddressCity &&
|
||||
o.Address.State == taxInfo.BillingAddressState &&
|
||||
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
|
||||
o.Address.Country == billingAddress.Country &&
|
||||
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||
o.Address.Line1 == billingAddress.Line1 &&
|
||||
o.Address.Line2 == billingAddress.Line2 &&
|
||||
o.Address.City == billingAddress.City &&
|
||||
o.Address.State == billingAddress.State &&
|
||||
o.Description == provider.DisplayBusinessName() &&
|
||||
o.Email == provider.BillingEmail &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
|
||||
o.Metadata["region"] == "" &&
|
||||
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
||||
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
||||
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
|
||||
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))
|
||||
.Throws<StripeException>();
|
||||
|
||||
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns("setup_intent_id");
|
||||
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns("setup_intent_id");
|
||||
|
||||
await Assert.ThrowsAsync<StripeException>(() =>
|
||||
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
|
||||
sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress));
|
||||
|
||||
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_id");
|
||||
|
||||
await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_id", Arg.Is<SetupIntentCancelOptions>(options =>
|
||||
options.CancellationReason == "abandoned"));
|
||||
|
||||
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Remove(provider.Id);
|
||||
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).RemoveSetupIntentForSubscriber(provider.Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetupCustomer_WithPayPal_Error_Reverts(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
provider.Name = "MSP";
|
||||
|
||||
sutProvider.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(
|
||||
p => p == taxInfo.BillingAddressCountry),
|
||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns(taxInfo.TaxIdType);
|
||||
|
||||
taxInfo.BillingAddressCountry = "AD";
|
||||
billingAddress.Country = "AD";
|
||||
billingAddress.TaxId = new TaxID("es_nif", "12345678Z");
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.PayPal, Token = "token" };
|
||||
|
||||
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token");
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token)
|
||||
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentMethod.Token)
|
||||
.Returns("braintree_customer_id");
|
||||
|
||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
o.Address.Country == taxInfo.BillingAddressCountry &&
|
||||
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||
o.Address.City == taxInfo.BillingAddressCity &&
|
||||
o.Address.State == taxInfo.BillingAddressState &&
|
||||
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
|
||||
o.Address.Country == billingAddress.Country &&
|
||||
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||
o.Address.Line1 == billingAddress.Line1 &&
|
||||
o.Address.Line2 == billingAddress.Line2 &&
|
||||
o.Address.City == billingAddress.City &&
|
||||
o.Address.State == billingAddress.State &&
|
||||
o.Description == provider.DisplayBusinessName() &&
|
||||
o.Email == provider.BillingEmail &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
|
||||
o.Metadata["region"] == "" &&
|
||||
o.Metadata["btCustomerId"] == "braintree_customer_id" &&
|
||||
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
||||
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
||||
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
|
||||
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))
|
||||
.Throws<StripeException>();
|
||||
|
||||
await Assert.ThrowsAsync<StripeException>(() =>
|
||||
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
|
||||
sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress));
|
||||
|
||||
await sutProvider.GetDependency<IBraintreeGateway>().Customer.Received(1).DeleteAsync("braintree_customer_id");
|
||||
}
|
||||
@ -1108,17 +991,11 @@ public class ProviderBillingServiceTests
|
||||
public async Task SetupCustomer_WithBankAccount_Success(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
provider.Name = "MSP";
|
||||
|
||||
sutProvider.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(
|
||||
p => p == taxInfo.BillingAddressCountry),
|
||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns(taxInfo.TaxIdType);
|
||||
|
||||
taxInfo.BillingAddressCountry = "AD";
|
||||
billingAddress.Country = "AD";
|
||||
billingAddress.TaxId = new TaxID("es_nif", "12345678Z");
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
@ -1128,33 +1005,30 @@ public class ProviderBillingServiceTests
|
||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||
};
|
||||
|
||||
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token");
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = "token" };
|
||||
|
||||
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
|
||||
options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([
|
||||
options.PaymentMethod == tokenizedPaymentMethod.Token)).Returns([
|
||||
new SetupIntent { Id = "setup_intent_id" }
|
||||
]);
|
||||
|
||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
o.Address.Country == taxInfo.BillingAddressCountry &&
|
||||
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||
o.Address.City == taxInfo.BillingAddressCity &&
|
||||
o.Address.State == taxInfo.BillingAddressState &&
|
||||
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
|
||||
o.Address.Country == billingAddress.Country &&
|
||||
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||
o.Address.Line1 == billingAddress.Line1 &&
|
||||
o.Address.Line2 == billingAddress.Line2 &&
|
||||
o.Address.City == billingAddress.City &&
|
||||
o.Address.State == billingAddress.State &&
|
||||
o.Description == provider.DisplayBusinessName() &&
|
||||
o.Email == provider.BillingEmail &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
|
||||
o.Metadata["region"] == "" &&
|
||||
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
||||
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
||||
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
|
||||
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))
|
||||
.Returns(expected);
|
||||
|
||||
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
|
||||
var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress);
|
||||
|
||||
Assert.Equivalent(expected, actual);
|
||||
|
||||
@ -1165,17 +1039,11 @@ public class ProviderBillingServiceTests
|
||||
public async Task SetupCustomer_WithPayPal_Success(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
provider.Name = "MSP";
|
||||
|
||||
sutProvider.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(
|
||||
p => p == taxInfo.BillingAddressCountry),
|
||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns(taxInfo.TaxIdType);
|
||||
|
||||
taxInfo.BillingAddressCountry = "AD";
|
||||
billingAddress.Country = "AD";
|
||||
billingAddress.TaxId = new TaxID("es_nif", "12345678Z");
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
@ -1185,32 +1053,29 @@ public class ProviderBillingServiceTests
|
||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||
};
|
||||
|
||||
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token");
|
||||
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.PayPal, Token = "token" };
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token)
|
||||
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentMethod.Token)
|
||||
.Returns("braintree_customer_id");
|
||||
|
||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
o.Address.Country == taxInfo.BillingAddressCountry &&
|
||||
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||
o.Address.City == taxInfo.BillingAddressCity &&
|
||||
o.Address.State == taxInfo.BillingAddressState &&
|
||||
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
|
||||
o.Address.Country == billingAddress.Country &&
|
||||
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||
o.Address.Line1 == billingAddress.Line1 &&
|
||||
o.Address.Line2 == billingAddress.Line2 &&
|
||||
o.Address.City == billingAddress.City &&
|
||||
o.Address.State == billingAddress.State &&
|
||||
o.Description == provider.DisplayBusinessName() &&
|
||||
o.Email == provider.BillingEmail &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
|
||||
o.Metadata["region"] == "" &&
|
||||
o.Metadata["btCustomerId"] == "braintree_customer_id" &&
|
||||
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
||||
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
||||
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
|
||||
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))
|
||||
.Returns(expected);
|
||||
|
||||
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
|
||||
var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress);
|
||||
|
||||
Assert.Equivalent(expected, actual);
|
||||
}
|
||||
@ -1219,17 +1084,11 @@ public class ProviderBillingServiceTests
|
||||
public async Task SetupCustomer_WithCard_Success(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
provider.Name = "MSP";
|
||||
|
||||
sutProvider.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(
|
||||
p => p == taxInfo.BillingAddressCountry),
|
||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns(taxInfo.TaxIdType);
|
||||
|
||||
taxInfo.BillingAddressCountry = "AD";
|
||||
billingAddress.Country = "AD";
|
||||
billingAddress.TaxId = new TaxID("es_nif", "12345678Z");
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
@ -1239,30 +1098,26 @@ public class ProviderBillingServiceTests
|
||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||
};
|
||||
|
||||
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token");
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" };
|
||||
|
||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
o.Address.Country == taxInfo.BillingAddressCountry &&
|
||||
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||
o.Address.City == taxInfo.BillingAddressCity &&
|
||||
o.Address.State == taxInfo.BillingAddressState &&
|
||||
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
|
||||
o.Address.Country == billingAddress.Country &&
|
||||
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||
o.Address.Line1 == billingAddress.Line1 &&
|
||||
o.Address.Line2 == billingAddress.Line2 &&
|
||||
o.Address.City == billingAddress.City &&
|
||||
o.Address.State == billingAddress.State &&
|
||||
o.Description == provider.DisplayBusinessName() &&
|
||||
o.Email == provider.BillingEmail &&
|
||||
o.PaymentMethod == tokenizedPaymentSource.Token &&
|
||||
o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentSource.Token &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
||||
o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentMethod.Token &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
|
||||
o.Metadata["region"] == "" &&
|
||||
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
||||
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
||||
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
|
||||
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))
|
||||
.Returns(expected);
|
||||
|
||||
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
|
||||
var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress);
|
||||
|
||||
Assert.Equivalent(expected, actual);
|
||||
}
|
||||
@ -1271,17 +1126,11 @@ public class ProviderBillingServiceTests
|
||||
public async Task SetupCustomer_WithCard_ReverseCharge_Success(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
provider.Name = "MSP";
|
||||
|
||||
sutProvider.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(
|
||||
p => p == taxInfo.BillingAddressCountry),
|
||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns(taxInfo.TaxIdType);
|
||||
|
||||
taxInfo.BillingAddressCountry = "AD";
|
||||
billingAddress.Country = "FR"; // Non-US country to trigger reverse charge
|
||||
billingAddress.TaxId = new TaxID("fr_siren", "123456789");
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
@ -1291,59 +1140,51 @@ public class ProviderBillingServiceTests
|
||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||
};
|
||||
|
||||
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token");
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
||||
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" };
|
||||
|
||||
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||
o.Address.Country == taxInfo.BillingAddressCountry &&
|
||||
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||
o.Address.City == taxInfo.BillingAddressCity &&
|
||||
o.Address.State == taxInfo.BillingAddressState &&
|
||||
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
|
||||
o.Address.Country == billingAddress.Country &&
|
||||
o.Address.PostalCode == billingAddress.PostalCode &&
|
||||
o.Address.Line1 == billingAddress.Line1 &&
|
||||
o.Address.Line2 == billingAddress.Line2 &&
|
||||
o.Address.City == billingAddress.City &&
|
||||
o.Address.State == billingAddress.State &&
|
||||
o.Description == provider.DisplayBusinessName() &&
|
||||
o.Email == provider.BillingEmail &&
|
||||
o.PaymentMethod == tokenizedPaymentSource.Token &&
|
||||
o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentSource.Token &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
||||
o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentMethod.Token &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
|
||||
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
|
||||
o.Metadata["region"] == "" &&
|
||||
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
||||
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber &&
|
||||
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
|
||||
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value &&
|
||||
o.TaxExempt == StripeConstants.TaxExempt.Reverse))
|
||||
.Returns(expected);
|
||||
|
||||
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
|
||||
var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress);
|
||||
|
||||
Assert.Equivalent(expected, actual);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
|
||||
public async Task SetupCustomer_WithInvalidTaxId_ThrowsBadRequestException(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
provider.Name = "MSP";
|
||||
billingAddress.Country = "AD";
|
||||
billingAddress.TaxId = new TaxID("es_nif", "invalid_tax_id");
|
||||
|
||||
taxInfo.BillingAddressCountry = "AD";
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" };
|
||||
|
||||
sutProvider.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(
|
||||
p => p == taxInfo.BillingAddressCountry),
|
||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns((string)null);
|
||||
stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>())
|
||||
.Throws(new StripeException("Invalid tax ID") { StripeError = new StripeError { Code = "tax_id_invalid" } });
|
||||
|
||||
var actual = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.SetupCustomer(provider, taxInfo));
|
||||
await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress));
|
||||
|
||||
Assert.IsType<BadRequestException>(actual);
|
||||
Assert.Equal("billingTaxIdTypeInferenceError", actual.Message);
|
||||
Assert.Equal("Your tax ID wasn't recognized for your selected country. Please ensure your country and tax ID are valid.", actual.Message);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -1397,7 +1238,7 @@ public class ProviderBillingServiceTests
|
||||
.Returns(providerPlans);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.EnterpriseMonthly)
|
||||
.Returns(StaticStore.GetPlan(PlanType.EnterpriseMonthly));
|
||||
.Returns(MockPlans.Get(PlanType.EnterpriseMonthly));
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider));
|
||||
|
||||
@ -1425,7 +1266,7 @@ public class ProviderBillingServiceTests
|
||||
.Returns(providerPlans);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly)
|
||||
.Returns(StaticStore.GetPlan(PlanType.TeamsMonthly));
|
||||
.Returns(MockPlans.Get(PlanType.TeamsMonthly));
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider));
|
||||
|
||||
@ -1476,7 +1317,7 @@ public class ProviderBillingServiceTests
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
.Returns(MockPlans.Get(plan.PlanType));
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
|
||||
@ -1532,7 +1373,7 @@ public class ProviderBillingServiceTests
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
.Returns(MockPlans.Get(plan.PlanType));
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
|
||||
@ -1608,7 +1449,7 @@ public class ProviderBillingServiceTests
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
.Returns(MockPlans.Get(plan.PlanType));
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
|
||||
@ -1616,8 +1457,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||
sub =>
|
||||
@ -1686,7 +1525,7 @@ public class ProviderBillingServiceTests
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
.Returns(MockPlans.Get(plan.PlanType));
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
|
||||
@ -1694,12 +1533,10 @@ public class ProviderBillingServiceTests
|
||||
|
||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
|
||||
const string setupIntentId = "seti_123";
|
||||
|
||||
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntentId);
|
||||
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntentId);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntentId, Arg.Is<SetupIntentGetOptions>(options =>
|
||||
options.Expand.Contains("payment_method"))).Returns(new SetupIntent
|
||||
@ -1789,7 +1626,7 @@ public class ProviderBillingServiceTests
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
.Returns(MockPlans.Get(plan.PlanType));
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
|
||||
@ -1797,8 +1634,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||
sub =>
|
||||
@ -1869,7 +1704,7 @@ public class ProviderBillingServiceTests
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
.Returns(MockPlans.Get(plan.PlanType));
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
|
||||
@ -1877,11 +1712,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||
sub =>
|
||||
@ -1942,8 +1772,8 @@ public class ProviderBillingServiceTests
|
||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||
const string teamsLineItemId = "teams_line_item_id";
|
||||
|
||||
var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
@ -1976,7 +1806,7 @@ public class ProviderBillingServiceTests
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
.Returns(MockPlans.Get(plan.PlanType));
|
||||
}
|
||||
|
||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
@ -2022,8 +1852,8 @@ public class ProviderBillingServiceTests
|
||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||
const string teamsLineItemId = "teams_line_item_id";
|
||||
|
||||
var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
@ -2056,7 +1886,7 @@ public class ProviderBillingServiceTests
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
.Returns(MockPlans.Get(plan.PlanType));
|
||||
}
|
||||
|
||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
@ -2102,8 +1932,8 @@ public class ProviderBillingServiceTests
|
||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||
const string teamsLineItemId = "teams_line_item_id";
|
||||
|
||||
var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
@ -2136,7 +1966,7 @@ public class ProviderBillingServiceTests
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
.Returns(MockPlans.Get(plan.PlanType));
|
||||
}
|
||||
|
||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
@ -2176,8 +2006,8 @@ public class ProviderBillingServiceTests
|
||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||
const string teamsLineItemId = "teams_line_item_id";
|
||||
|
||||
var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
@ -2210,7 +2040,7 @@ public class ProviderBillingServiceTests
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
.Returns(MockPlans.Get(plan.PlanType));
|
||||
}
|
||||
|
||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
@ -2256,8 +2086,8 @@ public class ProviderBillingServiceTests
|
||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||
const string teamsLineItemId = "teams_line_item_id";
|
||||
|
||||
var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
@ -2290,7 +2120,7 @@ public class ProviderBillingServiceTests
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
.Returns(MockPlans.Get(plan.PlanType));
|
||||
}
|
||||
|
||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
@ -1,7 +1,7 @@
|
||||
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
@ -6,7 +6,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@ -69,7 +69,7 @@ public class MaxProjectsQueryTests
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
|
||||
|
||||
@ -114,7 +114,7 @@ public class MaxProjectsQueryTests
|
||||
.Returns(projects);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
.Returns(MockPlans.Get(organization.PlanType));
|
||||
|
||||
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);
|
||||
|
||||
|
||||
@ -0,0 +1,130 @@
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.Repositories;
|
||||
|
||||
public class SecretVersionRepositoryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SecretVersion_EntityCreation_Success(SecretVersion secretVersion)
|
||||
{
|
||||
// Arrange & Act
|
||||
secretVersion.SetNewId();
|
||||
|
||||
// Assert
|
||||
Assert.NotEqual(Guid.Empty, secretVersion.Id);
|
||||
Assert.NotEqual(Guid.Empty, secretVersion.SecretId);
|
||||
Assert.NotNull(secretVersion.Value);
|
||||
Assert.NotEqual(default, secretVersion.VersionDate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SecretVersion_WithServiceAccountEditor_Success(SecretVersion secretVersion, Guid serviceAccountId)
|
||||
{
|
||||
// Arrange & Act
|
||||
secretVersion.EditorServiceAccountId = serviceAccountId;
|
||||
secretVersion.EditorOrganizationUserId = null;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(serviceAccountId, secretVersion.EditorServiceAccountId);
|
||||
Assert.Null(secretVersion.EditorOrganizationUserId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SecretVersion_WithOrganizationUserEditor_Success(SecretVersion secretVersion, Guid organizationUserId)
|
||||
{
|
||||
// Arrange & Act
|
||||
secretVersion.EditorOrganizationUserId = organizationUserId;
|
||||
secretVersion.EditorServiceAccountId = null;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(organizationUserId, secretVersion.EditorOrganizationUserId);
|
||||
Assert.Null(secretVersion.EditorServiceAccountId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SecretVersion_NullableEditors_Success(SecretVersion secretVersion)
|
||||
{
|
||||
// Arrange & Act
|
||||
secretVersion.EditorServiceAccountId = null;
|
||||
secretVersion.EditorOrganizationUserId = null;
|
||||
|
||||
// Assert
|
||||
Assert.Null(secretVersion.EditorServiceAccountId);
|
||||
Assert.Null(secretVersion.EditorOrganizationUserId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SecretVersion_VersionDateSet_Success(SecretVersion secretVersion)
|
||||
{
|
||||
// Arrange
|
||||
var versionDate = DateTime.UtcNow;
|
||||
|
||||
// Act
|
||||
secretVersion.VersionDate = versionDate;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(versionDate, secretVersion.VersionDate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SecretVersion_ValueEncrypted_Success(SecretVersion secretVersion, string encryptedValue)
|
||||
{
|
||||
// Arrange & Act
|
||||
secretVersion.Value = encryptedValue;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(encryptedValue, secretVersion.Value);
|
||||
Assert.NotEmpty(secretVersion.Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SecretVersion_MultipleVersions_DifferentIds(List<SecretVersion> secretVersions, Guid secretId)
|
||||
{
|
||||
// Arrange & Act
|
||||
foreach (var version in secretVersions)
|
||||
{
|
||||
version.SecretId = secretId;
|
||||
version.SetNewId();
|
||||
}
|
||||
|
||||
// Assert
|
||||
var distinctIds = secretVersions.Select(v => v.Id).Distinct();
|
||||
Assert.Equal(secretVersions.Count, distinctIds.Count());
|
||||
Assert.All(secretVersions, v => Assert.Equal(secretId, v.SecretId));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void SecretVersion_VersionDateOrdering_Success(SecretVersion version1, SecretVersion version2, SecretVersion version3, Guid secretId)
|
||||
{
|
||||
// Arrange
|
||||
var now = DateTime.UtcNow;
|
||||
version1.SecretId = secretId;
|
||||
version1.VersionDate = now.AddDays(-2);
|
||||
|
||||
version2.SecretId = secretId;
|
||||
version2.VersionDate = now.AddDays(-1);
|
||||
|
||||
version3.SecretId = secretId;
|
||||
version3.VersionDate = now;
|
||||
|
||||
var versions = new List<SecretVersion> { version2, version3, version1 };
|
||||
|
||||
// Act
|
||||
var orderedVersions = versions.OrderByDescending(v => v.VersionDate).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(version3.Id, orderedVersions[0].Id); // Most recent
|
||||
Assert.Equal(version2.Id, orderedVersions[1].Id);
|
||||
Assert.Equal(version1.Id, orderedVersions[2].Id); // Oldest
|
||||
}
|
||||
}
|
||||
1140
bitwarden_license/test/SSO.Test/Controllers/AccountControllerTest.cs
Normal file
1140
bitwarden_license/test/SSO.Test/Controllers/AccountControllerTest.cs
Normal file
File diff suppressed because it is too large
Load Diff
35
bitwarden_license/test/SSO.Test/SSO.Test.csproj
Normal file
35
bitwarden_license/test/SSO.Test/SSO.Test.csproj
Normal file
@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="$(CoverletCollectorVersion)">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Sso\Sso.csproj" />
|
||||
<ProjectReference Include="..\..\..\test\Common\Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -200,6 +200,38 @@ public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsy
|
||||
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetList_SearchDisplayNameWithoutOptionalParameters_Success()
|
||||
{
|
||||
string filter = "displayName eq Test Group 2";
|
||||
int? itemsPerPage = null;
|
||||
int? startIndex = null;
|
||||
var expectedResponse = new ScimListResponseModel<ScimGroupResponseModel>
|
||||
{
|
||||
ItemsPerPage = 50, //default value
|
||||
TotalResults = 1,
|
||||
StartIndex = 1, //default value
|
||||
Resources = new List<ScimGroupResponseModel>
|
||||
{
|
||||
new ScimGroupResponseModel
|
||||
{
|
||||
Id = ScimApplicationFactory.TestGroupId2,
|
||||
DisplayName = "Test Group 2",
|
||||
ExternalId = "B",
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaGroup }
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaListResponse }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsGetListAsync(ScimApplicationFactory.TestOrganizationId1, filter, itemsPerPage, startIndex);
|
||||
|
||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||
|
||||
var responseModel = JsonSerializer.Deserialize<ScimListResponseModel<ScimGroupResponseModel>>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Post_Success()
|
||||
{
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
{
|
||||
"globalSettings": {
|
||||
"baseServiceUri": {
|
||||
"vault": "https://localhost:8080",
|
||||
"api": "http://localhost:4000",
|
||||
"identity": "http://localhost:33656",
|
||||
"admin": "http://localhost:62911",
|
||||
"notifications": "http://localhost:61840",
|
||||
"sso": "http://localhost:51822",
|
||||
"internalNotifications": "http://localhost:61840",
|
||||
"internalAdmin": "http://localhost:62911",
|
||||
"internalIdentity": "http://localhost:33656",
|
||||
"internalApi": "http://localhost:4000",
|
||||
"internalVault": "https://localhost:8080",
|
||||
"internalSso": "http://localhost:51822",
|
||||
"internalScim": "http://localhost:44559"
|
||||
},
|
||||
"mail": {
|
||||
"smtp": {
|
||||
"host": "localhost",
|
||||
"port": 10250
|
||||
}
|
||||
},
|
||||
"attachment": {
|
||||
"connectionString": "UseDevelopmentStorage=true",
|
||||
"baseUrl": "http://localhost:4000/attachments/"
|
||||
},
|
||||
"events": {
|
||||
"connectionString": "UseDevelopmentStorage=true"
|
||||
},
|
||||
"storage": {
|
||||
"connectionString": "UseDevelopmentStorage=true"
|
||||
},
|
||||
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Scim.Groups;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
@ -24,7 +25,7 @@ public class GetGroupsListCommandTests
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, null, count, startIndex);
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Count = count, StartIndex = startIndex });
|
||||
|
||||
AssertHelper.AssertPropertyEqual(groups.Skip(startIndex - 1).Take(count).ToList(), result.groupList);
|
||||
AssertHelper.AssertPropertyEqual(groups.Count, result.totalResults);
|
||||
@ -47,7 +48,7 @@ public class GetGroupsListCommandTests
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, filter, null, null);
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Filter = filter });
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);
|
||||
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
|
||||
@ -67,7 +68,7 @@ public class GetGroupsListCommandTests
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, filter, null, null);
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Filter = filter });
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);
|
||||
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
|
||||
@ -90,7 +91,7 @@ public class GetGroupsListCommandTests
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, filter, null, null);
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Filter = filter });
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);
|
||||
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
|
||||
@ -112,7 +113,7 @@ public class GetGroupsListCommandTests
|
||||
.GetManyByOrganizationIdAsync(organizationId)
|
||||
.Returns(groups);
|
||||
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, filter, null, null);
|
||||
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Filter = filter });
|
||||
|
||||
AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);
|
||||
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
|
||||
|
||||
@ -436,7 +436,7 @@ public class PatchGroupCommandTests
|
||||
await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);
|
||||
|
||||
// Assert: logging
|
||||
sutProvider.GetDependency<ILogger<PatchGroupCommand>>().ReceivedWithAnyArgs().LogWarning(default);
|
||||
sutProvider.GetDependency<ILogger<PatchGroupCommand>>().ReceivedWithAnyArgs().LogWarning("");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Users;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Users;
|
||||
using Bit.Scim.Utilities;
|
||||
@ -101,7 +101,7 @@ public class PatchUserCommandTests
|
||||
|
||||
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM);
|
||||
await sutProvider.GetDependency<IRevokeOrganizationUserCommand>().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -129,7 +129,7 @@ public class PatchUserCommandTests
|
||||
|
||||
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM);
|
||||
await sutProvider.GetDependency<IRevokeOrganizationUserCommand>().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -149,7 +149,7 @@ public class PatchUserCommandTests
|
||||
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IRestoreOrganizationUserCommand>().DidNotReceiveWithAnyArgs().RestoreUserAsync(default, EventSystemUser.SCIM);
|
||||
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().RevokeUserAsync(default, EventSystemUser.SCIM);
|
||||
await sutProvider.GetDependency<IRevokeOrganizationUserCommand>().DidNotReceiveWithAnyArgs().RevokeUserAsync(default, EventSystemUser.SCIM);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
@ -53,10 +53,10 @@ services:
|
||||
- ./.data/postgres/log:/var/log/postgresql
|
||||
profiles:
|
||||
- postgres
|
||||
- ef
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: bw-mysql
|
||||
ports:
|
||||
- "3306:3306"
|
||||
command:
|
||||
@ -69,6 +69,7 @@ services:
|
||||
- mysql_dev_data:/var/lib/mysql
|
||||
profiles:
|
||||
- mysql
|
||||
- ef
|
||||
|
||||
mariadb:
|
||||
image: mariadb:10
|
||||
@ -76,17 +77,16 @@ services:
|
||||
- 4306:3306
|
||||
environment:
|
||||
MARIADB_USER: maria
|
||||
MARIADB_PASSWORD: ${MARIADB_ROOT_PASSWORD}
|
||||
MARIADB_DATABASE: vault_dev
|
||||
MARIADB_RANDOM_ROOT_PASSWORD: "true"
|
||||
volumes:
|
||||
- mariadb_dev_data:/var/lib/mysql
|
||||
profiles:
|
||||
- mariadb
|
||||
- ef
|
||||
|
||||
idp:
|
||||
image: kenchan0130/simplesamlphp:1.19.8
|
||||
container_name: idp
|
||||
ports:
|
||||
- "8090:8080"
|
||||
environment:
|
||||
@ -99,8 +99,7 @@ services:
|
||||
- idp
|
||||
|
||||
rabbitmq:
|
||||
image: rabbitmq:4.1.0-management
|
||||
container_name: rabbitmq
|
||||
image: rabbitmq:4.1.3-management
|
||||
ports:
|
||||
- "5672:5672"
|
||||
- "15672:15672"
|
||||
@ -114,7 +113,6 @@ services:
|
||||
|
||||
reverse-proxy:
|
||||
image: nginx:alpine
|
||||
container_name: reverse-proxy
|
||||
volumes:
|
||||
- "./reverse-proxy.conf:/etc/nginx/conf.d/default.conf"
|
||||
ports:
|
||||
@ -124,7 +122,6 @@ services:
|
||||
- proxy
|
||||
|
||||
service-bus:
|
||||
container_name: service-bus
|
||||
image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest
|
||||
pull_policy: always
|
||||
volumes:
|
||||
@ -140,7 +137,6 @@ services:
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
container_name: bw-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
@ -153,5 +149,6 @@ volumes:
|
||||
mssql_dev_data:
|
||||
postgres_dev_data:
|
||||
mysql_dev_data:
|
||||
mariadb_dev_data:
|
||||
rabbitmq_data:
|
||||
redis_data:
|
||||
|
||||
28
dev/generate_openapi_files.ps1
Normal file
28
dev/generate_openapi_files.ps1
Normal file
@ -0,0 +1,28 @@
|
||||
Set-Location "$PSScriptRoot/.."
|
||||
|
||||
$env:ASPNETCORE_ENVIRONMENT = "Development"
|
||||
$env:swaggerGen = "True"
|
||||
$env:DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX = "2"
|
||||
$env:GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING = "placeholder"
|
||||
|
||||
dotnet tool restore
|
||||
|
||||
# Identity
|
||||
Set-Location "./src/Identity"
|
||||
dotnet build
|
||||
dotnet swagger tofile --output "../../identity.json" --host "https://identity.bitwarden.com" "./bin/Debug/net8.0/Identity.dll" "v1"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
# Api internal & public
|
||||
Set-Location "../../src/Api"
|
||||
dotnet build
|
||||
dotnet swagger tofile --output "../../api.json" "./bin/Debug/net8.0/Api.dll" "internal"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
dotnet swagger tofile --output "../../api.public.json" "./bin/Debug/net8.0/Api.dll" "public"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
@ -70,7 +70,7 @@ Foreach ($item in @(
|
||||
@($mysql, "MySQL", "MySqlMigrations", "mySql", 2),
|
||||
# MariaDB shares the MySQL connection string in the server config so they are mutually exclusive in that context.
|
||||
# However they can still be run independently for integration tests.
|
||||
@($mariadb, "MariaDB", "MySqlMigrations", "mySql", 3)
|
||||
@($mariadb, "MariaDB", "MySqlMigrations", "mySql", 4)
|
||||
)) {
|
||||
if (!$item[0] -and !$all) {
|
||||
continue
|
||||
|
||||
@ -33,6 +33,8 @@
|
||||
"id": "<your Installation Id>",
|
||||
"key": "<your Installation Key>"
|
||||
},
|
||||
"licenseDirectory": "<full path to license directory>"
|
||||
"licenseDirectory": "<full path to license directory>",
|
||||
"enableNewDeviceVerification": true,
|
||||
"enableEmailVerification": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,22 +3,6 @@
|
||||
"Namespaces": [
|
||||
{
|
||||
"Name": "sbemulatorns",
|
||||
"Queues": [
|
||||
{
|
||||
"Name": "queue.1",
|
||||
"Properties": {
|
||||
"DeadLetteringOnMessageExpiration": false,
|
||||
"DefaultMessageTimeToLive": "PT1H",
|
||||
"DuplicateDetectionHistoryTimeWindow": "PT20S",
|
||||
"ForwardDeadLetteredMessagesTo": "",
|
||||
"ForwardTo": "",
|
||||
"LockDuration": "PT1M",
|
||||
"MaxDeliveryCount": 3,
|
||||
"RequiresDuplicateDetection": false,
|
||||
"RequiresSession": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"Topics": [
|
||||
{
|
||||
"Name": "event-logging",
|
||||
@ -34,6 +18,12 @@
|
||||
},
|
||||
{
|
||||
"Name": "events-hec-subscription"
|
||||
},
|
||||
{
|
||||
"Name": "events-datadog-subscription"
|
||||
},
|
||||
{
|
||||
"Name": "events-teams-subscription"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -81,6 +71,34 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "integration-datadog-subscription",
|
||||
"Rules": [
|
||||
{
|
||||
"Name": "datadog-integration-filter",
|
||||
"Properties": {
|
||||
"FilterType": "Correlation",
|
||||
"CorrelationFilter": {
|
||||
"Label": "datadog"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "integration-teams-subscription",
|
||||
"Rules": [
|
||||
{
|
||||
"Name": "teams-integration-filter",
|
||||
"Properties": {
|
||||
"FilterType": "Correlation",
|
||||
"CorrelationFilter": {
|
||||
"Label": "teams"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
132
dev/verify_migrations.ps1
Normal file
132
dev/verify_migrations.ps1
Normal file
@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Validates that new database migration files follow naming conventions and chronological order.
|
||||
|
||||
.DESCRIPTION
|
||||
This script validates migration files in util/Migrator/DbScripts/ to ensure:
|
||||
1. New migrations follow the naming format: YYYY-MM-DD_NN_Description.sql
|
||||
2. New migrations are chronologically ordered (filename sorts after existing migrations)
|
||||
3. Dates use leading zeros (e.g., 2025-01-05, not 2025-1-5)
|
||||
4. A 2-digit sequence number is included (e.g., _00, _01)
|
||||
|
||||
.PARAMETER BaseRef
|
||||
The base git reference to compare against (e.g., 'main', 'HEAD~1')
|
||||
|
||||
.PARAMETER CurrentRef
|
||||
The current git reference (defaults to 'HEAD')
|
||||
|
||||
.EXAMPLE
|
||||
# For pull requests - compare against main branch
|
||||
.\verify_migrations.ps1 -BaseRef main
|
||||
|
||||
.EXAMPLE
|
||||
# For pushes - compare against previous commit
|
||||
.\verify_migrations.ps1 -BaseRef HEAD~1
|
||||
#>
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$BaseRef,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$CurrentRef = "HEAD"
|
||||
)
|
||||
|
||||
# Use invariant culture for consistent string comparison
|
||||
[System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::InvariantCulture
|
||||
|
||||
$migrationPath = "util/Migrator/DbScripts"
|
||||
|
||||
# Get list of migrations from base reference
|
||||
try {
|
||||
$baseMigrations = git ls-tree -r --name-only $BaseRef -- "$migrationPath/*.sql" 2>$null | Sort-Object
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Warning: Could not retrieve migrations from base reference '$BaseRef'"
|
||||
$baseMigrations = @()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host "Warning: Could not retrieve migrations from base reference '$BaseRef'"
|
||||
$baseMigrations = @()
|
||||
}
|
||||
|
||||
# Get list of migrations from current reference
|
||||
$currentMigrations = git ls-tree -r --name-only $CurrentRef -- "$migrationPath/*.sql" | Sort-Object
|
||||
|
||||
# Find added migrations
|
||||
$addedMigrations = $currentMigrations | Where-Object { $_ -notin $baseMigrations }
|
||||
|
||||
if ($addedMigrations.Count -eq 0) {
|
||||
Write-Host "No new migration files added."
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "New migration files detected:"
|
||||
$addedMigrations | ForEach-Object { Write-Host " $_" }
|
||||
Write-Host ""
|
||||
|
||||
# Get the last migration from base reference
|
||||
if ($baseMigrations.Count -eq 0) {
|
||||
Write-Host "No previous migrations found (initial commit?). Skipping validation."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$lastBaseMigration = Split-Path -Leaf ($baseMigrations | Select-Object -Last 1)
|
||||
Write-Host "Last migration in base reference: $lastBaseMigration"
|
||||
Write-Host ""
|
||||
|
||||
# Required format regex: YYYY-MM-DD_NN_Description.sql
|
||||
$formatRegex = '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}_.+\.sql$'
|
||||
|
||||
$validationFailed = $false
|
||||
|
||||
foreach ($migration in $addedMigrations) {
|
||||
$migrationName = Split-Path -Leaf $migration
|
||||
|
||||
# Validate NEW migration filename format
|
||||
if ($migrationName -notmatch $formatRegex) {
|
||||
Write-Host "ERROR: Migration '$migrationName' does not match required format"
|
||||
Write-Host "Required format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " - YYYY: 4-digit year"
|
||||
Write-Host " - MM: 2-digit month with leading zero (01-12)"
|
||||
Write-Host " - DD: 2-digit day with leading zero (01-31)"
|
||||
Write-Host " - NN: 2-digit sequence number (00, 01, 02, etc.)"
|
||||
Write-Host "Example: 2025-01-15_00_MyMigration.sql"
|
||||
$validationFailed = $true
|
||||
continue
|
||||
}
|
||||
|
||||
# Compare migration name with last base migration (using ordinal string comparison)
|
||||
if ([string]::CompareOrdinal($migrationName, $lastBaseMigration) -lt 0) {
|
||||
Write-Host "ERROR: New migration '$migrationName' is not chronologically after '$lastBaseMigration'"
|
||||
$validationFailed = $true
|
||||
}
|
||||
else {
|
||||
Write-Host "OK: '$migrationName' is chronologically after '$lastBaseMigration'"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
if ($validationFailed) {
|
||||
Write-Host "FAILED: One or more migrations are incorrectly named or not in chronological order"
|
||||
Write-Host ""
|
||||
Write-Host "All new migration files must:"
|
||||
Write-Host " 1. Follow the naming format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " 2. Use leading zeros in dates (e.g., 2025-01-05, not 2025-1-5)"
|
||||
Write-Host " 3. Include a 2-digit sequence number (e.g., _00, _01)"
|
||||
Write-Host " 4. Have a filename that sorts after the last migration in base"
|
||||
Write-Host ""
|
||||
Write-Host "To fix this issue:"
|
||||
Write-Host " 1. Locate your migration file(s) in util/Migrator/DbScripts/"
|
||||
Write-Host " 2. Rename to follow format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " 3. Ensure the date is after $lastBaseMigration"
|
||||
Write-Host ""
|
||||
Write-Host "Example: 2025-01-15_00_AddNewFeature.sql"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "SUCCESS: All new migrations are correctly named and in chronological order"
|
||||
exit 0
|
||||
@ -20,7 +20,7 @@ public class StaticClientStoreTests
|
||||
[Benchmark]
|
||||
public Client? TryGetValue()
|
||||
{
|
||||
return _store.ApiClients.TryGetValue(ClientId, out var client)
|
||||
return _store.Clients.TryGetValue(ClientId, out var client)
|
||||
? client
|
||||
: null;
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.15.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -9,12 +9,6 @@ const AUTH_USERNAME = __ENV.AUTH_USER_EMAIL;
|
||||
const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;
|
||||
|
||||
export const options = {
|
||||
ext: {
|
||||
loadimpact: {
|
||||
projectID: 3639465,
|
||||
name: "Config",
|
||||
},
|
||||
},
|
||||
scenarios: {
|
||||
constant_load: {
|
||||
executor: "constant-arrival-rate",
|
||||
|
||||
@ -10,12 +10,6 @@ const AUTH_CLIENT_ID = __ENV.AUTH_CLIENT_ID;
|
||||
const AUTH_CLIENT_SECRET = __ENV.AUTH_CLIENT_SECRET;
|
||||
|
||||
export const options = {
|
||||
ext: {
|
||||
loadimpact: {
|
||||
projectID: 3639465,
|
||||
name: "Groups",
|
||||
},
|
||||
},
|
||||
scenarios: {
|
||||
constant_load: {
|
||||
executor: "constant-arrival-rate",
|
||||
|
||||
@ -6,12 +6,6 @@ const AUTH_USERNAME = __ENV.AUTH_USER_EMAIL;
|
||||
const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;
|
||||
|
||||
export const options = {
|
||||
ext: {
|
||||
loadimpact: {
|
||||
projectID: 3639465,
|
||||
name: "Login",
|
||||
},
|
||||
},
|
||||
scenarios: {
|
||||
constant_load: {
|
||||
executor: "constant-arrival-rate",
|
||||
|
||||
@ -9,12 +9,6 @@ const AUTH_USERNAME = __ENV.AUTH_USER_EMAIL;
|
||||
const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;
|
||||
|
||||
export const options = {
|
||||
ext: {
|
||||
loadimpact: {
|
||||
projectID: 3639465,
|
||||
name: "Sync",
|
||||
},
|
||||
},
|
||||
scenarios: {
|
||||
constant_load: {
|
||||
executor: "constant-arrival-rate",
|
||||
|
||||
@ -9,6 +9,7 @@ using Bit.Admin.Utilities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
@ -32,7 +33,6 @@ namespace Bit.Admin.AdminConsole.Controllers;
|
||||
[Authorize]
|
||||
public class OrganizationsController : Controller
|
||||
{
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
|
||||
@ -55,9 +55,9 @@ public class OrganizationsController : Controller
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationService organizationService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationConnectionRepository organizationConnectionRepository,
|
||||
@ -79,9 +79,9 @@ public class OrganizationsController : Controller
|
||||
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
||||
IProviderBillingService providerBillingService,
|
||||
IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand,
|
||||
IPricingClient pricingClient)
|
||||
IPricingClient pricingClient,
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationConnectionRepository = organizationConnectionRepository;
|
||||
@ -104,6 +104,7 @@ public class OrganizationsController : Controller
|
||||
_providerBillingService = providerBillingService;
|
||||
_organizationInitiateDeleteCommand = organizationInitiateDeleteCommand;
|
||||
_pricingClient = pricingClient;
|
||||
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Org_List_View)]
|
||||
@ -395,7 +396,7 @@ public class OrganizationsController : Controller
|
||||
var organizationUsers = await _organizationUserRepository.GetManyByOrganizationAsync(id, OrganizationUserType.Owner);
|
||||
foreach (var organizationUser in organizationUsers)
|
||||
{
|
||||
await _organizationService.ResendInviteAsync(id, null, organizationUser.Id, true);
|
||||
await _resendOrganizationInviteCommand.ResendInviteAsync(id, null, organizationUser.Id, true);
|
||||
}
|
||||
|
||||
return Json(null);
|
||||
@ -471,6 +472,8 @@ public class OrganizationsController : Controller
|
||||
organization.UseRiskInsights = model.UseRiskInsights;
|
||||
organization.UseOrganizationDomains = model.UseOrganizationDomains;
|
||||
organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;
|
||||
organization.UseAutomaticUserConfirmation = model.UseAutomaticUserConfirmation;
|
||||
organization.UsePhishingBlocker = model.UsePhishingBlocker;
|
||||
|
||||
//secrets
|
||||
organization.SmSeats = model.SmSeats;
|
||||
|
||||
@ -7,7 +7,6 @@ using Bit.Admin.AdminConsole.Models;
|
||||
using Bit.Admin.Enums;
|
||||
using Bit.Admin.Services;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
@ -22,6 +21,7 @@ using Bit.Core.Billing.Providers.Entities;
|
||||
using Bit.Core.Billing.Providers.Models;
|
||||
using Bit.Core.Billing.Providers.Repositories;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
@ -38,27 +38,26 @@ namespace Bit.Admin.AdminConsole.Controllers;
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public class ProvidersController : Controller
|
||||
{
|
||||
private readonly string _stripeUrl;
|
||||
private readonly string _braintreeMerchantUrl;
|
||||
private readonly string _braintreeMerchantId;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IResellerClientOrganizationSignUpCommand _resellerClientOrganizationSignUpCommand;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly ICreateProviderCommand _createProviderCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IStripeAdapter _stripeAdapter;
|
||||
private readonly IAccessControlService _accessControlService;
|
||||
private readonly string _stripeUrl;
|
||||
private readonly string _braintreeMerchantUrl;
|
||||
private readonly string _braintreeMerchantId;
|
||||
private readonly ISubscriberService _subscriberService;
|
||||
|
||||
public ProvidersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
public ProvidersController(IOrganizationRepository organizationRepository,
|
||||
IResellerClientOrganizationSignUpCommand resellerClientOrganizationSignUpCommand,
|
||||
IProviderRepository providerRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
@ -67,13 +66,13 @@ public class ProvidersController : Controller
|
||||
GlobalSettings globalSettings,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
ICreateProviderCommand createProviderCommand,
|
||||
IFeatureService featureService,
|
||||
IProviderPlanRepository providerPlanRepository,
|
||||
IProviderBillingService providerBillingService,
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
IPricingClient pricingClient,
|
||||
IStripeAdapter stripeAdapter,
|
||||
IAccessControlService accessControlService)
|
||||
IAccessControlService accessControlService,
|
||||
ISubscriberService subscriberService)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_resellerClientOrganizationSignUpCommand = resellerClientOrganizationSignUpCommand;
|
||||
@ -84,15 +83,15 @@ public class ProvidersController : Controller
|
||||
_globalSettings = globalSettings;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_createProviderCommand = createProviderCommand;
|
||||
_featureService = featureService;
|
||||
_providerPlanRepository = providerPlanRepository;
|
||||
_providerBillingService = providerBillingService;
|
||||
_pricingClient = pricingClient;
|
||||
_stripeAdapter = stripeAdapter;
|
||||
_accessControlService = accessControlService;
|
||||
_stripeUrl = webHostEnvironment.GetStripeUrl();
|
||||
_braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();
|
||||
_braintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
_accessControlService = accessControlService;
|
||||
_subscriberService = subscriberService;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Provider_List_View)]
|
||||
@ -299,6 +298,23 @@ public class ProvidersController : Controller
|
||||
|
||||
model.ToProvider(provider);
|
||||
|
||||
// validate the stripe ids to prevent saving a bad one
|
||||
if (provider.IsBillable())
|
||||
{
|
||||
if (!await _subscriberService.IsValidGatewayCustomerIdAsync(provider))
|
||||
{
|
||||
var oldModel = await GetEditModel(id);
|
||||
ModelState.AddModelError(nameof(model.GatewayCustomerId), $"Invalid Gateway Customer Id: {model.GatewayCustomerId}");
|
||||
return View(oldModel);
|
||||
}
|
||||
if (!await _subscriberService.IsValidGatewaySubscriptionIdAsync(provider))
|
||||
{
|
||||
var oldModel = await GetEditModel(id);
|
||||
ModelState.AddModelError(nameof(model.GatewaySubscriptionId), $"Invalid Gateway Subscription Id: {model.GatewaySubscriptionId}");
|
||||
return View(oldModel);
|
||||
}
|
||||
}
|
||||
|
||||
provider.Enabled = _accessControlService.UserHasPermission(Permission.Provider_CheckEnabledBox)
|
||||
? model.Enabled : originalProviderStatus;
|
||||
|
||||
@ -323,21 +339,17 @@ public class ProvidersController : Controller
|
||||
]);
|
||||
await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand);
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically))
|
||||
var customer = await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId);
|
||||
if (model.PayByInvoice != customer.ApprovedToPayByInvoice())
|
||||
{
|
||||
var customer = await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId);
|
||||
|
||||
if (model.PayByInvoice != customer.ApprovedToPayByInvoice())
|
||||
var approvedToPayByInvoice = model.PayByInvoice ? "1" : "0";
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
{
|
||||
var approvedToPayByInvoice = model.PayByInvoice ? "1" : "0";
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[StripeConstants.MetadataKeys.InvoiceApproved] = approvedToPayByInvoice
|
||||
}
|
||||
});
|
||||
}
|
||||
[StripeConstants.MetadataKeys.InvoiceApproved] = approvedToPayByInvoice
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ProviderType.BusinessUnit:
|
||||
@ -382,10 +394,7 @@ public class ProvidersController : Controller
|
||||
}
|
||||
|
||||
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||
|
||||
var payByInvoice =
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically) &&
|
||||
(await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId)).ApprovedToPayByInvoice();
|
||||
var payByInvoice = ((await _subscriberService.GetCustomer(provider))?.ApprovedToPayByInvoice() ?? false);
|
||||
|
||||
return new ProviderEditModel(
|
||||
provider, users, providerOrganizations,
|
||||
|
||||
@ -106,6 +106,9 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
SmServiceAccounts = org.SmServiceAccounts;
|
||||
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
|
||||
UseOrganizationDomains = org.UseOrganizationDomains;
|
||||
UseAutomaticUserConfirmation = org.UseAutomaticUserConfirmation;
|
||||
UsePhishingBlocker = org.UsePhishingBlocker;
|
||||
|
||||
_plans = plans;
|
||||
}
|
||||
|
||||
@ -158,6 +161,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
public new bool UseSecretsManager { get; set; }
|
||||
[Display(Name = "Risk Insights")]
|
||||
public new bool UseRiskInsights { get; set; }
|
||||
[Display(Name = "Phishing Blocker")]
|
||||
public new bool UsePhishingBlocker { get; set; }
|
||||
[Display(Name = "Admin Sponsored Families")]
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
[Display(Name = "Self Host")]
|
||||
@ -192,6 +197,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
[Display(Name = "Use Organization Domains")]
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
|
||||
[Display(Name = "Automatic User Confirmation")]
|
||||
public bool UseAutomaticUserConfirmation { get; set; }
|
||||
/**
|
||||
* Creates a Plan[] object for use in Javascript
|
||||
* This is mapped manually below to provide some type safety in case the plan objects change
|
||||
@ -231,6 +238,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
LegacyYear = p.LegacyYear,
|
||||
Disabled = p.Disabled,
|
||||
SupportsSecretsManager = p.SupportsSecretsManager,
|
||||
AutomaticUserConfirmation = p.AutomaticUserConfirmation,
|
||||
PasswordManager =
|
||||
new
|
||||
{
|
||||
@ -322,6 +330,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
existingOrganization.SmServiceAccounts = SmServiceAccounts;
|
||||
existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts;
|
||||
existingOrganization.UseOrganizationDomains = UseOrganizationDomains;
|
||||
existingOrganization.UsePhishingBlocker = UsePhishingBlocker;
|
||||
return existingOrganization;
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,6 +75,7 @@ public class OrganizationViewModel
|
||||
public int OccupiedSmSeatsCount { get; set; }
|
||||
public bool UseSecretsManager => Organization.UseSecretsManager;
|
||||
public bool UseRiskInsights => Organization.UseRiskInsights;
|
||||
public bool UsePhishingBlocker => Organization.UsePhishingBlocker;
|
||||
public IEnumerable<OrganizationUserUserDetails> OwnersDetails { get; set; }
|
||||
public IEnumerable<OrganizationUserUserDetails> AdminsDetails { get; set; }
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Providers.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
@ -87,14 +88,13 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
||||
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim();
|
||||
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim();
|
||||
existingProvider.Enabled = Enabled;
|
||||
switch (Type)
|
||||
if (Type.IsStripeSupported())
|
||||
{
|
||||
case ProviderType.Msp:
|
||||
existingProvider.Gateway = Gateway;
|
||||
existingProvider.GatewayCustomerId = GatewayCustomerId;
|
||||
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
|
||||
break;
|
||||
existingProvider.Gateway = Gateway;
|
||||
existingProvider.GatewayCustomerId = GatewayCustomerId;
|
||||
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
|
||||
}
|
||||
|
||||
return existingProvider;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user