diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginInfo.cs
index 12549bb1..8a95996e 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginInfo.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginInfo.cs
@@ -12,14 +12,24 @@ using Newtonsoft.Json.Converters;
namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts
{
[JsonConverter(typeof(StringEnumConverter))]
- public enum LoginType
+ public enum LoginAuthenticationType
{
[EnumMember(Value = "Windows")]
Windows,
[EnumMember(Value = "Sql")]
Sql,
[EnumMember(Value = "AAD")]
- AzureActiveDirectory
+ AAD,
+ [EnumMember(Value = "Others")]
+ Others
+ }
+
+ public class ServerLoginDatabaseUserMapping
+ {
+ public string Database { get; set; }
+ public string User { get; set; }
+ public string DefaultSchema { get; set; }
+ public string[] DatabaseRoles { get; set; }
}
///
@@ -27,27 +37,22 @@ namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts
///
public class LoginInfo
{
- public string LoginName { get; set; }
+ public string Name { get; set; }
- public LoginType LoginType { get; set; }
-
- public string CertificateName { get; set; }
-
- public string AsymmetricKeyName { get; set; }
+ public LoginAuthenticationType AuthenticationType { get; set; }
public bool WindowsGrantAccess { get; set; }
- public bool MustChange { get; set; }
+ public bool MustChangePassword { get; set; }
- public bool IsDisabled { get; set; }
+ public bool IsEnabled { get; set; }
+ public bool ConnectPermission { get; set; }
public bool IsLockedOut { get; set; }
- public bool EnforcePolicy { get; set; }
+ public bool EnforcePasswordPolicy { get; set; }
- public bool EnforceExpiration { get; set; }
-
- public bool WindowsAuthSupported { get; set; }
+ public bool EnforcePasswordExpiration { get; set; }
public string Password { get; set; }
@@ -56,5 +61,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts
public string DefaultLanguage { get; set; }
public string DefaultDatabase { get; set; }
+
+ public string[] ServerRoles {get; set;}
+
+ public ServerLoginDatabaseUserMapping[] UserMapping;
}
}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginRequest.cs
index 1029f2ec..5f5d168c 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginRequest.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginRequest.cs
@@ -6,7 +6,6 @@
#nullable disable
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
-using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts
@@ -16,20 +15,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts
///
public class CreateLoginParams : GeneralRequestDetails
{
- public string OwnerUri { get; set; }
+ public string ContextId { get; set; }
public LoginInfo Login { get; set; }
}
- ///
- /// Create Login result
- ///
- public class CreateLoginResult : ResultStatus
- {
- public LoginInfo Login { get; set; }
- }
-
-
///
/// Create Login request type
///
@@ -39,8 +29,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts
/// Request definition
///
public static readonly
- RequestType Type =
- RequestType.Create("security/createlogin");
+ RequestType Type =
+ RequestType.Create("objectManagement/createLogin");
}
///
@@ -48,9 +38,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts
///
public class DeleteLoginParams : GeneralRequestDetails
{
- public string OwnerUri { get; set; }
+ public string ConnectionUri { get; set; }
- public string LoginName { get; set; }
+ public string Name { get; set; }
}
///
@@ -62,7 +52,78 @@ namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts
/// Request definition
///
public static readonly
- RequestType Type =
- RequestType.Create("security/deletelogin");
+ RequestType Type =
+ RequestType.Create("objectManagement/deleteLogin");
+ }
+
+ ///
+ /// Update Login params
+ ///
+ public class UpdateLoginParams : GeneralRequestDetails
+ {
+ public string ContextId { get; set; }
+
+ public LoginInfo Login { get; set; }
+ }
+
+ ///
+ /// Update Login request type
+ ///
+ public class UpdateLoginRequest
+ {
+ ///
+ /// Request definition
+ ///
+ public static readonly
+ RequestType Type =
+ RequestType.Create("objectManagement/updateLogin");
+ }
+
+
+ ///
+ /// Update Login params
+ ///
+ public class DisposeLoginViewRequestParams : GeneralRequestDetails
+ {
+ public string ContextId { get; set; }
+ }
+
+ ///
+ /// Update Login request type
+ ///
+ public class DisposeLoginViewRequest
+ {
+ ///
+ /// Request definition
+ ///
+ public static readonly
+ RequestType Type =
+ RequestType.Create("objectManagement/disposeLoginView");
+ }
+
+ ///
+ /// Initialize Login View Request params
+ ///
+
+ public class InitializeLoginViewRequestParams : GeneralRequestDetails
+ {
+ public string ConnectionUri { get; set; }
+ public string ContextId { get; set; }
+ public bool IsNewObject { get; set; }
+
+ public string Name { get; set; }
+ }
+
+ ///
+ /// Initialize Login View request type
+ ///
+ public class InitializeLoginViewRequest
+ {
+ ///
+ /// Request definition
+ ///
+ public static readonly
+ RequestType Type =
+ RequestType.Create("objectManagement/initializeLoginView");
}
}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginViewInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginViewInfo.cs
new file mode 100644
index 00000000..28333888
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Security/Contracts/LoginViewInfo.cs
@@ -0,0 +1,22 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+namespace Microsoft.SqlTools.ServiceLayer.Security.Contracts
+{
+ public class LoginViewInfo
+ {
+
+ public LoginInfo ObjectInfo { get; set; }
+ public bool SupportWindowsAuthentication { get; set; }
+ public bool SupportAADAuthentication { get; set; }
+ public bool SupportSQLAuthentication { get; set; }
+ public bool CanEditLockedOutState { get; set; }
+ public string[] Databases;
+ public string[] Languages;
+ public string[] ServerRoles;
+ public bool SupportAdvancedPasswordOptions;
+ public bool SupportAdvancedOptions;
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/LoginData.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/LoginData.cs
index 81d177c9..7334ddd5 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/Security/LoginData.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/Security/LoginData.cs
@@ -8,6 +8,7 @@
using System;
using System.Collections;
+using System.Collections.Generic;
using System.Collections.Specialized;
using System.Data;
@@ -560,6 +561,50 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
private bool initialized;
+ // query to list all Sql and AAD logins with their role membership, ref: https://learn.microsoft.com/en-us/azure/azure-sql/database/security-server-roles?view=azuresql
+ // this is a temporary workaround for SMO not supporting server role population for Azure SQL
+ private static string AZURE_SERVER_ROLE_MEMBERSHIP_QUERY =
+@"SELECT
+ member.principal_id AS MemberPrincipalID
+ , member.name AS MemberPrincipalName
+ , roles.principal_id AS RolePrincipalID
+ , roles.name AS RolePrincipalName
+FROM sys.server_role_members AS server_role_members
+INNER JOIN sys.server_principals AS roles
+ ON server_role_members.role_principal_id = roles.principal_id
+INNER JOIN sys.server_principals AS member
+ ON server_role_members.member_principal_id = member.principal_id
+LEFT OUTER JOIN sys.sql_logins AS sql_logins
+ ON server_role_members.member_principal_id = sql_logins.principal_id
+WHERE member.principal_id NOT IN (-- prevent SQL Logins from interfering with resultset
+ SELECT principal_id FROM sys.sql_logins AS sql_logins
+ WHERE member.principal_id = sql_logins.principal_id)
+
+UNION
+
+SELECT
+ sql_logins.principal_id AS MemberPrincipalID
+ , sql_logins.name AS MemberPrincipalName
+ , roles.principal_id AS RolePrincipalID
+ , roles.name AS RolePrincipalName
+FROM sys.server_role_members AS server_role_members
+INNER JOIN sys.server_principals AS roles
+ ON server_role_members.role_principal_id = roles.principal_id
+INNER JOIN sys.sql_logins AS sql_logins
+ ON server_role_members.member_principal_id = sql_logins.principal_id
+";
+
+ private static string[] AZURE_SERVER_ROLES =
+ {
+ "##MS_DatabaseConnector##",
+ "##MS_DatabaseManager##",
+ "##MS_DefinitionReader##",
+ "##MS_LoginManager##",
+ "##MS_SecurityDefinitionReader##",
+ "##MS_ServerStateReader##",
+ "##MS_ServerStateManager##"
+ };
+
///
/// Simple description, isMember pair - used as the value in the serverRoles map
///
@@ -688,7 +733,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
if (0 != String.Compare(serverRoleName, "public", StringComparison.Ordinal))
{
- ((ServerRoleInfo) serverRoles[serverRoleName]).isMember = isMember;
+ var roleInfo = ((ServerRoleInfo) serverRoles[serverRoleName]);
+ roleInfo.isMember = isMember;
}
}
@@ -732,6 +778,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
this.initialized = true;
serverRoles.Clear();
+ if (server.DatabaseEngineType == DatabaseEngineType.SqlAzureDatabase)
+ {
+ PopulateServerRolesForAzure();
+ return;
+ }
+
try
{
foreach (ServerRole role in server.Roles)
@@ -758,6 +810,52 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
}
}
+ private void PopulateServerRolesForAzure()
+ {
+ Dictionary> roleToMembership = new Dictionary>();
+ DataSet dataset = server.ExecutionManager.ConnectionContext.ExecuteWithResults(AZURE_SERVER_ROLE_MEMBERSHIP_QUERY);
+
+ if (dataset != null)
+ {
+
+ for (int i = 0; i < dataset.Tables[0].Rows.Count; i++)
+ {
+ string login = dataset.Tables[0].Rows[i][1].ToString();
+ string role = dataset.Tables[0].Rows[i][3].ToString();
+
+ if (!roleToMembership.ContainsKey(role))
+ {
+ roleToMembership.Add(role, new HashSet());
+ }
+ HashSet members;
+ roleToMembership.TryGetValue(role, out members);
+
+ if (members != null)
+ {
+ members.Add(login);
+ }
+ }
+ }
+
+ foreach (string role in AZURE_SERVER_ROLES)
+ {
+ bool isRoleMember = false;
+
+ if (this.loginExists)
+ {
+ HashSet members;
+ roleToMembership.TryGetValue(role, out members);
+
+ if (members != null && members.Contains(this.loginName))
+ {
+ isRoleMember = true;
+ }
+ }
+
+ string roleDescription = String.Empty; // role.Description;
+ this.serverRoles.Add(role, new ServerRoleInfo(roleDescription, isRoleMember));
+ }
+ }
}
///
@@ -816,6 +914,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
private Microsoft.SqlServer.Management.Smo.Server server;
private static string defaultLanguageDisplay;
private bool windowsAuthSupported = true;
+ private bool aADAuthSupported = false;
private StringCollection credentials = null;
#endregion
@@ -953,7 +1052,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
{
get
{
- if (this.server.DatabaseEngineEdition == DatabaseEngineEdition.SqlManagedInstance)
+ if (this.server.ServerType == DatabaseEngineType.SqlAzureDatabase)
{
this.windowsAuthSupported = false;
}
@@ -962,6 +1061,19 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
}
}
+ public bool AADAuthSupported
+ {
+ get
+ {
+ if (this.server.ServerType == DatabaseEngineType.SqlAzureDatabase)
+ {
+ this.aADAuthSupported = true;
+ }
+
+ return this.aADAuthSupported;
+ }
+ }
+
public static string DefaultLanguageDisplay
{
get
@@ -1344,7 +1456,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
bool useSqlAuthentication = (login.LoginType == SqlServer.Management.Smo.LoginType.SqlLogin);
- this.windowsGrantAccess = !login.DenyWindowsLogin;
+ this.windowsGrantAccess = this.server.ServerType != DatabaseEngineType.SqlAzureDatabase ? !login.DenyWindowsLogin : false;
this.sqlPassword = useSqlAuthentication ? LoginPrototype.fakePassword : string.Empty;
this.sqlPasswordConfirm = useSqlAuthentication ? LoginPrototype.fakePassword : string.Empty;
@@ -1365,7 +1477,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
if (isYukon)
{
- if (login.LoginType == SqlServer.Management.Smo.LoginType.SqlLogin)
+ if (login.LoginType == SqlServer.Management.Smo.LoginType.SqlLogin && this.server.ServerType != DatabaseEngineType.SqlAzureDatabase)
{
// these properties make sense only for Yukon+ with SQL Authentication
this.mustChange = login.MustChangePassword;
@@ -1400,7 +1512,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
{
this.credentials.Add(login.Credential);
}
- else if (server.Information.Version.Major >= 10)
+ else if (server.Information.Version.Major >= 10 && server.ServerType != DatabaseEngineType.SqlAzureDatabase)
{
this.credentials.Clear();
foreach (string str in login.EnumCredentials())
@@ -1521,6 +1633,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
}
}
+ public bool AADAuthSupported
+ {
+ get
+ {
+ return this.currentState.AADAuthSupported;
+ }
+ }
+
///
/// Whether the Windows account is granted server access (e.g. true == grant access, false == deny access)
///
@@ -1963,12 +2083,31 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
this.originalState = (LoginPrototypeData) this.currentState.Clone();
this.comparer = new SqlCollationSensitiveStringComparer(server.Information.Collation);
- this.LoginName = login.LoginName;
+ this.LoginName = login.Name;
this.SqlPassword = login.Password;
this.OldPassword = login.OldPassword;
- this.LoginType = SqlServer.Management.Smo.LoginType.SqlLogin;
+ this.LoginType = GetLoginType(login);
this.DefaultLanguage = login.DefaultLanguage;
this.DefaultDatabase = login.DefaultDatabase;
+ this.EnforcePolicy = login.EnforcePasswordPolicy;
+ this.EnforceExpiration = login.EnforcePasswordPolicy ? login.EnforcePasswordExpiration : false;
+ this.IsLockedOut = login.IsLockedOut;
+ this.IsDisabled = !login.IsEnabled;
+ this.MustChange = login.EnforcePasswordPolicy ? login.MustChangePassword : false;
+ this.WindowsGrantAccess = login.ConnectPermission;
+ }
+
+ private LoginType GetLoginType(LoginInfo loginInfo)
+ {
+ switch (loginInfo.AuthenticationType)
+ {
+ case LoginAuthenticationType.AAD:
+ return SqlServer.Management.Smo.LoginType.ExternalUser;
+ case LoginAuthenticationType.Windows:
+ return SqlServer.Management.Smo.LoginType.WindowsUser; // TODO handle windows group
+ default:
+ return SqlServer.Management.Smo.LoginType.SqlLogin;
+ }
}
///
@@ -2030,7 +2169,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
changesMade = true;
}
- if (9 <= server.Information.Version.Major)
+ if (9 <= server.Information.Version.Major && server.ServerType != DatabaseEngineType.SqlAzureDatabase)
{
if (!this.Exists || (this.currentState.CertificateName != this.originalState.CertificateName))
{
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Security/SecurityService.cs b/src/Microsoft.SqlTools.ServiceLayer/Security/SecurityService.cs
index a0cf554f..1b26a266 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/Security/SecurityService.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/Security/SecurityService.cs
@@ -93,7 +93,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
// Login request handlers
this.ServiceHost.SetRequestHandler(CreateLoginRequest.Type, HandleCreateLoginRequest, true);
+ this.ServiceHost.SetRequestHandler(UpdateLoginRequest.Type, HandleUpdateLoginRequest, true);
this.ServiceHost.SetRequestHandler(DeleteLoginRequest.Type, HandleDeleteLoginRequest, true);
+ this.ServiceHost.SetRequestHandler(InitializeLoginViewRequest.Type, HandleInitializeLoginViewRequest, true);
+ this.ServiceHost.SetRequestHandler(DisposeLoginViewRequest.Type, HandleDisposeLoginViewRequest, true);
// User request handlers
this.ServiceHost.SetRequestHandler(InitializeUserViewRequest.Type, HandleInitializeUserViewRequest, true);
@@ -107,14 +110,17 @@ namespace Microsoft.SqlTools.ServiceLayer.Security
///
/// Handle request to create a login
///
- internal async Task HandleCreateLoginRequest(CreateLoginParams parameters, RequestContext requestContext)
+ internal async Task HandleCreateLoginRequest(CreateLoginParams parameters, RequestContext