1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-24 12:35:25 +01:00

Feature/self hosted families for enterprise (#1991)

* Families for enterprise/split up organization sponsorship service (#1829)

* Split OrganizationSponsorshipService into commands

* Use tokenable for token validation

* Use interfaces to set up for DI

* Use commands over services

* Move service tests to command tests

* Value types can't be null

* Run dotnet format

* Update src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommand.cs

Co-authored-by: Justin Baur <admin@justinbaur.com>

* Fix controller tests

Co-authored-by: Justin Baur <admin@justinbaur.com>

* Families for enterprise/split up organization sponsorship service (#1875)

* Split OrganizationSponsorshipService into commands

* Use tokenable for token validation

* Use interfaces to set up for DI

* Use commands over services

* Move service tests to command tests

* Value types can't be null

* Run dotnet format

* Update src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommand.cs

Co-authored-by: Justin Baur <admin@justinbaur.com>

* Fix controller tests

* Split create and send sponsorships

* Split up create sponsorship

* Add self hosted commands to dependency injection

* Add field to store cloud billing sync key on self host instances

* Fix typo

* Fix data protector purpose of sponsorship offers

* Split cloud and selfhosted sponsorship offer tokenable

* Generate offer from self hosted with all necessary auth data

* Add Required properties to constructor

* Split up cancel sponsorship command

* Split revoke sponsorship command between cloud and self hosted

* Fix/f4e multiple sponsorships (#1838)

* Use sponosorship from validate to redeem

* Update tests

* Format

* Remove sponsorship service

* Run dotnet format

* Fix self hosted only controller attribute

* Clean up file structure and fixes

* Remove unneeded tokenables

* Remove obsolete commands

* Do not require file/class prefix if unnecessary

* Update Organizaiton sprocs

* Remove unnecessary models

* Fix tests

* Generalize LicenseService path calculation

Use async file read and deserialization

* Use interfaces for testability

* Remove unused usings

* Correct test direction

* Test license reading

* remove unused usings

* Format

Co-authored-by: Justin Baur <admin@justinbaur.com>

* Improve DataProtectorTokenFactory test coverage (#1884)

* Add encstring to server

* Test factory

Co-authored-by: Carlos Muentes <cmuentes@bitwarden.com>

* Format

* Remove SymmetricKeyProtectedString

Not needed

* Set ForcInvalid

Co-authored-by: Carlos Muentes <cmuentes@bitwarden.com>

* Feature/self f4e/api keys (#1896)

* Add in ApiKey

* Work on API Key table

* Work on apikey table

* Fix response model

* Work on information for UI

* Work on last sync date

* Work on sync status

* Work on auth

* Work on tokenable

* Work on merge

* Add custom requirement

* Add policy

* Run formatting

* Work on EF Migrations

* Work on OrganizationConnection

* Work on database

* Work on additional database table

* Run formatting

* Small fixes

* More cleanup

* Cleanup

* Add RevisionDate

* Add GO

* Finish Sql project

* Add newlines

* Fix stored proc file

* Fix sqlproj

* Add newlines

* Fix table

* Add navigation property

* Delete Connections when organization is deleted

* Add connection validation

* Start adding ID column

* Work on ID column

* Work on SQL migration

* Work on migrations

* Run formatting

* Fix test build

* Fix sprocs

* Work on migrations

* Fix Create table

* Fix sproc

* Add prints to migration

* Add default value

* Update EF migrations

* Formatting

* Add to integration tests

* Minor fixes

* Formatting

* Cleanup

* Address PR feedback

* Address more PR feedback

* Fix formatting

* Fix formatting

* Fix

* Address PR feedback

* Remove accidential change

* Fix SQL build

* Run formatting

* Address PR feedback

* Add sync data to OrganizationUserOrgDetails

* Add comments

* Remove OrganizationConnectionService interface

* Remove unused using

* Address PR feedback

* Formatting

* Minor fix

* Feature/self f4e/update db (#1930)

* Fix migration

* Fix TimesRenewed

* Add comments

* Make two properties non-nullable

* Remove need for SponsoredOrg on SH (#1934)

* Remove need for SponsoredOrg on SH

* Add Family prefix

* Add check for enterprise org on BillingSync key (#1936)

* [PS-10] Feature/sponsorships removed at end of term (#1938)

* Rename commands to min unique names

* Inject revoke command based on self hosting

* WIP: Remove/Revoke marks to delete

* Complete WIP

* Improve remove/revoke tests

* PR review

* Fail validation if sponsorship has failed to sync for 6 months

* Feature/do not accept old self host sponsorships (#1939)

* Do not accept >6mo old self-hosted sponsorships

* Give disabled grace period of 3 months

* Fix issues of Sql.proj differing from migration outcome (#1942)

* Fix issues of Sql.proj differing from migration outcome

* Yoink int tests

* Add missing assert helpers

* Feature/org sponsorship sync (#1922)

* Self-hosted side sync first pass

TODO:
* flush out org sponsorship model
* implement cloud side
* process cloud-side response and update self-hosted records

* sync scaffolding second pass

* remove list of Org User ids from sync and begin work on SelfHostedRevokeSponsorship

* allow authenticated http calls from server to return a result

* update models

* add logic for sync and change offer email template

* add billing sync key and hide CreateSponsorship without user

* fix tests

* add job scheduling

* add authorize attributes to endpoints

* separate models into data/model and request/response

* batch sync more, add EnableCloudCommunication for testing

* send emails in bulk

* make userId and sponsorshipType non nullable

* batch more on self hosted side of sync

* remove TODOs and formatting

* changed logic of cloud sync

* let BaseIdentityClientService handle all logging

* call sync from scheduled job on self host

* create bulk db operations for OrganizationSponsorships

* remove SponsoredOrgId from sync, return default from server http call

* validate BillingSyncKey during sync

revert changes to CreateSponsorshipCommand

* revert changes to ICreateSponsorshipCommand

* add some tests

* add DeleteExpiredSponsorshipsJob

* add cloud sync test

* remove extra method

* formatting

* prevent new sponsorships from disabled orgs

* update packages

* - pulled out send sponsorship command dependency from sync on cloud
- don't throw error when sponsorships are empty
- formatting

* formatting models

* more formatting

* remove licensingService dependency from selfhosted sync

* use installation urls and formatting

* create constructor for RequestModel and formatting

* add date parameter to OrganizationSponsorship_DeleteExpired

* add new migration

* formatting

* rename OrganizationCreateSponsorshipRequestModel to OrganizationSponsorshipCreateRequestModel

* prevent whole sync from failing if one sponsorship type is unsupported

* deserialize config and billingsynckey from org connection

* alter log message when sync disabled

* Add grace period to disabled orgs

* return early on self hosted if there are no sponsorships in database

* rename BillingSyncConfig

* send sponsorship offers from controller

* allow config to be a null object

* better exception handling in sync scheduler

* add ef migrations

* formatting

* fix tests

* fix validate test

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* Fix OrganizationApiKey issues (#1941)

Co-authored-by: Justin Baur <admin@justinbaur.com>

* Feature/org sponsorship self hosted tests (#1947)

* Self-hosted side sync first pass

TODO:
* flush out org sponsorship model
* implement cloud side
* process cloud-side response and update self-hosted records

* sync scaffolding second pass

* remove list of Org User ids from sync and begin work on SelfHostedRevokeSponsorship

* allow authenticated http calls from server to return a result

* update models

* add logic for sync and change offer email template

* add billing sync key and hide CreateSponsorship without user

* fix tests

* add job scheduling

* add authorize attributes to endpoints

* separate models into data/model and request/response

* batch sync more, add EnableCloudCommunication for testing

* send emails in bulk

* make userId and sponsorshipType non nullable

* batch more on self hosted side of sync

* remove TODOs and formatting

* changed logic of cloud sync

* let BaseIdentityClientService handle all logging

* call sync from scheduled job on self host

* create bulk db operations for OrganizationSponsorships

* remove SponsoredOrgId from sync, return default from server http call

* validate BillingSyncKey during sync

revert changes to CreateSponsorshipCommand

* revert changes to ICreateSponsorshipCommand

* add some tests

* add DeleteExpiredSponsorshipsJob

* add cloud sync test

* remove extra method

* formatting

* prevent new sponsorships from disabled orgs

* update packages

* - pulled out send sponsorship command dependency from sync on cloud
- don't throw error when sponsorships are empty
- formatting

* formatting models

* more formatting

* remove licensingService dependency from selfhosted sync

* use installation urls and formatting

* create constructor for RequestModel and formatting

* add date parameter to OrganizationSponsorship_DeleteExpired

* add new migration

* formatting

* rename OrganizationCreateSponsorshipRequestModel to OrganizationSponsorshipCreateRequestModel

* prevent whole sync from failing if one sponsorship type is unsupported

* deserialize config and billingsynckey from org connection

* add mockHttp nuget package and use httpclientfactory

* fix current tests

* WIP of creating tests

* WIP of new self hosted tests

* WIP self hosted tests

* finish self hosted tests

* formatting

* format of interface

* remove extra config file

* added newlines

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* Fix Organization_DeleteById (#1950)

* Fix Organization_Delete

* Fix L

* [PS-4] block enterprise user from sponsoring itself (#1943)

* [PS-248] Feature/add connections enabled endpoint (#1953)

* Move Organization models to sub namespaces

* Add Organization Connection api endpoints

* Get all connections rather than just enabled ones

* Add missing services to DI

* pluralize private api endpoints

* Add type protection to org connection request/response

* Fix route

* Use nullable Id to signify no connection

* Test Get Connections enabled

* Fix data discoverer

* Also drop this sproc for rerunning

* Id is the OUTPUT of create sprocs

* Fix connection config parsing

* Linter fixes

* update sqlproj file name

* Use param xdocs on methods

* Simplify controller path attribute

* Use JsonDocument to avoid escaped json in our response/request strings

* Fix JsonDoc tests

* Linter fixes

* Fix ApiKey Command and add tests (#1949)

* Fix ApiKey command

* Formatting

* Fix test failures introduced in #1943 (#1957)

* Remove "Did you know?" copy from emails. (#1962)

* Remove "Did you know"

* Remove jsonIf helper

* Feature/fix send single sponsorship offer email (#1956)

* Fix sponsorship offer email

* Do not sanitize org name

* PR feedback

* Feature/f4e sync event [PS-75] (#1963)

* Create sponsorship sync event type

* Add InstallationId to Event model

* Add combinatorics-based test case generators

* Log sponsorships sync event on sync

* Linter and test fixes

* Fix failing test

* Migrate sprocs and view

* Remove unused `using`s

* [PS-190] Add manual sync trigger in self hosted (#1955)

* WIP add button to admin project for billing sync

* add connection table to view page

* minor fixes for self hosted side of sync

* fixes number of bugs for cloud side of sync

* deserialize before returning for some reason

* add json attributes to return models

* list of sponsorships parameter is immutable, add secondary list

* change sproc name

* add error handling

* Fix tests

* modify call to connection

* Update src/Admin/Controllers/OrganizationsController.cs

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* undo change to sproc name

* simplify logic

* Update src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* register services despite if self hosted or cloud

* remove json properties

* revert merge conflict

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* Update OrganizationSponsorship valid until when updating org expirati… (#1966)

* Update OrganizationSponsorship valid until when updating org expiration date

* Linter fixes

* [PS-7] change revert email copy and add ValidUntil to sponsorship (#1965)

* change revert email copy and add ValidUntil to sponsorship

* add 15 days if no ValidUntil

* Chore/merge/self hosted families for enterprise (#1972)

* Log swallowed HttpRequestExceptions (#1866)

Co-authored-by: Hinton <oscar@oscarhinton.com>

* Allow for utilization of  readonly db connection (#1937)

* Bump the pin of the download-artifacts action to bypass the broken GitHub api (#1952)

* Bumped version to 1.48.0 (#1958)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* [EC-160] Give Provider Users access to all org ciphers and collections (#1959)

* Bumped version to 1.48.1 (#1961)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Avoid sending "user need confirmation" emails when there are no org admins (#1960)

* Remove noncompliant users for new policies (#1951)

* [PS-284] Allow installation clients to not need a user. (#1968)

* Allow installation clients to not need a user.

* Run formatting

Co-authored-by: Andrei <30410186+Manolachi@users.noreply.github.com>
Co-authored-by: Hinton <oscar@oscarhinton.com>
Co-authored-by: sneakernuts <671942+sneakernuts@users.noreply.github.com>
Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Justin Baur <136baur@gmail.com>

* Fix/license file not found (#1974)

* Handle null license

* Throw hint message if license is not found by the admin project.

* Use CloudOrganizationId from Connection config

* Change test to support change

* Fix test

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* Feature/f4e selfhosted rename migration to .sql (#1971)

* rename migration to .sql

* format

* Add unit tests to self host F4E (#1975)

* Work on tests

* Added more tests

* Run linting

* Address PR feedback

* Fix AssertRecent

* Linting

* Fixed empty tests

* Fix/misc self hosted f4e (#1973)

* Allow setting of ApiUri

* Return updates sponsorshipsData objects

* Bind arguments by name

* Greedy load sponsorships to email.

When upsert was called, it creates Ids on _all_ records, which meant
that the lazy-evaluation from this call always returned an empty list.

* add scope for sync command DI in job. simplify error logic

* update the sync job to get CloudOrgId from the BillingSyncKey

Co-authored-by: Jacob Fink <jfink@bitwarden.com>

* Chore/merge/self hosted families for enterprise (#1987)

* Log swallowed HttpRequestExceptions (#1866)

Co-authored-by: Hinton <oscar@oscarhinton.com>

* Allow for utilization of  readonly db connection (#1937)

* Bump the pin of the download-artifacts action to bypass the broken GitHub api (#1952)

* Bumped version to 1.48.0 (#1958)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* [EC-160] Give Provider Users access to all org ciphers and collections (#1959)

* Bumped version to 1.48.1 (#1961)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Avoid sending "user need confirmation" emails when there are no org admins (#1960)

* Remove noncompliant users for new policies (#1951)

* [PS-284] Allow installation clients to not need a user. (#1968)

* Allow installation clients to not need a user.

* Run formatting

* Use accept flow for sponsorship offers (#1964)

* PS-82 check send 2FA email for new devices on TwoFactorController send-email-login (#1977)

* [Bug] Skip WebAuthn 2fa event logs during login flow (#1978)

* [Bug] Supress WebAuthn 2fa event logs during login process

* Formatting

* Simplified method call with new paramter input

* Update RealIps Description (#1980)

Describe the syntax of the real_ips configuration key with an example, to prevent type errors in the `setup` container when parsing `config.yml`

* add proper URI validation to duo host (#1984)

* captcha scores (#1967)

* captcha scores

* some api fixes

* check bot on captcha attribute

* Update src/Core/Services/Implementations/HCaptchaValidationService.cs

Co-authored-by: e271828- <e271828-@users.noreply.github.com>

Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
Co-authored-by: e271828- <e271828-@users.noreply.github.com>

* ensure no path specific in duo host (#1985)

Co-authored-by: Andrei <30410186+Manolachi@users.noreply.github.com>
Co-authored-by: Hinton <oscar@oscarhinton.com>
Co-authored-by: sneakernuts <671942+sneakernuts@users.noreply.github.com>
Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Justin Baur <136baur@gmail.com>
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Jordan Cooks <notnamed@users.noreply.github.com>
Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
Co-authored-by: e271828- <e271828-@users.noreply.github.com>

* Address feedback (#1990)

Co-authored-by: Justin Baur <admin@justinbaur.com>
Co-authored-by: Carlos Muentes <cmuentes@bitwarden.com>
Co-authored-by: Jake Fink <jfink@bitwarden.com>
Co-authored-by: Justin Baur <136baur@gmail.com>
Co-authored-by: Andrei <30410186+Manolachi@users.noreply.github.com>
Co-authored-by: Hinton <oscar@oscarhinton.com>
Co-authored-by: sneakernuts <671942+sneakernuts@users.noreply.github.com>
Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Jordan Cooks <notnamed@users.noreply.github.com>
Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
Co-authored-by: e271828- <e271828-@users.noreply.github.com>
This commit is contained in:
Matt Gibson 2022-05-10 17:12:09 -04:00 committed by GitHub
parent e5a9d3dec2
commit c54c39b28c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
304 changed files with 18514 additions and 1560 deletions

23
.vscode/launch.json vendored
View File

@ -40,6 +40,7 @@
"Identity", "Identity",
"Sso", "Sso",
"Icons", "Icons",
"Billing"
], ],
"presentation": { "presentation": {
"hidden": false, "hidden": false,
@ -125,6 +126,28 @@
"/Views": "${workspaceFolder}/Views" "/Views": "${workspaceFolder}/Views"
} }
}, },
{
"name": "Billing",
"presentation": {
"hidden": false,
"group": "cloud",
"order": 10
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildBilling",
"program": "${workspaceFolder}/src/Billing/bin/Debug/net5.0/Billing.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Billing",
"stopAtEntry": false,
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{ {
"name": "Admin", "name": "Admin",
"presentation": { "presentation": {

16
.vscode/tasks.json vendored
View File

@ -89,6 +89,22 @@
"isDefault": true "isDefault": true
} }
}, },
{
"label": "buildBilling",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/src/Billing/Billing.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile",
"group": {
"kind": "build",
"isDefault": true
}
},
{ {
"label": "clean", "label": "clean",
"type": "shell", "type": "shell",

View File

@ -78,7 +78,7 @@ namespace Bit.Sso
services.AddCustomIdentityServices(globalSettings); services.AddCustomIdentityServices(globalSettings);
// Services // Services
services.AddBaseServices(); services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings); services.AddDefaultServices(globalSettings);
services.AddCoreLocalizationServices(); services.AddCoreLocalizationServices();
} }

View File

@ -298,6 +298,15 @@
"IdentityModel": "4.3.0" "IdentityModel": "4.3.0"
} }
}, },
"Kralizek.AutoFixture.Extensions.MockHttp": {
"type": "Transitive",
"resolved": "1.2.0",
"contentHash": "6zmks7/5mVczazv910N7V2EdiU6B+rY61lwdgVO0o2iZuTI6KI3T+Hgkrjv0eGOKYucq2OMC+gnAc5Ej2ajoTQ==",
"dependencies": {
"AutoFixture": "4.11.0",
"RichardSzalay.MockHttp": "6.0.0"
}
},
"libsodium": { "libsodium": {
"type": "Transitive", "type": "Transitive",
"resolved": "1.0.18", "resolved": "1.0.18",
@ -1598,6 +1607,11 @@
"System.Diagnostics.DiagnosticSource": "4.7.1" "System.Diagnostics.DiagnosticSource": "4.7.1"
} }
}, },
"RichardSzalay.MockHttp": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "bStGNqIX/MGYtML7K3EzdsE/k5HGVAcg7XgN23TQXGXqxNC9fvYFR94fA0sGM5hAT36R+BBGet6ZDQxXL/IPxg=="
},
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
"type": "Transitive", "type": "Transitive",
"resolved": "4.3.2", "resolved": "4.3.2",
@ -3547,6 +3561,7 @@
"AutoFixture.AutoNSubstitute": "4.14.0", "AutoFixture.AutoNSubstitute": "4.14.0",
"AutoFixture.Xunit2": "4.14.0", "AutoFixture.Xunit2": "4.14.0",
"Core": "1.47.1", "Core": "1.47.1",
"Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0",
"Microsoft.NET.Test.Sdk": "16.6.1", "Microsoft.NET.Test.Sdk": "16.6.1",
"NSubstitute": "4.2.2", "NSubstitute": "4.2.2",
"xunit": "2.4.1" "xunit": "2.4.1"
@ -3599,6 +3614,7 @@
"AutoFixture.Xunit2": "4.14.0", "AutoFixture.Xunit2": "4.14.0",
"Common": "1.47.1", "Common": "1.47.1",
"Core": "1.47.1", "Core": "1.47.1",
"Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0",
"Microsoft.NET.Test.Sdk": "16.6.1", "Microsoft.NET.Test.Sdk": "16.6.1",
"Moq": "4.16.1", "Moq": "4.16.1",
"NSubstitute": "4.2.2", "NSubstitute": "4.2.2",

View File

@ -1,10 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Admin.Models; using Bit.Admin.Models;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
@ -19,11 +22,14 @@ namespace Bit.Admin.Controllers
{ {
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
private readonly ISelfHostedSyncSponsorshipsCommand _syncSponsorshipsCommand;
private readonly ICipherRepository _cipherRepository; private readonly ICipherRepository _cipherRepository;
private readonly ICollectionRepository _collectionRepository; private readonly ICollectionRepository _collectionRepository;
private readonly IGroupRepository _groupRepository; private readonly IGroupRepository _groupRepository;
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly IPaymentService _paymentService; private readonly IPaymentService _paymentService;
private readonly ILicensingService _licensingService;
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IReferenceEventService _referenceEventService; private readonly IReferenceEventService _referenceEventService;
@ -32,11 +38,14 @@ namespace Bit.Admin.Controllers
public OrganizationsController( public OrganizationsController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IOrganizationConnectionRepository organizationConnectionRepository,
ISelfHostedSyncSponsorshipsCommand syncSponsorshipsCommand,
ICipherRepository cipherRepository, ICipherRepository cipherRepository,
ICollectionRepository collectionRepository, ICollectionRepository collectionRepository,
IGroupRepository groupRepository, IGroupRepository groupRepository,
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
IPaymentService paymentService, IPaymentService paymentService,
ILicensingService licensingService,
IApplicationCacheService applicationCacheService, IApplicationCacheService applicationCacheService,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IReferenceEventService referenceEventService, IReferenceEventService referenceEventService,
@ -44,11 +53,14 @@ namespace Bit.Admin.Controllers
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_organizationConnectionRepository = organizationConnectionRepository;
_syncSponsorshipsCommand = syncSponsorshipsCommand;
_cipherRepository = cipherRepository; _cipherRepository = cipherRepository;
_collectionRepository = collectionRepository; _collectionRepository = collectionRepository;
_groupRepository = groupRepository; _groupRepository = groupRepository;
_policyRepository = policyRepository; _policyRepository = policyRepository;
_paymentService = paymentService; _paymentService = paymentService;
_licensingService = licensingService;
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_referenceEventService = referenceEventService; _referenceEventService = referenceEventService;
@ -104,7 +116,8 @@ namespace Bit.Admin.Controllers
policies = await _policyRepository.GetManyByOrganizationIdAsync(id); policies = await _policyRepository.GetManyByOrganizationIdAsync(id);
} }
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id); var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);
return View(new OrganizationViewModel(organization, users, ciphers, collections, groups, policies)); var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null;
return View(new OrganizationViewModel(organization, billingSyncConnection, users, ciphers, collections, groups, policies));
} }
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
@ -130,8 +143,9 @@ namespace Bit.Admin.Controllers
} }
var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id); var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);
var billingInfo = await _paymentService.GetBillingAsync(organization); var billingInfo = await _paymentService.GetBillingAsync(organization);
var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null;
return View(new OrganizationEditModel(organization, users, ciphers, collections, groups, policies, return View(new OrganizationEditModel(organization, users, ciphers, collections, groups, policies,
billingInfo, _globalSettings)); billingInfo, billingSyncConnection, _globalSettings));
} }
[HttpPost] [HttpPost]
@ -164,5 +178,40 @@ namespace Bit.Admin.Controllers
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
public async Task<IActionResult> TriggerBillingSync(Guid id)
{
var organization = await _organizationRepository.GetByIdAsync(id);
if (organization == null)
{
return RedirectToAction("Index");
}
var connection = (await _organizationConnectionRepository.GetEnabledByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync)).FirstOrDefault();
if (connection != null)
{
try
{
var config = connection.GetConfig<BillingSyncConfig>();
await _syncSponsorshipsCommand.SyncOrganization(id, config.CloudOrganizationId, connection);
TempData["ConnectionActivated"] = id;
TempData["ConnectionError"] = null;
}
catch (Exception ex)
{
TempData["ConnectionError"] = ex.Message;
}
if (_globalSettings.SelfHosted)
{
return RedirectToAction("View", new { id });
}
else
{
return RedirectToAction("Edit", new { id });
}
}
return RedirectToAction("Index");
}
} }
} }

View File

@ -0,0 +1,42 @@
using System;
using System.Threading.Tasks;
using Bit.Core;
using Bit.Core.Jobs;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
using Quartz;
namespace Bit.Admin.Jobs
{
public class DatabaseExpiredSponsorshipsJob : BaseJob
{
private GlobalSettings _globalSettings;
private readonly IMaintenanceRepository _maintenanceRepository;
public DatabaseExpiredSponsorshipsJob(
IMaintenanceRepository maintenanceRepository,
ILogger<DatabaseExpiredSponsorshipsJob> logger,
GlobalSettings globalSettings)
: base(logger)
{
_maintenanceRepository = maintenanceRepository;
_globalSettings = globalSettings;
}
protected override async Task ExecuteJobAsync(IJobExecutionContext context)
{
if (_globalSettings.SelfHosted && !_globalSettings.EnableCloudCommunication)
{
return;
}
_logger.LogInformation(Constants.BypassFiltersEventId, "Execute job task: DeleteExpiredSponsorshipsAsync");
// allow a 90 day grace period before deleting
var deleteDate = DateTime.UtcNow.AddDays(-90);
await _maintenanceRepository.DeleteExpiredSponsorshipsAsync(deleteDate);
_logger.LogInformation(Constants.BypassFiltersEventId, "Finished job task: DeleteExpiredSponsorshipsAsync");
}
}
}

View File

@ -55,6 +55,11 @@ namespace Bit.Admin.Jobs
.StartNow() .StartNow()
.WithCronSchedule("0 0 0 ? * SUN", x => x.InTimeZone(timeZone)) .WithCronSchedule("0 0 0 ? * SUN", x => x.InTimeZone(timeZone))
.Build(); .Build();
var everyMondayAtMidnightTrigger = TriggerBuilder.Create()
.WithIdentity("EveryMondayAtMidnightTrigger")
.StartNow()
.WithCronSchedule("0 0 0 ? * MON", x => x.InTimeZone(timeZone))
.Build();
var everyDayAtMidnightUtc = TriggerBuilder.Create() var everyDayAtMidnightUtc = TriggerBuilder.Create()
.WithIdentity("EveryDayAtMidnightUtc") .WithIdentity("EveryDayAtMidnightUtc")
.StartNow() .StartNow()
@ -67,7 +72,8 @@ namespace Bit.Admin.Jobs
new Tuple<Type, ITrigger>(typeof(DatabaseExpiredGrantsJob), everyFridayAt10pmTrigger), new Tuple<Type, ITrigger>(typeof(DatabaseExpiredGrantsJob), everyFridayAt10pmTrigger),
new Tuple<Type, ITrigger>(typeof(DatabaseUpdateStatisticsJob), everySaturdayAtMidnightTrigger), new Tuple<Type, ITrigger>(typeof(DatabaseUpdateStatisticsJob), everySaturdayAtMidnightTrigger),
new Tuple<Type, ITrigger>(typeof(DatabaseRebuildlIndexesJob), everySundayAtMidnightTrigger), new Tuple<Type, ITrigger>(typeof(DatabaseRebuildlIndexesJob), everySundayAtMidnightTrigger),
new Tuple<Type, ITrigger>(typeof(DeleteCiphersJob), everyDayAtMidnightUtc) new Tuple<Type, ITrigger>(typeof(DeleteCiphersJob), everyDayAtMidnightUtc),
new Tuple<Type, ITrigger>(typeof(DatabaseExpiredSponsorshipsJob), everyMondayAtMidnightTrigger)
}; };
if (!_globalSettings.SelfHosted) if (!_globalSettings.SelfHosted)
@ -88,6 +94,7 @@ namespace Bit.Admin.Jobs
services.AddTransient<DatabaseUpdateStatisticsJob>(); services.AddTransient<DatabaseUpdateStatisticsJob>();
services.AddTransient<DatabaseRebuildlIndexesJob>(); services.AddTransient<DatabaseRebuildlIndexesJob>();
services.AddTransient<DatabaseExpiredGrantsJob>(); services.AddTransient<DatabaseExpiredGrantsJob>();
services.AddTransient<DatabaseExpiredSponsorshipsJob>();
services.AddTransient<DeleteSendsJob>(); services.AddTransient<DeleteSendsJob>();
services.AddTransient<DeleteCiphersJob>(); services.AddTransient<DeleteCiphersJob>();
} }

View File

@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -16,8 +16,9 @@ namespace Bit.Admin.Models
public OrganizationEditModel(Organization org, IEnumerable<OrganizationUserUserDetails> orgUsers, public OrganizationEditModel(Organization org, IEnumerable<OrganizationUserUserDetails> orgUsers,
IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections, IEnumerable<Group> groups, IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections, IEnumerable<Group> groups,
IEnumerable<Policy> policies, BillingInfo billingInfo, GlobalSettings globalSettings) IEnumerable<Policy> policies, BillingInfo billingInfo, IEnumerable<OrganizationConnection> connections,
: base(org, orgUsers, ciphers, collections, groups, policies) GlobalSettings globalSettings)
: base(org, connections, orgUsers, ciphers, collections, groups, policies)
{ {
BillingInfo = billingInfo; BillingInfo = billingInfo;
BraintreeMerchantId = globalSettings.Braintree.MerchantId; BraintreeMerchantId = globalSettings.Braintree.MerchantId;

View File

@ -1,9 +1,8 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
namespace Bit.Admin.Models namespace Bit.Admin.Models
{ {
@ -11,11 +10,12 @@ namespace Bit.Admin.Models
{ {
public OrganizationViewModel() { } public OrganizationViewModel() { }
public OrganizationViewModel(Organization org, IEnumerable<OrganizationUserUserDetails> orgUsers, public OrganizationViewModel(Organization org, IEnumerable<OrganizationConnection> connections,
IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections, IEnumerable<Group> groups, IEnumerable<OrganizationUserUserDetails> orgUsers, IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
IEnumerable<Policy> policies) IEnumerable<Group> groups, IEnumerable<Policy> policies)
{ {
Organization = org; Organization = org;
Connections = connections ?? Enumerable.Empty<OrganizationConnection>();
HasPublicPrivateKeys = org.PublicKey != null && org.PrivateKey != null; HasPublicPrivateKeys = org.PublicKey != null && org.PrivateKey != null;
UserInvitedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Invited); UserInvitedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Invited);
UserAcceptedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Accepted); UserAcceptedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Accepted);
@ -36,6 +36,7 @@ namespace Bit.Admin.Models
} }
public Organization Organization { get; set; } public Organization Organization { get; set; }
public IEnumerable<OrganizationConnection> Connections { get; set; }
public string Owners { get; set; } public string Owners { get; set; }
public string Admins { get; set; } public string Admins { get; set; }
public int UserInvitedCount { get; set; } public int UserInvitedCount { get; set; }

View File

@ -69,7 +69,7 @@ namespace Bit.Admin
} }
// Services // Services
services.AddBaseServices(); services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings); services.AddDefaultServices(globalSettings);
#if OSS #if OSS

View File

@ -0,0 +1,81 @@
@model OrganizationViewModel
<h2>Connections</h2>
<div class="row">
<div class="col-8">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th style="width: 190px;">Type</th>
<th style="width: 40px;">Status</th>
<th style="width: 30px;"></th>
</tr>
</thead>
<tbody>
@if(!Model.Connections.Any())
{
<tr>
<td colspan="6">No results to list.</td>
</tr>
}
else
{
@foreach(var connection in Model.Connections)
{
<tr>
<td class="align-middle">
@if(connection.Type == OrganizationConnectionType.CloudBillingSync)
{
@:Billing Sync
}
</td>
<td class="align-middle">
@if(@TempData["ConnectionError"] != null)
{
<span class="text-danger">
@TempData["ConnectionError"]
</span>
}
else
{
@if(connection.Enabled)
{
@:Enabled
}
else
{
@:Disabled
}
}
</td>
<td>
@if(connection.Enabled)
{
@if(@TempData["ConnectionActivated"] != null && @TempData["ConnectionActivated"].ToString() == @Model.Organization.Id.ToString())
{
@if(connection.Type.Equals(OrganizationConnectionType.CloudBillingSync))
{
<button class="btn btn-outline-success btn-sm disabled" disabled>Billing Synced!</button>
}
}
else
{
@if(connection.Type.Equals(OrganizationConnectionType.CloudBillingSync))
{
<a class="btn btn-outline-secondary btn-sm"
data-id="@connection.Id" asp-controller="Organizations"
asp-action="TriggerBillingSync" asp-route-id="@Model.Organization.Id">
Manually Sync
</a>
}
}
}
</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</div>

View File

@ -1,4 +1,5 @@
@model OrganizationViewModel @inject Bit.Core.Settings.GlobalSettings GlobalSettings
@model OrganizationViewModel
@{ @{
ViewData["Title"] = "Organization: " + Model.Organization.Name; ViewData["Title"] = "Organization: " + Model.Organization.Name;
} }
@ -7,6 +8,10 @@
<h2>Information</h2> <h2>Information</h2>
@await Html.PartialAsync("_ViewInformation", Model) @await Html.PartialAsync("_ViewInformation", Model)
@if(GlobalSettings.SelfHosted)
{
@await Html.PartialAsync("Connections", Model)
}
<form asp-action="Delete" asp-route-id="@Model.Organization.Id" <form asp-action="Delete" asp-route-id="@Model.Organization.Id"
onsubmit="return confirm('Are you sure you want to delete this organization?')"> onsubmit="return confirm('Are you sure you want to delete this organization?')">
<button class="btn btn-danger" type="submit">Delete</button> <button class="btn btn-danger" type="submit">Delete</button>

View File

@ -2,5 +2,6 @@
@using Bit.Admin @using Bit.Admin
@using Bit.Admin.Models @using Bit.Admin.Models
@using Bit.Core.Enums.Provider @using Bit.Core.Enums.Provider
@using Bit.Core.Enums
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper "*, Admin" @addTagHelper "*, Admin"

View File

@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response.Organizations;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Controllers
{
[SelfHosted(SelfHostedOnly = true)]
[Authorize("Application")]
[Route("organizations/connections")]
public class OrganizationConnectionsController : Controller
{
private readonly ICreateOrganizationConnectionCommand _createOrganizationConnectionCommand;
private readonly IUpdateOrganizationConnectionCommand _updateOrganizationConnectionCommand;
private readonly IDeleteOrganizationConnectionCommand _deleteOrganizationConnectionCommand;
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
private readonly ICurrentContext _currentContext;
private readonly IGlobalSettings _globalSettings;
private readonly ILicensingService _licensingService;
public OrganizationConnectionsController(
ICreateOrganizationConnectionCommand createOrganizationConnectionCommand,
IUpdateOrganizationConnectionCommand updateOrganizationConnectionCommand,
IDeleteOrganizationConnectionCommand deleteOrganizationConnectionCommand,
IOrganizationConnectionRepository organizationConnectionRepository,
ICurrentContext currentContext,
IGlobalSettings globalSettings,
ILicensingService licensingService)
{
_createOrganizationConnectionCommand = createOrganizationConnectionCommand;
_updateOrganizationConnectionCommand = updateOrganizationConnectionCommand;
_deleteOrganizationConnectionCommand = deleteOrganizationConnectionCommand;
_organizationConnectionRepository = organizationConnectionRepository;
_currentContext = currentContext;
_globalSettings = globalSettings;
_licensingService = licensingService;
}
[HttpGet("enabled")]
public bool ConnectionsEnabled()
{
return _globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication;
}
[HttpPost]
public async Task<OrganizationConnectionResponseModel> CreateConnection([FromBody] OrganizationConnectionRequestModel model)
{
if (!await HasPermissionAsync(model?.OrganizationId))
{
throw new BadRequestException("Only the owner of an organization can create a connection.");
}
if (await HasConnectionTypeAsync(model))
{
throw new BadRequestException($"The requested organization already has a connection of type {model.Type}. Only one of each connection type may exist per organization.");
}
switch (model.Type)
{
case OrganizationConnectionType.CloudBillingSync:
var typedModel = new OrganizationConnectionRequestModel<BillingSyncConfig>(model);
var license = await _licensingService.ReadOrganizationLicenseAsync(model.OrganizationId);
typedModel.ParsedConfig.CloudOrganizationId = license.Id;
var connection = await _createOrganizationConnectionCommand.CreateAsync(typedModel.ToData());
return new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig));
default:
throw new BadRequestException($"Unknown Organization connection Type: {model.Type}");
}
}
[HttpPut("{organizationConnectionId}")]
public async Task<OrganizationConnectionResponseModel> UpdateConnection(Guid organizationConnectionId, [FromBody] OrganizationConnectionRequestModel model)
{
if (!await HasPermissionAsync(model?.OrganizationId))
{
throw new BadRequestException("Only the owner of an organization can update a connection.");
}
if (await HasConnectionTypeAsync(model, organizationConnectionId))
{
throw new BadRequestException($"The requested organization already has a connection of type {model.Type}. Only one of each connection type may exist per organization.");
}
switch (model.Type)
{
case OrganizationConnectionType.CloudBillingSync:
var typedModel = new OrganizationConnectionRequestModel<BillingSyncConfig>(model);
var connection = await _updateOrganizationConnectionCommand.UpdateAsync(typedModel.ToData(organizationConnectionId));
return new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig));
default:
throw new BadRequestException($"Unkown Organization connection Type: {model.Type}");
}
}
[HttpGet("{organizationId}/{type}")]
public async Task<OrganizationConnectionResponseModel> GetConnection(Guid organizationId, OrganizationConnectionType type)
{
if (!await HasPermissionAsync(organizationId))
{
throw new BadRequestException("Only the owner of an organization can retrieve a connection.");
}
var connections = await GetConnectionsAsync(organizationId);
var connection = connections.FirstOrDefault(c => c.Type == type);
switch (type)
{
case OrganizationConnectionType.CloudBillingSync:
return new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig));
default:
throw new BadRequestException($"Unkown Organization connection Type: {type}");
}
}
[HttpDelete("{organizationConnectionId}")]
[HttpPost("{organizationConnectionId}/delete")]
public async Task DeleteConnection(Guid organizationConnectionId)
{
var connection = await _organizationConnectionRepository.GetByIdAsync(organizationConnectionId);
if (connection == null)
{
throw new NotFoundException();
}
if (!await HasPermissionAsync(connection.OrganizationId))
{
throw new BadRequestException("Only the owner of an organization can remove a connection.");
}
await _deleteOrganizationConnectionCommand.DeleteAsync(connection);
}
private async Task<ICollection<OrganizationConnection>> GetConnectionsAsync(Guid organizationId) =>
await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organizationId, OrganizationConnectionType.CloudBillingSync);
private async Task<bool> HasConnectionTypeAsync(OrganizationConnectionRequestModel model, Guid? connectionId = null)
{
var existingConnections = await GetConnectionsAsync(model.OrganizationId);
return existingConnections.Any(c => c.Type == model.Type && (!connectionId.HasValue || c.Id != connectionId.Value));
}
private async Task<bool> HasPermissionAsync(Guid? organizationId) =>
organizationId.HasValue && await _currentContext.OrganizationOwner(organizationId.Value);
}
}

View File

@ -1,9 +1,15 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response.Organizations;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Api.Request.OrganizationSponsorships;
using Bit.Core.Models.Api.Request.OrganizationSponsorships;
using Bit.Core.Models.Api.Response.OrganizationSponsorships;
using Bit.Core.Models.Api.Response.OrganizationSponsorships;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -13,42 +19,65 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Controllers namespace Bit.Api.Controllers
{ {
[Route("organization/sponsorship")] [Route("organization/sponsorship")]
[Authorize("Application")]
public class OrganizationSponsorshipsController : Controller public class OrganizationSponsorshipsController : Controller
{ {
private readonly IOrganizationSponsorshipService _organizationsSponsorshipService;
private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IValidateRedemptionTokenCommand _validateRedemptionTokenCommand;
private readonly IValidateBillingSyncKeyCommand _validateBillingSyncKeyCommand;
private readonly ICreateSponsorshipCommand _createSponsorshipCommand;
private readonly ISendSponsorshipOfferCommand _sendSponsorshipOfferCommand;
private readonly ISetUpSponsorshipCommand _setUpSponsorshipCommand;
private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand;
private readonly IRemoveSponsorshipCommand _removeSponsorshipCommand;
private readonly ICloudSyncSponsorshipsCommand _syncSponsorshipsCommand;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IUserService _userService; private readonly IUserService _userService;
public OrganizationSponsorshipsController(IOrganizationSponsorshipService organizationSponsorshipService, public OrganizationSponsorshipsController(
IOrganizationSponsorshipRepository organizationSponsorshipRepository, IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IValidateRedemptionTokenCommand validateRedemptionTokenCommand,
IValidateBillingSyncKeyCommand validateBillingSyncKeyCommand,
ICreateSponsorshipCommand createSponsorshipCommand,
ISendSponsorshipOfferCommand sendSponsorshipOfferCommand,
ISetUpSponsorshipCommand setUpSponsorshipCommand,
IRevokeSponsorshipCommand revokeSponsorshipCommand,
IRemoveSponsorshipCommand removeSponsorshipCommand,
ICloudSyncSponsorshipsCommand syncSponsorshipsCommand,
IUserService userService, IUserService userService,
ICurrentContext currentContext) ICurrentContext currentContext)
{ {
_organizationsSponsorshipService = organizationSponsorshipService;
_organizationSponsorshipRepository = organizationSponsorshipRepository; _organizationSponsorshipRepository = organizationSponsorshipRepository;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_validateRedemptionTokenCommand = validateRedemptionTokenCommand;
_validateBillingSyncKeyCommand = validateBillingSyncKeyCommand;
_createSponsorshipCommand = createSponsorshipCommand;
_sendSponsorshipOfferCommand = sendSponsorshipOfferCommand;
_setUpSponsorshipCommand = setUpSponsorshipCommand;
_revokeSponsorshipCommand = revokeSponsorshipCommand;
_removeSponsorshipCommand = removeSponsorshipCommand;
_syncSponsorshipsCommand = syncSponsorshipsCommand;
_userService = userService; _userService = userService;
_currentContext = currentContext; _currentContext = currentContext;
} }
[Authorize("Application")]
[HttpPost("{sponsoringOrgId}/families-for-enterprise")] [HttpPost("{sponsoringOrgId}/families-for-enterprise")]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipRequestModel model) public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipCreateRequestModel model)
{ {
await _organizationsSponsorshipService.OfferSponsorshipAsync( var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync(
await _organizationRepository.GetByIdAsync(sponsoringOrgId), await _organizationRepository.GetByIdAsync(sponsoringOrgId),
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default), await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName, model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName);
(await CurrentUser).Email); await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, (await CurrentUser).Email);
} }
[Authorize("Application")]
[HttpPost("{sponsoringOrgId}/families-for-enterprise/resend")] [HttpPost("{sponsoringOrgId}/families-for-enterprise/resend")]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task ResendSponsorshipOffer(Guid sponsoringOrgId) public async Task ResendSponsorshipOffer(Guid sponsoringOrgId)
@ -56,26 +85,27 @@ namespace Bit.Api.Controllers
var sponsoringOrgUser = await _organizationUserRepository var sponsoringOrgUser = await _organizationUserRepository
.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default); .GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default);
await _organizationsSponsorshipService.ResendSponsorshipOfferAsync( await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(
await _organizationRepository.GetByIdAsync(sponsoringOrgId), await _organizationRepository.GetByIdAsync(sponsoringOrgId),
sponsoringOrgUser, sponsoringOrgUser,
await _organizationSponsorshipRepository await _organizationSponsorshipRepository
.GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id), .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id));
(await CurrentUser).Email);
} }
[Authorize("Application")]
[HttpPost("validate-token")] [HttpPost("validate-token")]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task<bool> PreValidateSponsorshipToken([FromQuery] string sponsorshipToken) public async Task<bool> PreValidateSponsorshipToken([FromQuery] string sponsorshipToken)
{ {
return (await _organizationsSponsorshipService.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email)).valid; return (await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email)).valid;
} }
[Authorize("Application")]
[HttpPost("redeem")] [HttpPost("redeem")]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBody] OrganizationSponsorshipRedeemRequestModel model) public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBody] OrganizationSponsorshipRedeemRequestModel model)
{ {
var (valid, sponsorship) = await _organizationsSponsorshipService.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email); var (valid, sponsorship) = await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email);
if (!valid) if (!valid)
{ {
@ -87,12 +117,27 @@ namespace Bit.Api.Controllers
throw new BadRequestException("Can only redeem sponsorship for an organization you own."); throw new BadRequestException("Can only redeem sponsorship for an organization you own.");
} }
await _organizationsSponsorshipService.SetUpSponsorshipAsync( await _setUpSponsorshipCommand.SetUpSponsorshipAsync(
sponsorship, sponsorship,
// Check org to sponsor's product type
await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId)); await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId));
} }
[Authorize("Installation")]
[HttpPost("sync")]
public async Task<OrganizationSponsorshipSyncResponseModel> Sync([FromBody] OrganizationSponsorshipSyncRequestModel model)
{
var sponsoringOrg = await _organizationRepository.GetByIdAsync(model.SponsoringOrganizationCloudId);
if (!await _validateBillingSyncKeyCommand.ValidateBillingSyncKeyAsync(sponsoringOrg, model.BillingSyncKey))
{
throw new BadRequestException("Invalid Billing Sync Key");
}
var (syncResponseData, offersToSend) = await _syncSponsorshipsCommand.SyncOrganization(sponsoringOrg, model.ToOrganizationSponsorshipSync().SponsorshipsBatch);
await _sendSponsorshipOfferCommand.BulkSendSponsorshipOfferAsync(sponsoringOrg.Name, offersToSend);
return new OrganizationSponsorshipSyncResponseModel(syncResponseData);
}
[Authorize("Application")]
[HttpDelete("{sponsoringOrganizationId}")] [HttpDelete("{sponsoringOrganizationId}")]
[HttpPost("{sponsoringOrganizationId}/delete")] [HttpPost("{sponsoringOrganizationId}/delete")]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
@ -108,12 +153,10 @@ namespace Bit.Api.Controllers
var existingOrgSponsorship = await _organizationSponsorshipRepository var existingOrgSponsorship = await _organizationSponsorshipRepository
.GetBySponsoringOrganizationUserIdAsync(orgUser.Id); .GetBySponsoringOrganizationUserIdAsync(orgUser.Id);
await _organizationsSponsorshipService.RevokeSponsorshipAsync( await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
await _organizationRepository
.GetByIdAsync(existingOrgSponsorship.SponsoredOrganizationId ?? default),
existingOrgSponsorship);
} }
[Authorize("Application")]
[HttpDelete("sponsored/{sponsoredOrgId}")] [HttpDelete("sponsored/{sponsoredOrgId}")]
[HttpPost("sponsored/{sponsoredOrgId}/remove")] [HttpPost("sponsored/{sponsoredOrgId}/remove")]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
@ -128,10 +171,21 @@ namespace Bit.Api.Controllers
var existingOrgSponsorship = await _organizationSponsorshipRepository var existingOrgSponsorship = await _organizationSponsorshipRepository
.GetBySponsoredOrganizationIdAsync(sponsoredOrgId); .GetBySponsoredOrganizationIdAsync(sponsoredOrgId);
await _organizationsSponsorshipService.RemoveSponsorshipAsync( await _removeSponsorshipCommand.RemoveSponsorshipAsync(existingOrgSponsorship);
await _organizationRepository }
.GetByIdAsync(existingOrgSponsorship.SponsoredOrganizationId.Value),
existingOrgSponsorship); [HttpGet("{sponsoringOrgId}/sync-status")]
public async Task<object> GetSyncStatus(Guid sponsoringOrgId)
{
var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId);
if (!await _currentContext.OrganizationOwner(sponsoringOrg.Id))
{
throw new NotFoundException();
}
var lastSyncDate = await _organizationSponsorshipRepository.GetLatestSyncDateBySponsoringOrganizationIdAsync(sponsoringOrg.Id);
return new OrganizationSponsorshipSyncStatusResponseModel(lastSyncDate);
} }
private Task<User> CurrentUser => _userService.GetUserByIdAsync(_currentContext.UserId.Value); private Task<User> CurrentUser => _userService.GetUserByIdAsync(_currentContext.UserId.Value);

