mirror of
https://github.com/bitwarden/server.git
synced 2024-11-25 12:45:18 +01:00
Create sso user api (#886)
* facilitate linking/unlinking existing users from an sso enabled org * added user_identifier to identity methods for sso * moved sso user delete method to account controller * fixed a broken test * Update AccountsController.cs * facilitate linking/unlinking existing users from an sso enabled org * added user_identifier to identity methods for sso * moved sso user delete method to account controller * fixed a broken test * added a token to the existing user sso link flow * added a token to the existing user sso link flow * fixed a typo * added an event log for unlink ssoUser records * fixed a merge issue * fixed a busted test * fixed a busted test * ran a formatter over everything & changed .vscode settings in .gitignore * chagned a variable to use string interpolation * removed a blank line * Changed TokenPurpose enum to a static class of strings * code review cleanups * formatting fix * Changed parameters & logging for delete sso user * changed th method used to get organization user for deleting sso user records Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
This commit is contained in:
parent
7cc9ce7bd5
commit
59f8467f7c
2
.gitignore
vendored
2
.gitignore
vendored
@ -207,3 +207,5 @@ src/Core/Properties/launchSettings.json
|
||||
**/*.DS_Store
|
||||
src/Admin/wwwroot/lib
|
||||
src/Admin/wwwroot/css
|
||||
.vscode/*
|
||||
**/.vscode/*
|
||||
|
@ -1,21 +1,22 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Enums;
|
||||
using System.Linq;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core.Models.Table;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Bit.Core.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Models.Data;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Api.Controllers
|
||||
{
|
||||
@ -23,30 +24,34 @@ namespace Bit.Api.Controllers
|
||||
[Authorize("Application")]
|
||||
public class AccountsController : Controller
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ICipherRepository _cipherRepository;
|
||||
private readonly IFolderRepository _folderRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public AccountsController(
|
||||
IUserService userService,
|
||||
IUserRepository userRepository,
|
||||
GlobalSettings globalSettings,
|
||||
ICipherRepository cipherRepository,
|
||||
IFolderRepository folderRepository,
|
||||
IOrganizationService organizationService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IPaymentService paymentService,
|
||||
GlobalSettings globalSettings)
|
||||
ISsoUserRepository ssoUserRepository,
|
||||
IUserRepository userRepository,
|
||||
IUserService userService)
|
||||
{
|
||||
_userService = userService;
|
||||
_userRepository = userRepository;
|
||||
_cipherRepository = cipherRepository;
|
||||
_folderRepository = folderRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_organizationService = organizationService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_paymentService = paymentService;
|
||||
_globalSettings = globalSettings;
|
||||
_userRepository = userRepository;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
[HttpPost("prelogin")]
|
||||
@ -708,5 +713,27 @@ namespace Bit.Api.Controllers
|
||||
};
|
||||
await _paymentService.SaveTaxInfoAsync(user, taxInfo);
|
||||
}
|
||||
|
||||
[HttpDelete("sso/{organizationId}")]
|
||||
public async Task DeleteSsoUser(string organizationId)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _organizationService.DeleteSsoUserAsync(userId.Value, new Guid(organizationId));
|
||||
}
|
||||
|
||||
[HttpGet("sso/user-identifier")]
|
||||
public async Task<string> GetSsoUserIdentifier()
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var token = await _userService.GenerateSignInTokenAsync(user, TokenPurposes.LinkSso);
|
||||
var bytes = Encoding.UTF8.GetBytes($"{user.Id},{token}");
|
||||
var userIdentifier = Convert.ToBase64String(bytes);
|
||||
return userIdentifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,4 +4,9 @@
|
||||
{
|
||||
public const int BypassFiltersEventId = 12482444;
|
||||
}
|
||||
|
||||
public static class TokenPurposes
|
||||
{
|
||||
public const string LinkSso = "LinkSso";
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,7 @@
|
||||
OrganizationUser_Updated = 1502,
|
||||
OrganizationUser_Removed = 1503,
|
||||
OrganizationUser_UpdatedGroups = 1504,
|
||||
OrganizationUser_UnlinkedSso = 1505,
|
||||
|
||||
Organization_Updated = 1600,
|
||||
Organization_PurgedVault = 1601,
|
||||
|
@ -28,6 +28,7 @@ namespace Bit.Core.Models.Api
|
||||
Type = organization.Type;
|
||||
Enabled = organization.Enabled;
|
||||
SsoBound = !string.IsNullOrWhiteSpace(organization.SsoExternalId);
|
||||
Identifier = organization.Identifier;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
@ -51,5 +52,6 @@ namespace Bit.Core.Models.Api
|
||||
public OrganizationUserType Type { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public bool SsoBound { get; set; }
|
||||
public string Identifier { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -25,5 +25,6 @@ namespace Bit.Core.Models.Data
|
||||
public Enums.OrganizationUserType Type { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public string SsoExternalId { get; set; }
|
||||
public string Identifier { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
using Bit.Core.Models.Table;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Repositories
|
||||
{
|
||||
public interface ISsoUserRepository : IRepository<SsoUser, long>
|
||||
{
|
||||
Task DeleteAsync(Guid userId, Guid? organizationId);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,9 @@
|
||||
using Bit.Core.Models.Table;
|
||||
using Dapper;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Data.SqlClient;
|
||||
using System.Data;
|
||||
|
||||
namespace Bit.Core.Repositories.SqlServer
|
||||
{
|
||||
@ -11,5 +16,16 @@ namespace Bit.Core.Repositories.SqlServer
|
||||
public SsoUserRepository(string connectionString, string readOnlyConnectionString)
|
||||
: base(connectionString, readOnlyConnectionString)
|
||||
{ }
|
||||
|
||||
public async Task DeleteAsync(Guid userId, Guid? organizationId)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.ExecuteAsync(
|
||||
$"[{Schema}].[SsoUser_Delete]",
|
||||
new { UserId = userId, OrganizationId = organizationId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,5 +51,6 @@ namespace Bit.Core.Services
|
||||
IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<string> removeUserExternalIds,
|
||||
bool overwriteExisting);
|
||||
Task RotateApiKeyAsync(Organization organization);
|
||||
Task DeleteSsoUserAsync(Guid userId, Guid? organizationId);
|
||||
}
|
||||
}
|
||||
|
@ -68,5 +68,6 @@ namespace Bit.Core.Services
|
||||
Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user);
|
||||
Task<bool> TwoFactorProviderIsEnabledAsync(TwoFactorProviderType provider, ITwoFactorProvidersUser user);
|
||||
Task<string> GenerateEnterprisePortalSignInTokenAsync(User user);
|
||||
Task<string> GenerateSignInTokenAsync(User user, string purpose);
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ namespace Bit.Core.Services
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly ISsoUserRepository _ssoUserRepository;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
@ -56,6 +57,7 @@ namespace Bit.Core.Services
|
||||
IPaymentService paymentService,
|
||||
IPolicyRepository policyRepository,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
ISsoUserRepository ssoUserRepository,
|
||||
IReferenceEventService referenceEventService,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
@ -76,6 +78,7 @@ namespace Bit.Core.Services
|
||||
_paymentService = paymentService;
|
||||
_policyRepository = policyRepository;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_ssoUserRepository = ssoUserRepository;
|
||||
_referenceEventService = referenceEventService;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
@ -1497,6 +1500,19 @@ namespace Bit.Core.Services
|
||||
await ReplaceAndUpdateCache(organization);
|
||||
}
|
||||
|
||||
public async Task DeleteSsoUserAsync(Guid userId, Guid? organizationId)
|
||||
{
|
||||
await _ssoUserRepository.DeleteAsync(userId, organizationId);
|
||||
if (organizationId.HasValue)
|
||||
{
|
||||
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId.Value, userId);
|
||||
if (organizationUser != null)
|
||||
{
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UnlinkedSso);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateUsersAsync(Group group, HashSet<string> groupUsers,
|
||||
Dictionary<string, Guid> existingUsersIdDict, HashSet<Guid> existingUsers = null)
|
||||
{
|
||||
|
@ -1087,6 +1087,7 @@ namespace Bit.Core.Services
|
||||
return await CanAccessPremium(user);
|
||||
}
|
||||
|
||||
//TODO refactor this to use the below method and enum
|
||||
public async Task<string> GenerateEnterprisePortalSignInTokenAsync(User user)
|
||||
{
|
||||
var token = await GenerateUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider,
|
||||
@ -1094,6 +1095,14 @@ namespace Bit.Core.Services
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
public async Task<string> GenerateSignInTokenAsync(User user, string purpose)
|
||||
{
|
||||
var token = await GenerateUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider,
|
||||
purpose);
|
||||
return token;
|
||||
}
|
||||
|
||||
private async Task<IdentityResult> UpdatePasswordHash(User user, string newPassword,
|
||||
bool validatePassword = true, bool refreshStamp = true)
|
||||
{
|
||||
|
@ -1,62 +1,75 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Identity.Models;
|
||||
using IdentityModel;
|
||||
using IdentityServer4;
|
||||
using IdentityServer4.Services;
|
||||
using IdentityServer4.Stores;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Identity.Controllers
|
||||
{
|
||||
public class AccountController : Controller
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IClientStore _clientStore;
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly ILogger<AccountController> _logger;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public AccountController(
|
||||
IIdentityServerInteractionService interaction,
|
||||
IUserRepository userRepository,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IClientStore clientStore,
|
||||
ILogger<AccountController> logger)
|
||||
IIdentityServerInteractionService interaction,
|
||||
ILogger<AccountController> logger,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserRepository userRepository,
|
||||
IUserService userService)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_userRepository = userRepository;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_clientStore = clientStore;
|
||||
_interaction = interaction;
|
||||
_logger = logger;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Login(string returnUrl)
|
||||
{
|
||||
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||
if (context.Parameters.AllKeys.Contains("domain_hint") &&
|
||||
!string.IsNullOrWhiteSpace(context.Parameters["domain_hint"]))
|
||||
|
||||
var domainHint = context.Parameters.AllKeys.Contains("domain_hint") ?
|
||||
context.Parameters["domain_hint"] : null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(domainHint))
|
||||
{
|
||||
return RedirectToAction(nameof(ExternalChallenge),
|
||||
new { organizationIdentifier = context.Parameters["domain_hint"], returnUrl = returnUrl });
|
||||
throw new Exception("No domain_hint provided");
|
||||
}
|
||||
else
|
||||
|
||||
var userIdentifier = context.Parameters.AllKeys.Contains("user_identifier") ?
|
||||
context.Parameters["user_identifier"] : null;
|
||||
|
||||
return RedirectToAction(nameof(ExternalChallenge), new
|
||||
{
|
||||
throw new Exception("No domain_hint provided.");
|
||||
}
|
||||
organizationIdentifier = domainHint,
|
||||
returnUrl,
|
||||
userIdentifier
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ExternalChallenge(string organizationIdentifier, string returnUrl)
|
||||
public async Task<IActionResult> ExternalChallenge(string organizationIdentifier, string returnUrl,
|
||||
string userIdentifier)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(organizationIdentifier))
|
||||
{
|
||||
@ -82,6 +95,11 @@ namespace Bit.Identity.Controllers
|
||||
},
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(userIdentifier))
|
||||
{
|
||||
props.Items.Add("user_identifier", userIdentifier);
|
||||
}
|
||||
|
||||
return Challenge(props, scheme);
|
||||
}
|
||||
|
||||
|
@ -100,6 +100,7 @@ namespace Bit.Identity
|
||||
{
|
||||
// Pass domain_hint onto the sso idp
|
||||
context.ProtocolMessage.DomainHint = context.Properties.Items["domain_hint"];
|
||||
context.ProtocolMessage.SessionState = context.Properties.Items["user_identifier"];
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
};
|
||||
|
@ -18,6 +18,7 @@ SELECT
|
||||
O.[Seats],
|
||||
O.[MaxCollections],
|
||||
O.[MaxStorageGb],
|
||||
O.[Identifier],
|
||||
OU.[Key],
|
||||
OU.[Status],
|
||||
OU.[Type],
|
||||
|
@ -1,6 +1,3 @@
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Api.Controllers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Enums;
|
||||
@ -12,21 +9,26 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using NSubstitute;
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.Controllers
|
||||
{
|
||||
public class AccountsControllerTests : IDisposable
|
||||
{
|
||||
private readonly AccountsController _sut;
|
||||
|
||||
private readonly IUserService _userService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly AccountsController _sut;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ICipherRepository _cipherRepository;
|
||||
private readonly IFolderRepository _folderRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ISsoUserRepository _ssoUserRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public AccountsControllerTests()
|
||||
{
|
||||
@ -34,17 +36,20 @@ namespace Bit.Api.Test.Controllers
|
||||
_userRepository = Substitute.For<IUserRepository>();
|
||||
_cipherRepository = Substitute.For<ICipherRepository>();
|
||||
_folderRepository = Substitute.For<IFolderRepository>();
|
||||
_organizationService = Substitute.For<IOrganizationService>();
|
||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||
_paymentService = Substitute.For<IPaymentService>();
|
||||
_globalSettings = new GlobalSettings();
|
||||
_sut = new AccountsController(
|
||||
_userService,
|
||||
_userRepository,
|
||||
_globalSettings,
|
||||
_cipherRepository,
|
||||
_folderRepository,
|
||||
_organizationService,
|
||||
_organizationUserRepository,
|
||||
_paymentService,
|
||||
_globalSettings
|
||||
_ssoUserRepository,
|
||||
_userRepository,
|
||||
_userService
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -33,13 +33,14 @@ namespace Bit.Core.Test.Services
|
||||
var paymentService = Substitute.For<IPaymentService>();
|
||||
var policyRepo = Substitute.For<IPolicyRepository>();
|
||||
var ssoConfigRepo = Substitute.For<ISsoConfigRepository>();
|
||||
var ssoUserRepo = Substitute.For<ISsoUserRepository>();
|
||||
var referenceEventService = Substitute.For<IReferenceEventService>();
|
||||
var globalSettings = Substitute.For<GlobalSettings>();
|
||||
|
||||
var orgService = new OrganizationService(orgRepo, orgUserRepo, collectionRepo, userRepo,
|
||||
groupRepo, dataProtector, mailService, pushNotService, pushRegService, deviceRepo,
|
||||
licenseService, eventService, installationRepo, appCacheService, paymentService, policyRepo,
|
||||
ssoConfigRepo, referenceEventService, globalSettings);
|
||||
ssoConfigRepo, ssoUserRepo, referenceEventService, globalSettings);
|
||||
|
||||
var id = Guid.NewGuid();
|
||||
var userId = Guid.NewGuid();
|
||||
@ -93,13 +94,14 @@ namespace Bit.Core.Test.Services
|
||||
var paymentService = Substitute.For<IPaymentService>();
|
||||
var policyRepo = Substitute.For<IPolicyRepository>();
|
||||
var ssoConfigRepo = Substitute.For<ISsoConfigRepository>();
|
||||
var ssoUserRepo = Substitute.For<ISsoUserRepository>();
|
||||
var referenceEventService = Substitute.For<IReferenceEventService>();
|
||||
var globalSettings = Substitute.For<GlobalSettings>();
|
||||
|
||||
var orgService = new OrganizationService(orgRepo, orgUserRepo, collectionRepo, userRepo,
|
||||
groupRepo, dataProtector, mailService, pushNotService, pushRegService, deviceRepo,
|
||||
licenseService, eventService, installationRepo, appCacheService, paymentService, policyRepo,
|
||||
ssoConfigRepo, referenceEventService, globalSettings);
|
||||
ssoConfigRepo, ssoUserRepo, referenceEventService, globalSettings);
|
||||
|
||||
var id = Guid.NewGuid();
|
||||
var userId = Guid.NewGuid();
|
||||
|
@ -0,0 +1,39 @@
|
||||
IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationUserOrganizationDetailsView')
|
||||
BEGIN
|
||||
DROP VIEW [dbo].[OrganizationUserOrganizationDetailsView];
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE VIEW [dbo].[OrganizationUserOrganizationDetailsView]
|
||||
AS
|
||||
SELECT
|
||||
OU.[UserId],
|
||||
OU.[OrganizationId],
|
||||
O.[Name],
|
||||
O.[Enabled],
|
||||
O.[UsePolicies],
|
||||
O.[UseSso],
|
||||
O.[UseGroups],
|
||||
O.[UseDirectory],
|
||||
O.[UseEvents],
|
||||
O.[UseTotp],
|
||||
O.[Use2fa],
|
||||
O.[UseApi],
|
||||
O.[SelfHost],
|
||||
O.[UsersGetPremium],
|
||||
O.[Seats],
|
||||
O.[MaxCollections],
|
||||
O.[MaxStorageGb],
|
||||
O.[Identifier],
|
||||
OU.[Key],
|
||||
OU.[Status],
|
||||
OU.[Type],
|
||||
SU.[ExternalId] SsoExternalId
|
||||
FROM
|
||||
[dbo].[OrganizationUser] OU
|
||||
INNER JOIN
|
||||
[dbo].[Organization] O ON O.[Id] = OU.[OrganizationId]
|
||||
LEFT JOIN
|
||||
[dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId]
|
||||
|
||||
GO
|
Loading…
Reference in New Issue
Block a user