View File

@ -4,11 +4,12 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.Models.Response.Organizations;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;

View File

@ -6,12 +6,14 @@ using Bit.Api.Models.Request;
using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Request.Accounts;
using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.Models.Response.Organizations;
using Bit.Api.Utilities; using Bit.Api.Utilities;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
@ -34,6 +36,9 @@ namespace Bit.Api.Controllers
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly ISsoConfigService _ssoConfigService; private readonly ISsoConfigService _ssoConfigService;
private readonly IGetOrganizationApiKeyCommand _getOrganizationApiKeyCommand;
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
public OrganizationsController( public OrganizationsController(
@ -46,6 +51,9 @@ namespace Bit.Api.Controllers
ICurrentContext currentContext, ICurrentContext currentContext,
ISsoConfigRepository ssoConfigRepository, ISsoConfigRepository ssoConfigRepository,
ISsoConfigService ssoConfigService, ISsoConfigService ssoConfigService,
IGetOrganizationApiKeyCommand getOrganizationApiKeyCommand,
IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand,
IOrganizationApiKeyRepository organizationApiKeyRepository,
GlobalSettings globalSettings) GlobalSettings globalSettings)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
@ -57,6 +65,9 @@ namespace Bit.Api.Controllers
_currentContext = currentContext; _currentContext = currentContext;
_ssoConfigRepository = ssoConfigRepository; _ssoConfigRepository = ssoConfigRepository;
_ssoConfigService = ssoConfigService; _ssoConfigService = ssoConfigService;
_getOrganizationApiKeyCommand = getOrganizationApiKeyCommand;
_rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand;
_organizationApiKeyRepository = organizationApiKeyRepository;
_globalSettings = globalSettings; _globalSettings = globalSettings;
} }
@ -482,7 +493,7 @@ namespace Bit.Api.Controllers
} }
[HttpPost("{id}/api-key")] [HttpPost("{id}/api-key")]
public async Task<ApiKeyResponseModel> ApiKey(string id, [FromBody] SecretVerificationRequestModel model) public async Task<ApiKeyResponseModel> ApiKey(string id, [FromBody] OrganizationApiKeyRequestModel model)
{ {
var orgIdGuid = new Guid(id); var orgIdGuid = new Guid(id);
if (!await _currentContext.OrganizationOwner(orgIdGuid)) if (!await _currentContext.OrganizationOwner(orgIdGuid))
@ -496,6 +507,19 @@ namespace Bit.Api.Controllers
throw new NotFoundException(); throw new NotFoundException();
} }
if (model.Type == OrganizationApiKeyType.BillingSync)
{
// Non-enterprise orgs should not be able to create or view an apikey of billing sync key type
var plan = StaticStore.GetPlan(organization.PlanType);
if (plan.Product != ProductType.Enterprise)
{
throw new NotFoundException();
}
}
var organizationApiKey = await _getOrganizationApiKeyCommand
.GetOrganizationApiKeyAsync(organization.Id, model.Type);
var user = await _userService.GetUserByPrincipalAsync(User); var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null) if (user == null)
{ {
@ -509,13 +533,27 @@ namespace Bit.Api.Controllers
} }
else else
{ {
var response = new ApiKeyResponseModel(organization); var response = new ApiKeyResponseModel(organizationApiKey);
return response; return response;
} }
} }
[HttpGet("{id}/api-key-information")]
public async Task<ListResponseModel<OrganizationApiKeyInformation>> ApiKeyInformation(Guid id)
{
if (!await _currentContext.OrganizationOwner(id))
{
throw new NotFoundException();
}
var apiKeys = await _organizationApiKeyRepository.GetManyByOrganizationIdTypeAsync(id);
return new ListResponseModel<OrganizationApiKeyInformation>(
apiKeys.Select(k => new OrganizationApiKeyInformation(k)));
}
[HttpPost("{id}/rotate-api-key")] [HttpPost("{id}/rotate-api-key")]
public async Task<ApiKeyResponseModel> RotateApiKey(string id, [FromBody] SecretVerificationRequestModel model) public async Task<ApiKeyResponseModel> RotateApiKey(string id, [FromBody] OrganizationApiKeyRequestModel model)
{ {
var orgIdGuid = new Guid(id); var orgIdGuid = new Guid(id);
if (!await _currentContext.OrganizationOwner(orgIdGuid)) if (!await _currentContext.OrganizationOwner(orgIdGuid))
@ -529,6 +567,9 @@ namespace Bit.Api.Controllers
throw new NotFoundException(); throw new NotFoundException();
} }
var organizationApiKey = await _getOrganizationApiKeyCommand
.GetOrganizationApiKeyAsync(organization.Id, model.Type);
var user = await _userService.GetUserByPrincipalAsync(User); var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null) if (user == null)
{ {
@ -542,8 +583,8 @@ namespace Bit.Api.Controllers
} }
else else
{ {
await _organizationService.RotateApiKeyAsync(organization); await _rotateOrganizationApiKeyCommand.RotateApiKeyAsync(organizationApiKey);
var response = new ApiKeyResponseModel(organization); var response = new ApiKeyResponseModel(organizationApiKey);
return response; return response;
} }
} }

View File

@ -0,0 +1,69 @@
using System;
using System.Threading.Tasks;
using Bit.Api.Models.Request.Organizations;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Controllers.SelfHosted
{
[Route("organization/sponsorship/self-hosted")]
[Authorize("Application")]
[SelfHosted(SelfHostedOnly = true)]
public class SelfHostedOrganizationSponsorshipsController : Controller
{
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;
private readonly ICreateSponsorshipCommand _offerSponsorshipCommand;
private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand;
private readonly ICurrentContext _currentContext;
public SelfHostedOrganizationSponsorshipsController(
ICreateSponsorshipCommand offerSponsorshipCommand,
IRevokeSponsorshipCommand revokeSponsorshipCommand,
IOrganizationRepository organizationRepository,
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IOrganizationUserRepository organizationUserRepository,
ICurrentContext currentContext
)
{
_offerSponsorshipCommand = offerSponsorshipCommand;
_revokeSponsorshipCommand = revokeSponsorshipCommand;
_organizationRepository = organizationRepository;
_organizationSponsorshipRepository = organizationSponsorshipRepository;
_organizationUserRepository = organizationUserRepository;
_currentContext = currentContext;
}
[HttpPost("{sponsoringOrgId}/families-for-enterprise")]
public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipCreateRequestModel model)
{
await _offerSponsorshipCommand.CreateSponsorshipAsync(
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName);
}
[HttpDelete("{sponsoringOrgId}")]
[HttpPost("{sponsoringOrgId}/delete")]
public async Task RevokeSponsorship(Guid sponsoringOrgId)
{
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default);
if (orgUser == null)
{
throw new BadRequestException("Unknown Organization User");
}
var existingOrgSponsorship = await _organizationSponsorshipRepository
.GetBySponsoringOrganizationUserIdAsync(orgUser.Id);
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
}
}
}

View File

@ -46,8 +46,15 @@ namespace Bit.Api.Jobs
.StartNow() .StartNow()
.WithCronSchedule("0 30 */12 * * ?") .WithCronSchedule("0 30 */12 * * ?")
.Build(); .Build();
var randomDailySponsorshipSyncTrigger = TriggerBuilder.Create()
.WithIdentity("RandomDailySponsorshipSyncTrigger")
.StartAt(DateBuilder.FutureDate(new Random().Next(24), IntervalUnit.Hour))
.WithSimpleSchedule(x => x
.WithIntervalInHours(24)
.RepeatForever())
.Build();
Jobs = new List<Tuple<Type, ITrigger>> var jobs = new List<Tuple<Type, ITrigger>>
{ {
new Tuple<Type, ITrigger>(typeof(AliveJob), everyTopOfTheHourTrigger), new Tuple<Type, ITrigger>(typeof(AliveJob), everyTopOfTheHourTrigger),
new Tuple<Type, ITrigger>(typeof(EmergencyAccessNotificationJob), emergencyAccessNotificationTrigger), new Tuple<Type, ITrigger>(typeof(EmergencyAccessNotificationJob), emergencyAccessNotificationTrigger),
@ -56,11 +63,22 @@ namespace Bit.Api.Jobs
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger) new Tuple<Type, ITrigger>(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger)
}; };
if (_globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication)
{
jobs.Add(new Tuple<Type, ITrigger>(typeof(SelfHostedSponsorshipSyncJob), randomDailySponsorshipSyncTrigger));
}
Jobs = jobs;
await base.StartAsync(cancellationToken); await base.StartAsync(cancellationToken);
} }
public static void AddJobsServices(IServiceCollection services) public static void AddJobsServices(IServiceCollection services, bool selfHosted)
{ {
if (selfHosted)
{
services.AddTransient<SelfHostedSponsorshipSyncJob>();
}
services.AddTransient<AliveJob>(); services.AddTransient<AliveJob>();
services.AddTransient<EmergencyAccessNotificationJob>(); services.AddTransient<EmergencyAccessNotificationJob>();
services.AddTransient<EmergencyAccessTimeoutJob>(); services.AddTransient<EmergencyAccessTimeoutJob>();

View File

@ -0,0 +1,73 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Jobs;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Quartz;
namespace Bit.Api.Jobs
{
public class SelfHostedSponsorshipSyncJob : BaseJob
{
private readonly IServiceProvider _serviceProvider;
private IOrganizationRepository _organizationRepository;
private IOrganizationConnectionRepository _organizationConnectionRepository;
private readonly ILicensingService _licensingService;
private GlobalSettings _globalSettings;
public SelfHostedSponsorshipSyncJob(
IServiceProvider serviceProvider,
IOrganizationRepository organizationRepository,
IOrganizationConnectionRepository organizationConnectionRepository,
ILicensingService licensingService,
ILogger<SelfHostedSponsorshipSyncJob> logger,
GlobalSettings globalSettings)
: base(logger)
{
_serviceProvider = serviceProvider;
_organizationRepository = organizationRepository;
_organizationConnectionRepository = organizationConnectionRepository;
_licensingService = licensingService;
_globalSettings = globalSettings;
}
protected override async Task ExecuteJobAsync(IJobExecutionContext context)
{
if (!_globalSettings.EnableCloudCommunication)
{
_logger.LogInformation("Skipping Organization sync with cloud - Cloud communication is disabled in global settings");
return;
}
var organizations = await _organizationRepository.GetManyByEnabledAsync();
using (var scope = _serviceProvider.CreateScope())
{
var syncCommand = scope.ServiceProvider.GetRequiredService<ISelfHostedSyncSponsorshipsCommand>();
foreach (var org in organizations)
{
var connection = (await _organizationConnectionRepository.GetEnabledByOrganizationIdTypeAsync(org.Id, OrganizationConnectionType.CloudBillingSync)).FirstOrDefault();
if (connection != null)
{
try
{
var config = connection.GetConfig<BillingSyncConfig>();
await syncCommand.SyncOrganization(org.Id, config.CloudOrganizationId, connection);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Sponsorship sync for organization {org.Name} Failed");
}
}
}
}
}
}
}

View File

@ -2,7 +2,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
namespace Bit.Api.Models.Public namespace Bit.Api.Models.Public
{ {

View File

@ -5,6 +5,7 @@ using System.Linq;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
namespace Bit.Api.Models.Public.Response namespace Bit.Api.Models.Public.Response
{ {

View File

@ -0,0 +1,9 @@
using Bit.Core.Enums;
namespace Bit.Api.Models.Request.Accounts
{
public class OrganizationApiKeyRequestModel : SecretVerificationRequestModel
{
public OrganizationApiKeyType Type { get; set; }
}
}

View File

@ -0,0 +1,52 @@
using System;
using System.Text.Json;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationConnections;
using Bit.Core.Utilities;
namespace Bit.Api.Models.Request.Organizations
{
public class OrganizationConnectionRequestModel
{
public OrganizationConnectionType Type { get; set; }
public Guid OrganizationId { get; set; }
public bool Enabled { get; set; }
public JsonDocument Config { get; set; }
public OrganizationConnectionRequestModel() { }
}
public class OrganizationConnectionRequestModel<T> : OrganizationConnectionRequestModel where T : new()
{
public T ParsedConfig { get; private set; }
public OrganizationConnectionRequestModel(OrganizationConnectionRequestModel model)
{
Type = model.Type;
OrganizationId = model.OrganizationId;
Enabled = model.Enabled;
Config = model.Config;
try
{
ParsedConfig = model.Config.ToObject<T>(JsonHelpers.IgnoreCase);
}
catch (JsonException)
{
throw new BadRequestException("Organization Connection configuration malformed");
}
}
public OrganizationConnectionData<T> ToData(Guid? id = null) =>
new()
{
Id = id,
Type = Type,
OrganizationId = OrganizationId,
Enabled = Enabled,
Config = ParsedConfig,
};
}
}

View File

@ -4,7 +4,7 @@ using Bit.Core.Utilities;
namespace Bit.Api.Models.Request.Organizations namespace Bit.Api.Models.Request.Organizations
{ {
public class OrganizationSponsorshipRequestModel public class OrganizationSponsorshipCreateRequestModel
{ {
[Required] [Required]
public PlanSponsorshipType PlanSponsorshipType { get; set; } public PlanSponsorshipType PlanSponsorshipType { get; set; }

View File

@ -6,6 +6,7 @@ using System.Text.Json;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Utilities; using Bit.Core.Utilities;
namespace Bit.Api.Models.Request.Organizations namespace Bit.Api.Models.Request.Organizations

View File

@ -6,14 +6,15 @@ namespace Bit.Api.Models.Response
{ {
public class ApiKeyResponseModel : ResponseModel public class ApiKeyResponseModel : ResponseModel
{ {
public ApiKeyResponseModel(Organization organization, string obj = "apiKey") public ApiKeyResponseModel(OrganizationApiKey organizationApiKey, string obj = "apiKey")
: base(obj) : base(obj)
{ {
if (organization == null) if (organizationApiKey == null)
{ {
throw new ArgumentNullException(nameof(organization)); throw new ArgumentNullException(nameof(organizationApiKey));
} }
ApiKey = organization.ApiKey; ApiKey = organizationApiKey.ApiKey;
RevisionDate = organizationApiKey.RevisionDate;
} }
public ApiKeyResponseModel(User user, string obj = "apiKey") public ApiKeyResponseModel(User user, string obj = "apiKey")
@ -24,8 +25,10 @@ namespace Bit.Api.Models.Response
throw new ArgumentNullException(nameof(user)); throw new ArgumentNullException(nameof(user));
} }
ApiKey = user.ApiKey; ApiKey = user.ApiKey;
RevisionDate = user.RevisionDate;
} }
public string ApiKey { get; set; } public string ApiKey { get; set; }
public DateTime RevisionDate { get; set; }
} }
} }

View File

@ -0,0 +1,19 @@
using System;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
namespace Bit.Api.Models.Response.Organizations
{
public class OrganizationApiKeyInformation : ResponseModel
{
public OrganizationApiKeyInformation(OrganizationApiKey key) : base("keyInformation")
{
KeyType = key.Type;
RevisionDate = key.RevisionDate;
}
public OrganizationApiKeyType KeyType { get; set; }
public DateTime RevisionDate { get; set; }
}
}

View File

@ -1,7 +1,7 @@
using System; using System;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
namespace Bit.Api.Models.Response namespace Bit.Api.Models.Response.Organizations
{ {
public class OrganizationAutoEnrollStatusResponseModel : ResponseModel public class OrganizationAutoEnrollStatusResponseModel : ResponseModel
{ {

View File

@ -0,0 +1,30 @@
using System;
using System.Text.Json;
using Bit.Core.Entities;
using Bit.Core.Enums;
namespace Bit.Api.Models.Response.Organizations
{
public class OrganizationConnectionResponseModel
{
public Guid? Id { get; set; }
public OrganizationConnectionType Type { get; set; }
public Guid OrganizationId { get; set; }
public bool Enabled { get; set; }
public JsonDocument Config { get; set; }
public OrganizationConnectionResponseModel(OrganizationConnection connection, Type configType)
{
if (connection == null)
{
return;
}
Id = connection.Id;
Type = connection.Type;
OrganizationId = connection.OrganizationId;
Enabled = connection.Enabled;
Config = JsonDocument.Parse(connection.Config);
}
}
}

View File

@ -2,7 +2,7 @@
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
namespace Bit.Api.Models.Response namespace Bit.Api.Models.Response.Organizations
{ {
public class OrganizationKeysResponseModel : ResponseModel public class OrganizationKeysResponseModel : ResponseModel
{ {

View File

@ -6,7 +6,7 @@ using Bit.Core.Models.Api;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Utilities; using Bit.Core.Utilities;
namespace Bit.Api.Models.Response namespace Bit.Api.Models.Response.Organizations
{ {
public class OrganizationResponseModel : ResponseModel public class OrganizationResponseModel : ResponseModel
{ {

View File

@ -0,0 +1,16 @@
using System;
using Bit.Core.Models.Api;
namespace Bit.Api.Models.Response.Organizations
{
public class OrganizationSponsorshipSyncStatusResponseModel : ResponseModel
{
public OrganizationSponsorshipSyncStatusResponseModel(DateTime? lastSyncDate)
: base("syncStatus")
{
LastSyncDate = lastSyncDate;
}
public DateTime? LastSyncDate { get; set; }
}
}

View File

@ -3,7 +3,7 @@ using Bit.Core.Models.Api;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Settings; using Bit.Core.Settings;
namespace Bit.Api.Models.Response namespace Bit.Api.Models.Response.Organizations
{ {
public class OrganizationSsoResponseModel : ResponseModel public class OrganizationSsoResponseModel : ResponseModel
{ {

View File

@ -5,9 +5,10 @@ using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Utilities; using Bit.Core.Utilities;
namespace Bit.Api.Models.Response namespace Bit.Api.Models.Response.Organizations
{ {
public class OrganizationUserResponseModel : ResponseModel public class OrganizationUserResponseModel : ResponseModel
{ {

View File

@ -1,6 +1,8 @@
using Bit.Core.Enums; using System;
using Bit.Core.Enums;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Utilities; using Bit.Core.Utilities;
namespace Bit.Api.Models.Response namespace Bit.Api.Models.Response
@ -45,6 +47,9 @@ namespace Bit.Api.Models.Response
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
.UsersCanSponsor(organization); .UsersCanSponsor(organization);
PlanProductType = StaticStore.GetPlan(organization.PlanType).Product; PlanProductType = StaticStore.GetPlan(organization.PlanType).Product;
FamilySponsorshipLastSyncDate = organization.FamilySponsorshipLastSyncDate;
FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete;
FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil;
if (organization.SsoConfig != null) if (organization.SsoConfig != null)
{ {
@ -88,5 +93,8 @@ namespace Bit.Api.Models.Response
public ProductType PlanProductType { get; set; } public ProductType PlanProductType { get; set; }
public bool KeyConnectorEnabled { get; set; } public bool KeyConnectorEnabled { get; set; }
public string KeyConnectorUrl { get; set; } public string KeyConnectorUrl { get; set; }
public DateTime? FamilySponsorshipLastSyncDate { get; set; }
public DateTime? FamilySponsorshipValidUntil { get; set; }
public bool? FamilySponsorshipToDelete { get; set; }
} }
} }

View File

@ -5,6 +5,7 @@ using Bit.Api.Models.Response.Providers;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
namespace Bit.Api.Models.Response namespace Bit.Api.Models.Response
{ {

View File

@ -4,6 +4,7 @@ using System.Linq;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Settings; using Bit.Core.Settings;
using Core.Models.Data; using Core.Models.Data;

View File

@ -114,12 +114,17 @@ namespace Bit.Api
policy.RequireAuthenticatedUser(); policy.RequireAuthenticatedUser();
policy.RequireClaim(JwtClaimTypes.Scope, "api.organization"); policy.RequireClaim(JwtClaimTypes.Scope, "api.organization");
}); });
config.AddPolicy("Installation", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim(JwtClaimTypes.Scope, "api.installation");
});
}); });
services.AddScoped<AuthenticatorTokenProvider>(); services.AddScoped<AuthenticatorTokenProvider>();
// Services // Services
services.AddBaseServices(); services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings); services.AddDefaultServices(globalSettings);
services.AddCoreLocalizationServices(); services.AddCoreLocalizationServices();
@ -137,15 +142,9 @@ namespace Bit.Api
}); });
services.AddSwagger(globalSettings); services.AddSwagger(globalSettings);
Jobs.JobsHostedService.AddJobsServices(services); Jobs.JobsHostedService.AddJobsServices(services, globalSettings.SelfHosted);
services.AddHostedService<Jobs.JobsHostedService>(); services.AddHostedService<Jobs.JobsHostedService>();
if (globalSettings.SelfHosted)
{
// Jobs service
Jobs.JobsHostedService.AddJobsServices(services);
services.AddHostedService<Jobs.JobsHostedService>();
}
if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) && if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) &&
CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName)) CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName))
{ {

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
@ -30,7 +31,8 @@ namespace Bit.Billing.Controllers
private readonly BillingSettings _billingSettings; private readonly BillingSettings _billingSettings;
private readonly IWebHostEnvironment _hostingEnvironment; private readonly IWebHostEnvironment _hostingEnvironment;
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
private readonly IOrganizationSponsorshipService _organizationSponsorshipService; private readonly IValidateSponsorshipCommand _validateSponsorshipCommand;
private readonly IOrganizationSponsorshipRenewCommand _organizationSponsorshipRenewCommand;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly ITransactionRepository _transactionRepository; private readonly ITransactionRepository _transactionRepository;
private readonly IUserService _userService; private readonly IUserService _userService;
@ -47,7 +49,8 @@ namespace Bit.Billing.Controllers
IOptions<BillingSettings> billingSettings, IOptions<BillingSettings> billingSettings,
IWebHostEnvironment hostingEnvironment, IWebHostEnvironment hostingEnvironment,
IOrganizationService organizationService, IOrganizationService organizationService,
IOrganizationSponsorshipService organizationSponsorshipService, IValidateSponsorshipCommand validateSponsorshipCommand,
IOrganizationSponsorshipRenewCommand organizationSponsorshipRenewCommand,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
ITransactionRepository transactionRepository, ITransactionRepository transactionRepository,
IUserService userService, IUserService userService,
@ -61,7 +64,8 @@ namespace Bit.Billing.Controllers
_billingSettings = billingSettings?.Value; _billingSettings = billingSettings?.Value;
_hostingEnvironment = hostingEnvironment; _hostingEnvironment = hostingEnvironment;
_organizationService = organizationService; _organizationService = organizationService;
_organizationSponsorshipService = organizationSponsorshipService; _validateSponsorshipCommand = validateSponsorshipCommand;
_organizationSponsorshipRenewCommand = organizationSponsorshipRenewCommand;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_transactionRepository = transactionRepository; _transactionRepository = transactionRepository;
_userService = userService; _userService = userService;
@ -142,6 +146,10 @@ namespace Bit.Billing.Controllers
{ {
await _organizationService.UpdateExpirationDateAsync(ids.Item1.Value, await _organizationService.UpdateExpirationDateAsync(ids.Item1.Value,
subscription.CurrentPeriodEnd); subscription.CurrentPeriodEnd);
if (IsSponsoredSubscription(subscription))
{
await _organizationSponsorshipRenewCommand.UpdateExpirationDateAsync(ids.Item1.Value, subscription.CurrentPeriodEnd);
}
} }
// user // user
else if (ids.Item2.HasValue) else if (ids.Item2.HasValue)
@ -169,10 +177,9 @@ namespace Bit.Billing.Controllers
if (ids.Item1.HasValue) if (ids.Item1.HasValue)
{ {
// sponsored org // sponsored org
if (CheckSponsoredSubscription(subscription)) if (IsSponsoredSubscription(subscription))
{ {
await _organizationSponsorshipService await _validateSponsorshipCommand.ValidateSponsorshipAsync(ids.Item1.Value);
.ValidateSponsorshipAsync(ids.Item1.Value);
} }
var org = await _organizationRepository.GetByIdAsync(ids.Item1.Value); var org = await _organizationRepository.GetByIdAsync(ids.Item1.Value);
@ -795,7 +802,7 @@ namespace Bit.Billing.Controllers
return subscription; return subscription;
} }
private static bool CheckSponsoredSubscription(Subscription subscription) => private static bool IsSponsoredSubscription(Subscription subscription) =>
StaticStore.SponsoredPlans.Any(p => p.StripePlanId == subscription.Id); StaticStore.SponsoredPlans.Any(p => p.StripePlanId == subscription.Id);
} }
} }

View File

@ -58,7 +58,7 @@ namespace Bit.Billing
//services.AddPasswordlessIdentityServices<ReadOnlyDatabaseIdentityUserStore>(globalSettings); //services.AddPasswordlessIdentityServices<ReadOnlyDatabaseIdentityUserStore>(globalSettings);
// Services // Services
services.AddBaseServices(); services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings); services.AddDefaultServices(globalSettings);
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

View File

@ -22,6 +22,7 @@ namespace Bit.Core.Entities
PolicyId = e.PolicyId; PolicyId = e.PolicyId;
GroupId = e.GroupId; GroupId = e.GroupId;
OrganizationUserId = e.OrganizationUserId; OrganizationUserId = e.OrganizationUserId;
InstallationId = e.InstallationId;
ProviderUserId = e.ProviderUserId; ProviderUserId = e.ProviderUserId;
ProviderOrganizationId = e.ProviderOrganizationId; ProviderOrganizationId = e.ProviderOrganizationId;
DeviceType = e.DeviceType; DeviceType = e.DeviceType;
@ -34,6 +35,7 @@ namespace Bit.Core.Entities
public EventType Type { get; set; } public EventType Type { get; set; }
public Guid? UserId { get; set; } public Guid? UserId { get; set; }
public Guid? OrganizationId { get; set; } public Guid? OrganizationId { get; set; }
public Guid? InstallationId { get; set; }
public Guid? ProviderId { get; set; } public Guid? ProviderId { get; set; }
public Guid? CipherId { get; set; } public Guid? CipherId { get; set; }
public Guid? CollectionId { get; set; } public Guid? CollectionId { get; set; }

View File

@ -60,8 +60,6 @@ namespace Bit.Core.Entities
public bool Enabled { get; set; } = true; public bool Enabled { get; set; } = true;
[MaxLength(100)] [MaxLength(100)]
public string LicenseKey { get; set; } public string LicenseKey { get; set; }
[MaxLength(30)]
public string ApiKey { get; set; }
public string PublicKey { get; set; } public string PublicKey { get; set; }
public string PrivateKey { get; set; } public string PrivateKey { get; set; }
public string TwoFactorProviders { get; set; } public string TwoFactorProviders { get; set; }

View File

@ -0,0 +1,22 @@
using System;
using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
using Bit.Core.Utilities;
namespace Bit.Core.Entities
{
public class OrganizationApiKey : ITableObject<Guid>
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public OrganizationApiKeyType Type { get; set; }
[MaxLength(30)]
public string ApiKey { get; set; }
public DateTime RevisionDate { get; set; }
public void SetNewId()
{
Id = CoreHelpers.GenerateComb();
}
}
}

View File

@ -0,0 +1,47 @@
using System;
using System.Text.Json;
using Bit.Core.Enums;
using Bit.Core.Utilities;
namespace Bit.Core.Entities
{
public class OrganizationConnection<T> : OrganizationConnection where T : new()
{
public new T Config
{
get => base.GetConfig<T>();
set => base.SetConfig<T>(value);
}
}
public class OrganizationConnection : ITableObject<Guid>
{
public Guid Id { get; set; }
public OrganizationConnectionType Type { get; set; }
public Guid OrganizationId { get; set; }
public bool Enabled { get; set; }
public string Config { get; set; }
public void SetNewId()
{
Id = CoreHelpers.GenerateComb();
}
public T GetConfig<T>() where T : new()
{
try
{
return JsonSerializer.Deserialize<T>(Config);
}
catch (JsonException)
{
return default;
}
}
public void SetConfig<T>(T config) where T : new()
{
Config = JsonSerializer.Serialize(config);
}
}
}

View File

@ -8,20 +8,17 @@ namespace Bit.Core.Entities
public class OrganizationSponsorship : ITableObject<Guid> public class OrganizationSponsorship : ITableObject<Guid>
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid? InstallationId { get; set; }
public Guid? SponsoringOrganizationId { get; set; } public Guid? SponsoringOrganizationId { get; set; }
public Guid? SponsoringOrganizationUserId { get; set; } public Guid SponsoringOrganizationUserId { get; set; }
public Guid? SponsoredOrganizationId { get; set; } public Guid? SponsoredOrganizationId { get; set; }
[MaxLength(256)] [MaxLength(256)]
public string FriendlyName { get; set; } public string FriendlyName { get; set; }
[MaxLength(256)] [MaxLength(256)]
public string OfferedToEmail { get; set; } public string OfferedToEmail { get; set; }
public PlanSponsorshipType? PlanSponsorshipType { get; set; } public PlanSponsorshipType? PlanSponsorshipType { get; set; }
[Required]
public bool CloudSponsor { get; set; }
public DateTime? LastSyncDate { get; set; } public DateTime? LastSyncDate { get; set; }
public byte TimesRenewedWithoutValidation { get; set; } public DateTime? ValidUntil { get; set; }
public DateTime? SponsorshipLapsedDate { get; set; } public bool ToDelete { get; set; }
public void SetNewId() public void SetNewId()
{ {

View File

@ -60,6 +60,7 @@
Organization_DisabledSso = 1605, Organization_DisabledSso = 1605,
Organization_EnabledKeyConnector = 1606, Organization_EnabledKeyConnector = 1606,
Organization_DisabledKeyConnector = 1607, Organization_DisabledKeyConnector = 1607,
Organization_SponsorshipsSynced = 1608,
Policy_Updated = 1700, Policy_Updated = 1700,

View File

@ -0,0 +1,8 @@
namespace Bit.Core.Enums
{
public enum OrganizationApiKeyType : byte
{
Default,
BillingSync,
}
}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Enums
{
public enum OrganizationConnectionType : byte
{
CloudBillingSync = 1,
}
}

View File

@ -30,6 +30,7 @@ namespace Bit.Core.IdentityServer
new ApiResource("api.licensing", new string[] { JwtClaimTypes.Subject }), new ApiResource("api.licensing", new string[] { JwtClaimTypes.Subject }),
new ApiResource("api.organization", new string[] { JwtClaimTypes.Subject }), new ApiResource("api.organization", new string[] { JwtClaimTypes.Subject }),
new ApiResource("api.provider", new string[] { JwtClaimTypes.Subject }), new ApiResource("api.provider", new string[] { JwtClaimTypes.Subject }),
new ApiResource("api.installation", new string[] { JwtClaimTypes.Subject }),
}; };
} }
} }

View File

@ -13,6 +13,7 @@ namespace Bit.Core.IdentityServer
new ApiScope("api.push", "API Push Access"), new ApiScope("api.push", "API Push Access"),
new ApiScope("api.licensing", "API Licensing Access"), new ApiScope("api.licensing", "API Licensing Access"),
new ApiScope("api.organization", "API Organization Access"), new ApiScope("api.organization", "API Organization Access"),
new ApiScope("api.installation", "API Installation Access"),
new ApiScope("internal", "Internal Access") new ApiScope("internal", "Internal Access")
}; };
} }

View File

@ -12,8 +12,7 @@ using Bit.Core.Enums;
using Bit.Core.Identity; using Bit.Core.Identity;
using Bit.Core.Models; using Bit.Core.Models;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
@ -27,6 +28,7 @@ namespace Bit.Core.IdentityServer
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IProviderUserRepository _providerUserRepository; private readonly IProviderUserRepository _providerUserRepository;
private readonly IProviderOrganizationRepository _providerOrganizationRepository; private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
public ClientStore( public ClientStore(
IInstallationRepository installationRepository, IInstallationRepository installationRepository,
@ -38,7 +40,8 @@ namespace Bit.Core.IdentityServer
ICurrentContext currentContext, ICurrentContext currentContext,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IProviderUserRepository providerUserRepository, IProviderUserRepository providerUserRepository,
IProviderOrganizationRepository providerOrganizationRepository) IProviderOrganizationRepository providerOrganizationRepository,
IOrganizationApiKeyRepository organizationApiKeyRepository)
{ {
_installationRepository = installationRepository; _installationRepository = installationRepository;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
@ -50,6 +53,7 @@ namespace Bit.Core.IdentityServer
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_providerUserRepository = providerUserRepository; _providerUserRepository = providerUserRepository;
_providerOrganizationRepository = providerOrganizationRepository; _providerOrganizationRepository = providerOrganizationRepository;
_organizationApiKeyRepository = organizationApiKeyRepository;
} }
public async Task<Client> FindClientByIdAsync(string clientId) public async Task<Client> FindClientByIdAsync(string clientId)
@ -67,7 +71,7 @@ namespace Bit.Core.IdentityServer
ClientId = $"installation.{installation.Id}", ClientId = $"installation.{installation.Id}",
RequireClientSecret = true, RequireClientSecret = true,
ClientSecrets = { new Secret(installation.Key.Sha256()) }, ClientSecrets = { new Secret(installation.Key.Sha256()) },
AllowedScopes = new string[] { "api.push", "api.licensing" }, AllowedScopes = new string[] { "api.push", "api.licensing", "api.installation" },
AllowedGrantTypes = GrantTypes.ClientCredentials, AllowedGrantTypes = GrantTypes.ClientCredentials,
AccessTokenLifetime = 3600 * 24, AccessTokenLifetime = 3600 * 24,
Enabled = installation.Enabled, Enabled = installation.Enabled,
@ -113,11 +117,14 @@ namespace Bit.Core.IdentityServer
var org = await _organizationRepository.GetByIdAsync(id); var org = await _organizationRepository.GetByIdAsync(id);
if (org != null) if (org != null)
{ {
var orgApiKey = (await _organizationApiKeyRepository
.GetManyByOrganizationIdTypeAsync(org.Id, OrganizationApiKeyType.Default))
.First();
return new Client return new Client
{ {
ClientId = $"organization.{org.Id}", ClientId = $"organization.{org.Id}",
RequireClientSecret = true, RequireClientSecret = true,
ClientSecrets = { new Secret(org.ApiKey.Sha256()) }, ClientSecrets = { new Secret(orgApiKey.ApiKey.Sha256()) },
AllowedScopes = new string[] { "api.organization" }, AllowedScopes = new string[] { "api.organization" },
AllowedGrantTypes = GrantTypes.ClientCredentials, AllowedGrantTypes = GrantTypes.ClientCredentials,
AccessTokenLifetime = 3600 * 1, AccessTokenLifetime = 3600 * 1,

View File

@ -2,7 +2,7 @@
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;"> <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;">
<tr style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;"> <tr style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
A Bitwarden account, {{SponsorEmail}}, has sponsored a free Families subscription for you! To activate your complimentary subscription, click the link below. A Bitwarden organization, {{SponsorOrgName}}, has sponsored a free Families subscription for you! To activate your complimentary subscription, click the link below.
</td> </td>
</tr> </tr>
<tr style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;"> <tr style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;">

View File

@ -1,5 +1,5 @@
{{#>BasicTextLayout}} {{#>BasicTextLayout}}
A Bitwarden account, {{SponsorEmail}}, has sponsored a free Families subscription for you! To activate your complimentary subscription, click the link below. A Bitwarden organization, {{SponsorOrgName}}, has sponsored a free Families subscription for you! To activate your complimentary subscription, click the link below.
{{Url}} {{Url}}
{{/BasicTextLayout}} {{/BasicTextLayout}}

View File

@ -2,7 +2,7 @@
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-text-size-adjust: none;"> <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-text-size-adjust: none;">
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-text-size-adjust: none;"> <tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
A Bitwarden account, {{SponsorEmail}}, has sponsored a free Families subscription for you! To accept your complimentary subscription, you will need to create an account with this email address. A Bitwarden organization, {{SponsorOrgName}}, has sponsored a free Families subscription for you! To accept your complimentary subscription, you will need to create an account with this email address.
</td> </td>
</tr> </tr>
<tr style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;"> <tr style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;">

View File

@ -1,5 +1,5 @@
{{#>BasicTextLayout}} {{#>BasicTextLayout}}
A Bitwarden account, {{SponsorEmail}}, has sponsored a free Families subscription for you! To accept your complimentary subscription, you will need to create an account with this email address. Click the link below. A Bitwarden organization, {{SponsorOrgName}}, has sponsored a free Families subscription for you! To accept your complimentary subscription, you will need to create an account with this email address. Click the link below.
{{Url}} {{Url}}

View File

@ -2,7 +2,7 @@
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;"> <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;">
<tr style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;"> <tr style="margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Your Families for Enterprise sponsorship will revert back to your existing payment method at the end of the current billing cycle. Your Families subscription will remain sponsored until {{ExpirationDate}}. To continue your plan, make sure you have a current payment method for the subscription. Review or update your payment method under Settings in your Families Organization.
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -1,3 +1,3 @@
{{#>BasicTextLayout}} {{#>BasicTextLayout}}
Your Families for Enterprise sponsorship will revert back to your existing payment method at the end of the current billing cycle. Your Families subscription will remain sponsored until {{Date}}. To continue your plan, make sure you have a current payment method for the subscription. Review or update your payment method under Settings in your Families Organization.
{{/BasicTextLayout}} {{/BasicTextLayout}}

View File

@ -15,16 +15,6 @@
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center"> <td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
If you do not wish to join this organization, you can safely ignore this email. If you do not wish to join this organization, you can safely ignore this email.
{{#jsonIf OrganizationCanSponsor}}
<p style="margin-top:10px">
<b
style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
Did you know?
</b>
Members of {{OrganizationName}} receive a complimentary Families subscription. Learn more at the
following link: https://bitwarden.com/help/article/families-for-enterprise/
</p>
{{/jsonIf}}
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -6,7 +6,4 @@ You have been invited to join the {{OrganizationName}} organization. To accept t
This link expires on {{ExpirationDate}}. This link expires on {{ExpirationDate}}.
If you do not wish to join this organization, you can safely ignore this email. If you do not wish to join this organization, you can safely ignore this email.
{{#jsonIf OrganizationCanSponsor}}
Did you know? Members of {{OrganizationName}} receive a complimentary Families subscription. Learn more here: https://bitwarden.com/help/article/families-for-enterprise/
{{/jsonIf}}
{{/BasicTextLayout}} {{/BasicTextLayout}}

View File

@ -0,0 +1,57 @@
using System;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationSponsorships;
namespace Bit.Core.Models.Api.Request.OrganizationSponsorships
{
public class OrganizationSponsorshipRequestModel
{
public Guid SponsoringOrganizationUserId { get; set; }
public string FriendlyName { get; set; }
public string OfferedToEmail { get; set; }
public PlanSponsorshipType PlanSponsorshipType { get; set; }
public DateTime? LastSyncDate { get; set; }
public DateTime? ValidUntil { get; set; }
public bool ToDelete { get; set; }
public OrganizationSponsorshipRequestModel() { }
public OrganizationSponsorshipRequestModel(OrganizationSponsorshipData sponsorshipData)
{
SponsoringOrganizationUserId = sponsorshipData.SponsoringOrganizationUserId;
FriendlyName = sponsorshipData.FriendlyName;
OfferedToEmail = sponsorshipData.OfferedToEmail;
PlanSponsorshipType = sponsorshipData.PlanSponsorshipType;
LastSyncDate = sponsorshipData.LastSyncDate;
ValidUntil = sponsorshipData.ValidUntil;
ToDelete = sponsorshipData.ToDelete;
}
public OrganizationSponsorshipRequestModel(OrganizationSponsorship sponsorship)
{
SponsoringOrganizationUserId = sponsorship.SponsoringOrganizationUserId;
FriendlyName = sponsorship.FriendlyName;
OfferedToEmail = sponsorship.OfferedToEmail;
PlanSponsorshipType = sponsorship.PlanSponsorshipType.GetValueOrDefault();
LastSyncDate = sponsorship.LastSyncDate;
ValidUntil = sponsorship.ValidUntil;
ToDelete = sponsorship.ToDelete;
}
public OrganizationSponsorshipData ToOrganizationSponsorship()
{
return new OrganizationSponsorshipData
{
SponsoringOrganizationUserId = SponsoringOrganizationUserId,
FriendlyName = FriendlyName,
OfferedToEmail = OfferedToEmail,
PlanSponsorshipType = PlanSponsorshipType,
LastSyncDate = LastSyncDate,
ValidUntil = ValidUntil,
ToDelete = ToDelete,
};
}
}
}

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Bit.Core.Models.Data.Organizations.OrganizationSponsorships;
namespace Bit.Core.Models.Api.Request.OrganizationSponsorships
{
public class OrganizationSponsorshipSyncRequestModel
{
public string BillingSyncKey { get; set; }
public Guid SponsoringOrganizationCloudId { get; set; }
public IEnumerable<OrganizationSponsorshipRequestModel> SponsorshipsBatch { get; set; }
public OrganizationSponsorshipSyncRequestModel() { }
public OrganizationSponsorshipSyncRequestModel(IEnumerable<OrganizationSponsorshipRequestModel> sponsorshipsBatch)
{
SponsorshipsBatch = sponsorshipsBatch;
}
public OrganizationSponsorshipSyncRequestModel(OrganizationSponsorshipSyncData syncData)
{
if (syncData == null)
{
return;
}
BillingSyncKey = syncData.BillingSyncKey;
SponsoringOrganizationCloudId = syncData.SponsoringOrganizationCloudId;
SponsorshipsBatch = syncData.SponsorshipsBatch.Select(o => new OrganizationSponsorshipRequestModel(o));
}
public OrganizationSponsorshipSyncData ToOrganizationSponsorshipSync()
{
return new OrganizationSponsorshipSyncData()
{
BillingSyncKey = BillingSyncKey,
SponsoringOrganizationCloudId = SponsoringOrganizationCloudId,
SponsorshipsBatch = SponsorshipsBatch.Select(o => o.ToOrganizationSponsorship())
};
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Text.Json.Serialization;
using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationSponsorships;
namespace Bit.Core.Models.Api.Response.OrganizationSponsorships
{
public class OrganizationSponsorshipResponseModel
{
public Guid SponsoringOrganizationUserId { get; set; }
public string FriendlyName { get; set; }
public string OfferedToEmail { get; set; }
public PlanSponsorshipType PlanSponsorshipType { get; set; }
public DateTime? LastSyncDate { get; set; }
public DateTime? ValidUntil { get; set; }
public bool ToDelete { get; set; }
public bool CloudSponsorshipRemoved { get; set; }
public OrganizationSponsorshipResponseModel() { }
public OrganizationSponsorshipResponseModel(OrganizationSponsorshipData sponsorshipData)
{
SponsoringOrganizationUserId = sponsorshipData.SponsoringOrganizationUserId;
FriendlyName = sponsorshipData.FriendlyName;
OfferedToEmail = sponsorshipData.OfferedToEmail;
PlanSponsorshipType = sponsorshipData.PlanSponsorshipType;
LastSyncDate = sponsorshipData.LastSyncDate;
ValidUntil = sponsorshipData.ValidUntil;
ToDelete = sponsorshipData.ToDelete;
CloudSponsorshipRemoved = sponsorshipData.CloudSponsorshipRemoved;
}
public OrganizationSponsorshipData ToOrganizationSponsorship()
{
return new OrganizationSponsorshipData
{
SponsoringOrganizationUserId = SponsoringOrganizationUserId,
FriendlyName = FriendlyName,
OfferedToEmail = OfferedToEmail,
PlanSponsorshipType = PlanSponsorshipType,
LastSyncDate = LastSyncDate,
ValidUntil = ValidUntil,
ToDelete = ToDelete,
CloudSponsorshipRemoved = CloudSponsorshipRemoved
};
}
}
}

View File

@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using Bit.Core.Models.Data.Organizations.OrganizationSponsorships;
namespace Bit.Core.Models.Api.Response.OrganizationSponsorships
{
public class OrganizationSponsorshipSyncResponseModel
{
public IEnumerable<OrganizationSponsorshipResponseModel> SponsorshipsBatch { get; set; }
public OrganizationSponsorshipSyncResponseModel() { }
public OrganizationSponsorshipSyncResponseModel(OrganizationSponsorshipSyncData syncData)
{
if (syncData == null)
{
return;
}
SponsorshipsBatch = syncData.SponsorshipsBatch.Select(o => new OrganizationSponsorshipResponseModel(o));
}
public OrganizationSponsorshipSyncData ToOrganizationSponsorshipSync()
{
return new OrganizationSponsorshipSyncData()
{
SponsorshipsBatch = SponsorshipsBatch.Select(o => o.ToOrganizationSponsorship())
};
}
}
}

View File

@ -210,7 +210,7 @@ namespace Bit.Core.Models.Business
} }
} }
public bool VerifyData(Organization organization, GlobalSettings globalSettings) public bool VerifyData(Organization organization, IGlobalSettings globalSettings)
{ {
if (Issued > DateTime.UtcNow || Expires < DateTime.UtcNow) if (Issued > DateTime.UtcNow || Expires < DateTime.UtcNow)
{ {

View File

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
namespace Bit.Core.Models.Business namespace Bit.Core.Models.Business
{ {

View File

@ -0,0 +1,58 @@
using System;
using System.Text.Json.Serialization;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Tokens;
namespace Bit.Core.Models.Business.Tokenables
{
public class OrganizationSponsorshipOfferTokenable : Tokenable
{
public const string ClearTextPrefix = "BWOrganizationSponsorship_";
public const string DataProtectorPurpose = "OrganizationSponsorshipDataProtector";
public const string TokenIdentifier = "OrganizationSponsorshipOfferToken";
public string Identifier { get; set; } = TokenIdentifier;
public Guid Id { get; set; }
public PlanSponsorshipType SponsorshipType { get; set; }
public string Email { get; set; }
public override bool Valid => !string.IsNullOrWhiteSpace(Email) &&
Identifier == TokenIdentifier &&
Id != default;
[JsonConstructor]
public OrganizationSponsorshipOfferTokenable() { }
public OrganizationSponsorshipOfferTokenable(OrganizationSponsorship sponsorship)
{
if (string.IsNullOrWhiteSpace(sponsorship.OfferedToEmail))
{
throw new ArgumentException("Invalid OrganizationSponsorship to create a token, OfferedToEmail is required", nameof(sponsorship));
}
Email = sponsorship.OfferedToEmail;
if (!sponsorship.PlanSponsorshipType.HasValue)
{
throw new ArgumentException("Invalid OrganizationSponsorship to create a token, PlanSponsorshipType is required", nameof(sponsorship));
}
SponsorshipType = sponsorship.PlanSponsorshipType.Value;
if (sponsorship.Id == default)
{
throw new ArgumentException("Invalid OrganizationSponsorship to create a token, Id is required", nameof(sponsorship));
}
Id = sponsorship.Id;
}
public bool IsValid(OrganizationSponsorship sponsorship, string currentUserEmail) =>
sponsorship != null &&
sponsorship.PlanSponsorshipType.HasValue &&
SponsorshipType == sponsorship.PlanSponsorshipType.Value &&
Id == sponsorship.Id &&
!string.IsNullOrWhiteSpace(sponsorship.OfferedToEmail) &&
Email.Equals(currentUserEmail, StringComparison.InvariantCultureIgnoreCase) &&
Email.Equals(sponsorship.OfferedToEmail, StringComparison.InvariantCultureIgnoreCase);
}
}

View File

@ -1,7 +1,6 @@
using System; using System;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Settings;
namespace Bit.Core.Models.Data namespace Bit.Core.Models.Data
{ {
@ -20,6 +19,7 @@ namespace Bit.Core.Models.Data
public EventType Type { get; set; } public EventType Type { get; set; }
public Guid? UserId { get; set; } public Guid? UserId { get; set; }
public Guid? OrganizationId { get; set; } public Guid? OrganizationId { get; set; }
public Guid? InstallationId { get; set; }
public Guid? ProviderId { get; set; } public Guid? ProviderId { get; set; }
public Guid? CipherId { get; set; } public Guid? CipherId { get; set; }
public Guid? CollectionId { get; set; } public Guid? CollectionId { get; set; }

View File

@ -16,6 +16,7 @@ namespace Bit.Core.Models.Data
Type = e.Type; Type = e.Type;
UserId = e.UserId; UserId = e.UserId;
OrganizationId = e.OrganizationId; OrganizationId = e.OrganizationId;
InstallationId = e.InstallationId;
ProviderId = e.ProviderId; ProviderId = e.ProviderId;
CipherId = e.CipherId; CipherId = e.CipherId;
CollectionId = e.CollectionId; CollectionId = e.CollectionId;
@ -33,6 +34,7 @@ namespace Bit.Core.Models.Data
public EventType Type { get; set; } public EventType Type { get; set; }
public Guid? UserId { get; set; } public Guid? UserId { get; set; }
public Guid? OrganizationId { get; set; } public Guid? OrganizationId { get; set; }
public Guid? InstallationId { get; set; }
public Guid? ProviderId { get; set; } public Guid? ProviderId { get; set; }
public Guid? CipherId { get; set; } public Guid? CipherId { get; set; }
public Guid? CollectionId { get; set; } public Guid? CollectionId { get; set; }

View File

@ -8,6 +8,7 @@ namespace Bit.Core.Models.Data
EventType Type { get; set; } EventType Type { get; set; }
Guid? UserId { get; set; } Guid? UserId { get; set; }
Guid? OrganizationId { get; set; } Guid? OrganizationId { get; set; }
Guid? InstallationId { get; set; }
Guid? ProviderId { get; set; } Guid? ProviderId { get; set; }
Guid? CipherId { get; set; } Guid? CipherId { get; set; }
Guid? CollectionId { get; set; } Guid? CollectionId { get; set; }

View File

@ -1,7 +1,7 @@
using System; using System;
using Bit.Core.Entities; using Bit.Core.Entities;
namespace Bit.Core.Models.Data namespace Bit.Core.Models.Data.Organizations
{ {
public class OrganizationAbility public class OrganizationAbility
{ {

View File

@ -0,0 +1,35 @@

using System;
using Bit.Core.Entities;
using Bit.Core.Enums;
namespace Bit.Core.Models.Data.Organizations.OrganizationConnections
{
public class OrganizationConnectionData<T> where T : new()
{
public Guid? Id { get; set; }
public OrganizationConnectionType Type { get; set; }
public Guid OrganizationId { get; set; }
public bool Enabled { get; set; }
public T Config { get; set; }
public OrganizationConnection ToEntity()
{
var result = new OrganizationConnection()
{
Type = Type,
OrganizationId = OrganizationId,
Enabled = Enabled,
};
result.SetConfig(Config);
if (Id.HasValue)
{
result.Id = Id.Value;
}
return result;
}
}
}

View File

@ -0,0 +1,32 @@
using System;
using Bit.Core.Entities;
using Bit.Core.Enums;
namespace Bit.Core.Models.Data.Organizations.OrganizationSponsorships
{
public class OrganizationSponsorshipData
{
public OrganizationSponsorshipData() { }
public OrganizationSponsorshipData(OrganizationSponsorship sponsorship)
{
SponsoringOrganizationUserId = sponsorship.SponsoringOrganizationUserId;
SponsoredOrganizationId = sponsorship.SponsoredOrganizationId;
FriendlyName = sponsorship.FriendlyName;
OfferedToEmail = sponsorship.OfferedToEmail;
PlanSponsorshipType = sponsorship.PlanSponsorshipType.GetValueOrDefault();
LastSyncDate = sponsorship.LastSyncDate;
ValidUntil = sponsorship.ValidUntil;
ToDelete = sponsorship.ToDelete;
}
public Guid SponsoringOrganizationUserId { get; set; }
public Guid? SponsoredOrganizationId { get; set; }
public string FriendlyName { get; set; }
public string OfferedToEmail { get; set; }
public PlanSponsorshipType PlanSponsorshipType { get; set; }
public DateTime? LastSyncDate { get; set; }
public DateTime? ValidUntil { get; set; }
public bool ToDelete { get; set; }
public bool CloudSponsorshipRemoved { get; set; }
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
namespace Bit.Core.Models.Data.Organizations.OrganizationSponsorships
{
public class OrganizationSponsorshipSyncData
{
public string BillingSyncKey { get; set; }
public Guid SponsoringOrganizationCloudId { get; set; }
public IEnumerable<OrganizationSponsorshipData> SponsorshipsBatch { get; set; }
}
}

View File

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using Bit.Core.Enums; using Bit.Core.Enums;
namespace Bit.Core.Models.Data namespace Bit.Core.Models.Data.Organizations.OrganizationUsers
{ {
public class OrganizationUserInviteData public class OrganizationUserInviteData
{ {

View File

@ -1,6 +1,6 @@
using System; using System;
namespace Bit.Core.Models.Data namespace Bit.Core.Models.Data.Organizations.OrganizationUsers
{ {
public class OrganizationUserOrganizationDetails public class OrganizationUserOrganizationDetails
{ {
@ -37,5 +37,8 @@ namespace Bit.Core.Models.Data
public string ProviderName { get; set; } public string ProviderName { get; set; }
public string FamilySponsorshipFriendlyName { get; set; } public string FamilySponsorshipFriendlyName { get; set; }
public string SsoConfig { get; set; } public string SsoConfig { get; set; }
public DateTime? FamilySponsorshipLastSyncDate { get; set; }
public DateTime? FamilySponsorshipValidUntil { get; set; }
public bool? FamilySponsorshipToDelete { get; set; }
} }
} }

View File

@ -1,6 +1,6 @@
using System; using System;
namespace Bit.Core.Models.Data namespace Bit.Core.Models.Data.Organizations.OrganizationUsers
{ {
public class OrganizationUserPublicKey public class OrganizationUserPublicKey
{ {

View File

@ -2,7 +2,7 @@
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
namespace Bit.Core.Models.Data namespace Bit.Core.Models.Data.Organizations.OrganizationUsers
{ {
public class OrganizationUserResetPasswordDetails public class OrganizationUserResetPasswordDetails
{ {

View File

@ -1,10 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Utilities;
namespace Bit.Core.Models.Data namespace Bit.Core.Models.Data.Organizations.OrganizationUsers
{ {
public class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser public class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser
{ {

View File

@ -1,7 +1,7 @@
using System.Data; using System.Data;
using Bit.Core.Entities; using Bit.Core.Entities;
namespace Bit.Core.Models.Data namespace Bit.Core.Models.Data.Organizations.OrganizationUsers
{ {
public class OrganizationUserWithCollections : OrganizationUser public class OrganizationUserWithCollections : OrganizationUser
{ {

View File

@ -2,7 +2,7 @@
{ {
public class FamiliesForEnterpriseOfferViewModel : BaseMailModel public class FamiliesForEnterpriseOfferViewModel : BaseMailModel
{ {
public string SponsorEmail { get; set; } public string SponsorOrgName { get; set; }
public string SponsoredEmail { get; set; } public string SponsoredEmail { get; set; }
public string SponsorshipToken { get; set; } public string SponsorshipToken { get; set; }
public bool ExistingAccount { get; set; } public bool ExistingAccount { get; set; }

View File

@ -1,7 +1,9 @@
namespace Bit.Core.Models.Mail.FamiliesForEnterprise using System;
namespace Bit.Core.Models.Mail.FamiliesForEnterprise
{ {
public class FamiliesForEnterpriseSponsorshipRevertingViewModel : BaseMailModel public class FamiliesForEnterpriseSponsorshipRevertingViewModel : BaseMailModel
{ {
public string OrganizationName { get; set; } public DateTime ExpirationDate { get; set; }
} }
} }

View File

@ -11,7 +11,6 @@ namespace Bit.Core.Models.Mail
public string OrganizationNameUrlEncoded { get; set; } public string OrganizationNameUrlEncoded { get; set; }
public string Token { get; set; } public string Token { get; set; }
public string ExpirationDate { get; set; } public string ExpirationDate { get; set; }
public bool OrganizationCanSponsor { get; set; }
public string Url => string.Format("{0}/accept-organization?organizationId={1}&" + public string Url => string.Format("{0}/accept-organization?organizationId={1}&" +
"organizationUserId={2}&email={3}&organizationName={4}&token={5}", "organizationUserId={2}&email={3}&organizationName={4}&token={5}",
WebVaultUrl, WebVaultUrl,

View File

@ -0,0 +1,10 @@
using System;
namespace Bit.Core.Models.OrganizationConnectionConfigs
{
public class BillingSyncConfig
{
public string BillingSyncKey { get; set; }
public Guid CloudOrganizationId { get; set; }
}
}

View File

@ -1,6 +1,6 @@
using System; using System;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
namespace Bit.Core.Models.StaticStore namespace Bit.Core.Models.StaticStore
{ {

View File

@ -0,0 +1,49 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
namespace Bit.Core.OrganizationFeatures.OrganizationApiKeys
{
public class GetOrganizationApiKeyCommand : IGetOrganizationApiKeyCommand
{
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
public GetOrganizationApiKeyCommand(IOrganizationApiKeyRepository organizationApiKeyRepository)
{
_organizationApiKeyRepository = organizationApiKeyRepository;
}
public async Task<OrganizationApiKey> GetOrganizationApiKeyAsync(Guid organizationId, OrganizationApiKeyType organizationApiKeyType)
{
if (!Enum.IsDefined(organizationApiKeyType))
{
throw new ArgumentOutOfRangeException(nameof(organizationApiKeyType), $"Invalid value for enum {nameof(OrganizationApiKeyType)}");
}
var apiKeys = await _organizationApiKeyRepository
.GetManyByOrganizationIdTypeAsync(organizationId, organizationApiKeyType);
if (apiKeys == null || !apiKeys.Any())
{
var apiKey = new OrganizationApiKey
{
OrganizationId = organizationId,
Type = organizationApiKeyType,
ApiKey = CoreHelpers.SecureRandomString(30),
RevisionDate = DateTime.UtcNow,
};
await _organizationApiKeyRepository.CreateAsync(apiKey);
return apiKey;
}
// NOTE: Currently we only allow one type of api key per organization
return apiKeys.Single();
}
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Entities;
using Bit.Core.Enums;
namespace Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces
{
public interface IGetOrganizationApiKeyCommand
{
Task<OrganizationApiKey> GetOrganizationApiKeyAsync(Guid organizationId, OrganizationApiKeyType organizationApiKeyType);
}
}

View File

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Bit.Core.Entities;
namespace Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces
{
public interface IRotateOrganizationApiKeyCommand
{
Task<OrganizationApiKey> RotateApiKeyAsync(OrganizationApiKey organizationApiKey);
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Entities;
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
namespace Bit.Core.OrganizationFeatures.OrganizationApiKeys
{
public class RotateOrganizationApiKeyCommand : IRotateOrganizationApiKeyCommand
{
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
public RotateOrganizationApiKeyCommand(IOrganizationApiKeyRepository organizationApiKeyRepository)
{
_organizationApiKeyRepository = organizationApiKeyRepository;
}
public async Task<OrganizationApiKey> RotateApiKeyAsync(OrganizationApiKey organizationApiKey)
{
organizationApiKey.ApiKey = CoreHelpers.SecureRandomString(30);
organizationApiKey.RevisionDate = DateTime.UtcNow;
await _organizationApiKeyRepository.UpsertAsync(organizationApiKey);
return organizationApiKey;
}
}
}

View File

@ -0,0 +1,23 @@
using System.Threading.Tasks;
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations.OrganizationConnections;
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.Repositories;
namespace Bit.Core.OrganizationFeatures.OrganizationConnections
{
public class CreateOrganizationConnectionCommand : ICreateOrganizationConnectionCommand
{
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
public CreateOrganizationConnectionCommand(IOrganizationConnectionRepository organizationConnectionRepository)
{
_organizationConnectionRepository = organizationConnectionRepository;
}
public async Task<OrganizationConnection> CreateAsync<T>(OrganizationConnectionData<T> connectionData) where T : new()
{
return await _organizationConnectionRepository.CreateAsync(connectionData.ToEntity());
}
}
}

View File

@ -0,0 +1,22 @@
using System.Threading.Tasks;
using Bit.Core.Entities;
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.Repositories;
namespace Bit.Core.OrganizationFeatures.OrganizationConnections
{
public class DeleteOrganizationConnectionCommand : IDeleteOrganizationConnectionCommand
{
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
public DeleteOrganizationConnectionCommand(IOrganizationConnectionRepository organizationConnectionRepository)
{
_organizationConnectionRepository = organizationConnectionRepository;
}
public async Task DeleteAsync(OrganizationConnection connection)
{
await _organizationConnectionRepository.DeleteAsync(connection);
}
}
}

View File

@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations.OrganizationConnections;
namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces
{
public interface ICreateOrganizationConnectionCommand
{
Task<OrganizationConnection> CreateAsync<T>(OrganizationConnectionData<T> connectionData) where T : new();
}
}

View File

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Bit.Core.Entities;
namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces
{
public interface IDeleteOrganizationConnectionCommand
{
Task DeleteAsync(OrganizationConnection connection);
}
}

View File

@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations.OrganizationConnections;
namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces
{
public interface IUpdateOrganizationConnectionCommand
{
Task<OrganizationConnection> UpdateAsync<T>(OrganizationConnectionData<T> connectionData) where T : new();
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationConnections;
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.Repositories;
namespace Bit.Core.OrganizationFeatures.OrganizationConnections
{
public class UpdateOrganizationConnectionCommand : IUpdateOrganizationConnectionCommand
{
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
public UpdateOrganizationConnectionCommand(IOrganizationConnectionRepository organizationConnectionRepository)
{
_organizationConnectionRepository = organizationConnectionRepository;
}
public async Task<OrganizationConnection> UpdateAsync<T>(OrganizationConnectionData<T> connectionData) where T : new()
{
if (!connectionData.Id.HasValue)
{
throw new Exception("Cannot update connection, Connection does not exist.");
}
var connection = await _organizationConnectionRepository.GetByIdAsync(connectionData.Id.Value);
if (connection == null)
{
throw new NotFoundException();
}
var entity = connectionData.ToEntity();
await _organizationConnectionRepository.UpsertAsync(entity);
return entity;
}
}
}

View File

@ -0,0 +1,77 @@
using Bit.Core.Models.Business.Tokenables;
using Bit.Core.OrganizationFeatures.OrganizationApiKeys;
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationConnections;
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.OrganizationFeatures
{
public static class OrganizationServiceCollectionExtensions
{
public static void AddOrganizationServices(this IServiceCollection services, IGlobalSettings globalSettings)
{
services.AddScoped<IOrganizationService, OrganizationService>();
services.AddTokenizers();
services.AddOrganizationConnectionCommands();
services.AddOrganizationSponsorshipCommands(globalSettings);
services.AddOrganizationApiKeyCommands();
}
private static void AddOrganizationConnectionCommands(this IServiceCollection services)
{
services.AddScoped<ICreateOrganizationConnectionCommand, CreateOrganizationConnectionCommand>();
services.AddScoped<IDeleteOrganizationConnectionCommand, DeleteOrganizationConnectionCommand>();
services.AddScoped<IUpdateOrganizationConnectionCommand, UpdateOrganizationConnectionCommand>();
}
private static void AddOrganizationSponsorshipCommands(this IServiceCollection services, IGlobalSettings globalSettings)
{
services.AddScoped<ICreateSponsorshipCommand, CreateSponsorshipCommand>();
services.AddScoped<IRemoveSponsorshipCommand, RemoveSponsorshipCommand>();
services.AddScoped<ISendSponsorshipOfferCommand, SendSponsorshipOfferCommand>();
services.AddScoped<ISetUpSponsorshipCommand, SetUpSponsorshipCommand>();
services.AddScoped<IValidateRedemptionTokenCommand, ValidateRedemptionTokenCommand>();
services.AddScoped<IValidateSponsorshipCommand, ValidateSponsorshipCommand>();
services.AddScoped<IValidateBillingSyncKeyCommand, ValidateBillingSyncKeyCommand>();
services.AddScoped<IOrganizationSponsorshipRenewCommand, OrganizationSponsorshipRenewCommand>();
services.AddScoped<ICloudSyncSponsorshipsCommand, CloudSyncSponsorshipsCommand>();
services.AddScoped<ISelfHostedSyncSponsorshipsCommand, SelfHostedSyncSponsorshipsCommand>();
services.AddScoped<ISelfHostedSyncSponsorshipsCommand, SelfHostedSyncSponsorshipsCommand>();
services.AddScoped<ICloudSyncSponsorshipsCommand, CloudSyncSponsorshipsCommand>();
services.AddScoped<IValidateBillingSyncKeyCommand, ValidateBillingSyncKeyCommand>();
if (globalSettings.SelfHosted)
{
services.AddScoped<IRevokeSponsorshipCommand, SelfHostedRevokeSponsorshipCommand>();
}
else
{
services.AddScoped<IRevokeSponsorshipCommand, CloudRevokeSponsorshipCommand>();
}
}
private static void AddOrganizationApiKeyCommands(this IServiceCollection services)
{
services.AddScoped<IGetOrganizationApiKeyCommand, GetOrganizationApiKeyCommand>();
services.AddScoped<IRotateOrganizationApiKeyCommand, RotateOrganizationApiKeyCommand>();
}
private static void AddTokenizers(this IServiceCollection services)
{
services.AddSingleton<IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>(
OrganizationSponsorshipOfferTokenable.ClearTextPrefix,
OrganizationSponsorshipOfferTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider())
);
}
}
}

View File

@ -0,0 +1,42 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise
{
public abstract class CancelSponsorshipCommand
{
protected readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;
protected readonly IOrganizationRepository _organizationRepository;
public CancelSponsorshipCommand(IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IOrganizationRepository organizationRepository)
{
_organizationSponsorshipRepository = organizationSponsorshipRepository;
_organizationRepository = organizationRepository;
}
protected virtual async Task DeleteSponsorshipAsync(OrganizationSponsorship sponsorship = null)
{
if (sponsorship == null)
{
return;
}
await _organizationSponsorshipRepository.DeleteAsync(sponsorship);
}
protected async Task MarkToDeleteSponsorshipAsync(OrganizationSponsorship sponsorship)
{
if (sponsorship == null)
{
throw new BadRequestException("The sponsorship you are trying to cancel does not exist");
}
sponsorship.ToDelete = true;
await _organizationSponsorshipRepository.UpsertAsync(sponsorship);
}
}
}

View File

@ -0,0 +1,34 @@
using System.Threading.Tasks;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories;
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud
{
public class CloudRevokeSponsorshipCommand : CancelSponsorshipCommand, IRevokeSponsorshipCommand
{
public CloudRevokeSponsorshipCommand(
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IOrganizationRepository organizationRepository) : base(organizationSponsorshipRepository, organizationRepository)
{
}
public async Task RevokeSponsorshipAsync(OrganizationSponsorship sponsorship)
{
if (sponsorship == null)
{
throw new BadRequestException("You are not currently sponsoring an organization.");
}
if (sponsorship.SponsoredOrganizationId == null)
{
await base.DeleteSponsorshipAsync(sponsorship);
}
else
{
await MarkToDeleteSponsorshipAsync(sponsorship);
}
}
}
}

View File

@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationSponsorships;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud
{
public class CloudSyncSponsorshipsCommand : ICloudSyncSponsorshipsCommand
{
private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;
private readonly IEventService _eventService;
public CloudSyncSponsorshipsCommand(
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IEventService eventService)
{
_organizationSponsorshipRepository = organizationSponsorshipRepository;
_eventService = eventService;
}
public async Task<(OrganizationSponsorshipSyncData, IEnumerable<OrganizationSponsorship>)> SyncOrganization(Organization sponsoringOrg, IEnumerable<OrganizationSponsorshipData> sponsorshipsData)
{
if (sponsoringOrg == null)
{
throw new BadRequestException("Failed to sync sponsorship - missing organization.");
}
var (processedSponsorshipsData, sponsorshipsToEmailOffer) = sponsorshipsData.Any() ?
await DoSyncAsync(sponsoringOrg, sponsorshipsData) :
(sponsorshipsData, Array.Empty<OrganizationSponsorship>());
await RecordEvent(sponsoringOrg);
return (new OrganizationSponsorshipSyncData
{
SponsorshipsBatch = processedSponsorshipsData
}, sponsorshipsToEmailOffer);
}
private async Task<(IEnumerable<OrganizationSponsorshipData> data, IEnumerable<OrganizationSponsorship> toOffer)> DoSyncAsync(Organization sponsoringOrg, IEnumerable<OrganizationSponsorshipData> sponsorshipsData)
{
var existingSponsorshipsDict = (await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrg.Id))
.ToDictionary(i => i.SponsoringOrganizationUserId);
var sponsorshipsToUpsert = new List<OrganizationSponsorship>();
var sponsorshipIdsToDelete = new List<Guid>();
var sponsorshipsToReturn = new List<OrganizationSponsorshipData>();
foreach (var selfHostedSponsorship in sponsorshipsData)
{
var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(selfHostedSponsorship.PlanSponsorshipType)?.SponsoringProductType;
if (requiredSponsoringProductType == null
|| StaticStore.GetPlan(sponsoringOrg.PlanType).Product != requiredSponsoringProductType.Value)
{
continue; // prevent unsupported sponsorships
}
if (!existingSponsorshipsDict.TryGetValue(selfHostedSponsorship.SponsoringOrganizationUserId, out var cloudSponsorship))
{
if (selfHostedSponsorship.ToDelete && selfHostedSponsorship.LastSyncDate == null)
{
continue; // prevent invalid sponsorships in cloud. These should have been deleted by self hosted
}
if (OrgDisabledForMoreThanGracePeriod(sponsoringOrg))
{
continue; // prevent new sponsorships from disabled orgs
}
cloudSponsorship = new OrganizationSponsorship
{
SponsoringOrganizationId = sponsoringOrg.Id,
SponsoringOrganizationUserId = selfHostedSponsorship.SponsoringOrganizationUserId,
FriendlyName = selfHostedSponsorship.FriendlyName,
OfferedToEmail = selfHostedSponsorship.OfferedToEmail,
PlanSponsorshipType = selfHostedSponsorship.PlanSponsorshipType,
LastSyncDate = DateTime.UtcNow,
};
}
else
{
cloudSponsorship.LastSyncDate = DateTime.UtcNow;
}
if (selfHostedSponsorship.ToDelete)
{
if (cloudSponsorship.SponsoredOrganizationId == null)
{
sponsorshipIdsToDelete.Add(cloudSponsorship.Id);
selfHostedSponsorship.CloudSponsorshipRemoved = true;
}
else
{
cloudSponsorship.ToDelete = true;
}
}
sponsorshipsToUpsert.Add(cloudSponsorship);
selfHostedSponsorship.ValidUntil = cloudSponsorship.ValidUntil;
selfHostedSponsorship.LastSyncDate = DateTime.UtcNow;
sponsorshipsToReturn.Add(selfHostedSponsorship);
}
var sponsorshipsToEmailOffer = sponsorshipsToUpsert.Where(s => s.Id == default).ToArray();
if (sponsorshipsToUpsert.Any())
{
await _organizationSponsorshipRepository.UpsertManyAsync(sponsorshipsToUpsert);
}
if (sponsorshipIdsToDelete.Any())
{
await _organizationSponsorshipRepository.DeleteManyAsync(sponsorshipIdsToDelete);
}
return (sponsorshipsToReturn, sponsorshipsToEmailOffer);
}
/// <summary>
/// True if Organization is disabled and the expiration date is more than three months ago
/// </summary>
/// <param name="organization"></param>
private bool OrgDisabledForMoreThanGracePeriod(Organization organization) =>
!organization.Enabled &&
(
!organization.ExpirationDate.HasValue ||
DateTime.UtcNow.Subtract(organization.ExpirationDate.Value).TotalDays > 93
);
private async Task RecordEvent(Organization organization)
{
await _eventService.LogOrganizationEventAsync(organization, EventType.Organization_SponsorshipsSynced);
}
}
}

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