1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-10 13:23:18 +00:00

stub out electron app

This commit is contained in:
Kyle Spearrin
2018-04-24 17:31:40 -04:00
parent 2afbeb1c10
commit e1e532ed91
78 changed files with 2301 additions and 8284 deletions

5
.gitignore vendored
View File

@@ -258,4 +258,7 @@ paket-files/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
*.pyc
build/
dist/

4
.gitmodules vendored Normal file
View File

@@ -0,0 +1,4 @@
[submodule "jslib"]
path = jslib
url = https://github.com/bitwarden/jslib.git
branch = master

View File

@@ -1,38 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "bitwarden-dc", ".", "{A4DE5293-DB47-41D1-8890-7C67B83F663C}"
ProjectSection(WebsiteProperties) = preProject
TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0"
Debug.AspNetCompiler.VirtualPath = "/localhost_4405"
Debug.AspNetCompiler.PhysicalPath = "."
Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_4405\"
Debug.AspNetCompiler.Updateable = "true"
Debug.AspNetCompiler.ForceOverwrite = "true"
Debug.AspNetCompiler.FixedNames = "false"
Debug.AspNetCompiler.Debug = "True"
Release.AspNetCompiler.VirtualPath = "/localhost_4405"
Release.AspNetCompiler.PhysicalPath = "."
Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_4405\"
Release.AspNetCompiler.Updateable = "true"
Release.AspNetCompiler.ForceOverwrite = "true"
Release.AspNetCompiler.FixedNames = "false"
Release.AspNetCompiler.Debug = "False"
VWDPort = "4405"
SlnRelativePath = "."
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A4DE5293-DB47-41D1-8890-7C67B83F663C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4DE5293-DB47-41D1-8890-7C67B83F663C}.Debug|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -1,37 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.10
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Console", "src\Console\Console.csproj", "{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "src\Core\Core.csproj", "{AE082484-A34C-4B3A-A69F-49E5EF298B27}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Service", "src\Service\Service.csproj", "{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}.Release|Any CPU.Build.0 = Release|Any CPU
{AE082484-A34C-4B3A-A69F-49E5EF298B27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AE082484-A34C-4B3A-A69F-49E5EF298B27}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AE082484-A34C-4B3A-A69F-49E5EF298B27}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AE082484-A34C-4B3A-A69F-49E5EF298B27}.Release|Any CPU.Build.0 = Release|Any CPU
{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {52CB25C1-56A6-4FBE-977C-E842EA0AFAD7}
EndGlobalSection
EndGlobal

View File

@@ -1,41 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27004.2009
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Console", "src\Console\Console.csproj", "{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "src\Core\Core.csproj", "{AE082484-A34C-4B3A-A69F-49E5EF298B27}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Service", "src\Service\Service.csproj", "{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}"
EndProject
Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "Setup", "src\Setup\Setup.vdproj", "{4D852DF8-9327-43D0-93AB-FA68D4F3414B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD4E5CD2-C9DD-4912-9A25-1600A07BF8C2}.Release|Any CPU.Build.0 = Release|Any CPU
{AE082484-A34C-4B3A-A69F-49E5EF298B27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AE082484-A34C-4B3A-A69F-49E5EF298B27}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AE082484-A34C-4B3A-A69F-49E5EF298B27}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AE082484-A34C-4B3A-A69F-49E5EF298B27}.Release|Any CPU.Build.0 = Release|Any CPU
{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8FD8CED-5510-4EBD-AACE-5D3CBB7516DB}.Release|Any CPU.Build.0 = Release|Any CPU
{4D852DF8-9327-43D0-93AB-FA68D4F3414B}.Debug|Any CPU.ActiveCfg = Debug
{4D852DF8-9327-43D0-93AB-FA68D4F3414B}.Release|Any CPU.ActiveCfg = Release
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AB7EF6F7-C38B-4E66-BBF9-1F6915896CB0}
EndGlobalSection
EndGlobal

1
jslib Submodule

Submodule jslib added at 5d3b99ce6f

74
package-lock.json generated
View File

@@ -955,6 +955,12 @@
"isarray": "1.0.0"
}
},
"buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
"dev": true
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -2723,6 +2729,15 @@
"integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
"dev": true
},
"encoding": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
"dev": true,
"requires": {
"iconv-lite": "0.4.21"
}
},
"end-of-stream": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
@@ -3718,7 +3733,8 @@
"jsbn": {
"version": "0.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"json-schema": {
"version": "0.2.3",
@@ -4393,6 +4409,52 @@
"retry-axios": "0.3.2"
}
},
"google-fonts-webpack-plugin": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/google-fonts-webpack-plugin/-/google-fonts-webpack-plugin-0.4.4.tgz",
"integrity": "sha512-+e2D9/DVBG9EDydRovzoqMZ658SsTBGbC0c65GyZqkwNvdj8vRSYQKXqbz7/yt7QaXsCPT1MpH45r3ivWOitcw==",
"dev": true,
"requires": {
"lodash": "4.17.5",
"node-fetch": "1.7.3",
"webpack-sources": "0.2.3",
"yauzl": "2.9.1"
},
"dependencies": {
"source-list-map": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-1.1.2.tgz",
"integrity": "sha1-mIkBnRAkzOVc3AaUmDN+9hhqEaE=",
"dev": true
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true
},
"webpack-sources": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-0.2.3.tgz",
"integrity": "sha1-F8Yr+vE8cH+dAsR54Nzd6DgGl/s=",
"dev": true,
"requires": {
"source-list-map": "1.1.2",
"source-map": "0.5.7"
}
},
"yauzl": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.9.1.tgz",
"integrity": "sha1-qBmB6nCleUYTOIPwKcWCGok1mn8=",
"dev": true,
"requires": {
"buffer-crc32": "0.2.13",
"fd-slicer": "1.0.1"
}
}
}
},
"google-p12-pem": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.2.tgz",
@@ -5980,6 +6042,16 @@
"semver": "5.5.0"
}
},
"node-fetch": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
"integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
"dev": true,
"requires": {
"encoding": "0.1.12",
"is-stream": "1.1.0"
}
},
"node-forge": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz",

View File

@@ -61,6 +61,7 @@
"extract-text-webpack-plugin": "^3.0.1",
"file-loader": "^1.1.5",
"font-awesome": "4.7.0",
"google-fonts-webpack-plugin": "^0.4.4",
"html-loader": "^0.5.1",
"html-webpack-plugin": "^2.30.1",
"node-loader": "^0.6.0",

View File

@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net461</TargetFramework>
<RootNamespace>Bit.Console</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net461</TargetFramework>
<RootNamespace>Bit.Core</RootNamespace>
<AssemblyName>Core</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Apis.Admin.Directory.directory_v1" Version="1.31.0.1061" />
<PackageReference Include="Microsoft.Graph" Version="1.6.2" />
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="3.17.2" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.1.3" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net461' ">
<Reference Include="System.DirectoryServices" />
<Reference Include="System.ServiceProcess" />
<Reference Include="System.Security" />
<Reference Include="System" />
<Reference Include="Microsoft.CSharp" />
</ItemGroup>
</Project>

View File

@@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bit.Core.Enums
{
public enum DirectoryType : byte
{
ActiveDirectory = 0,
AzureActiveDirectory = 1,
Other = 2,
GSuite = 3
}
}

View File

@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bit.Core.Enums
{
public enum OrganizationUserStatusType : byte
{
Invited = 0,
Accepted = 1,
Confirmed = 2
}
}

View File

@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bit.Core.Enums
{
public enum OrganizationUserType : byte
{
Owner = 0,
Admin = 1,
User = 2
}
}

View File

@@ -1,13 +0,0 @@
namespace Bit.Core.Enums
{
public enum TwoFactorProviderType : byte
{
Authenticator = 0,
Email = 1,
Duo = 2,
YubiKey = 3,
U2f = 4,
Remember = 5,
OrganizationDuo = 6
}
}

View File

@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bit.Core.Enums
{
[Flags]
public enum UserAccountControl : int
{
AccountDisabled = 0x00000002,
LockOut = 0x00000010,
}
}

View File

@@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bit.Core.Models
{
public class ApiError
{
public string Message { get; set; }
}
}

View File

@@ -1,75 +0,0 @@
using System.Collections.Generic;
using System.Net;
namespace Bit.Core.Models
{
public class ApiResult<T>
{
private List<ApiError> m_errors = new List<ApiError>();
public bool Succeeded { get; private set; }
public T Result { get; set; }
public IEnumerable<ApiError> Errors => m_errors;
public HttpStatusCode StatusCode { get; private set; }
public static ApiResult<T> Success(T result, HttpStatusCode statusCode)
{
return new ApiResult<T>
{
Succeeded = true,
Result = result,
StatusCode = statusCode
};
}
public static ApiResult<T> Failed(HttpStatusCode statusCode, params ApiError[] errors)
{
var result = new ApiResult<T>
{
Succeeded = false,
StatusCode = statusCode
};
if(errors != null)
{
result.m_errors.AddRange(errors);
}
return result;
}
}
public class ApiResult
{
private List<ApiError> m_errors = new List<ApiError>();
public bool Succeeded { get; private set; }
public IEnumerable<ApiError> Errors => m_errors;
public HttpStatusCode StatusCode { get; private set; }
public static ApiResult Success(HttpStatusCode statusCode)
{
return new ApiResult
{
Succeeded = true,
StatusCode = statusCode
};
}
public static ApiResult Failed(HttpStatusCode statusCode, params ApiError[] errors)
{
var result = new ApiResult
{
Succeeded = false,
StatusCode = statusCode
};
if(errors != null)
{
result.m_errors.AddRange(errors);
}
return result;
}
}
}

View File

@@ -1,9 +0,0 @@
namespace Bit.Core.Models
{
public class AzureConfiguration
{
public string Tenant { get; set; } = "yourcompany.onmicrosoft.com";
public string Id { get; set; }
public EncryptedData Secret { get; set; }
}
}

View File

@@ -1,60 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace Bit.Core.Models
{
public class EncryptedData
{
public EncryptedData() { }
public EncryptedData(byte[] plainValue)
{
IV = RandomBytes();
#if NET461
Value = ProtectedData.Protect(plainValue, IV, DataProtectionScope.LocalMachine);
#endif
}
public EncryptedData(string plainValue)
{
var bytes = Encoding.UTF8.GetBytes(plainValue);
IV = RandomBytes();
#if NET461
Value = ProtectedData.Protect(bytes, IV, DataProtectionScope.LocalMachine);
#endif
}
public byte[] Value { get; set; }
public byte[] IV { get; set; }
public byte[] Decrypt()
{
#if NET461
return ProtectedData.Unprotect(Value, IV, DataProtectionScope.LocalMachine);
#else
return new byte[0];
#endif
}
public string DecryptToString()
{
#if NET461
var bytes = ProtectedData.Unprotect(Value, IV, DataProtectionScope.LocalMachine);
#else
var bytes = new byte[0];
#endif
return Encoding.UTF8.GetString(bytes);
}
private byte[] RandomBytes()
{
var entropy = new byte[16];
new RNGCryptoServiceProvider().GetBytes(entropy);
return entropy;
}
}
}

View File

@@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bit.Core.Models
{
public abstract class Entry
{
public string ReferenceId { get; set; }
public string ExternalId { get; set; }
public DateTime? CreationDate { get; set; }
public DateTime? RevisionDate { get; set; }
}
public class GroupEntry : Entry
{
public string Name { get; set; }
public HashSet<string> UserMemberExternalIds { get; set; } = new HashSet<string>();
public HashSet<string> GroupMemberReferenceIds { get; set; } = new HashSet<string>();
}
public class UserEntry : Entry
{
public string Email { get; set; }
public bool Disabled { get; set; }
public bool Deleted { get; set; }
}
}

View File

@@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bit.Core.Models
{
public class ErrorResponse
{
public string Message { get; set; }
public Dictionary<string, IEnumerable<string>> ValidationErrors { get; set; }
// For use in development environments.
public string ExceptionMessage { get; set; }
public string ExceptionStackTrace { get; set; }
public string InnerExceptionMessage { get; set; }
}
}

View File

@@ -1,10 +0,0 @@
namespace Bit.Core.Models
{
public class GSuiteConfiguration
{
public string SecretFile { get; set; } = "client_secret.json";
public string Customer { get; set; }
public string Domain { get; set; } = "yourcompany.com";
public string AdminUser { get; set; } = "adminuser@yourcompany.com";
}
}

View File

@@ -1,47 +0,0 @@
using Bit.Core.Services;
using System.Collections.Generic;
using System.Linq;
namespace Bit.Core.Models
{
public class ImportRequest
{
public ImportRequest(List<GroupEntry> groups, List<UserEntry> users)
{
Groups = groups?.Select(g => new Group(g)).ToArray() ?? new Group[] { };
Users = users?.Select(u => new User(u)).ToArray() ?? new User[] { };
}
public Group[] Groups { get; set; }
public User[] Users { get; set; }
public class Group
{
public Group(GroupEntry entry)
{
Name = entry.Name;
ExternalId = entry.ExternalId;
Users = entry.UserMemberExternalIds;
}
public string Name { get; set; }
public string ExternalId { get; set; }
public IEnumerable<string> Users { get; set; }
}
public class User
{
public User(UserEntry entry)
{
Email = entry.Email;
Deleted = (SettingsService.Instance.Sync.RemoveDisabledUsers && entry.Disabled) || entry.Deleted;
ExternalId = entry.ExternalId;
}
public string ExternalId { get; set; }
public string Email { get; set; }
public bool Deleted { get; set; }
}
}
}

View File

@@ -1,64 +0,0 @@
using Bit.Core.Services;
using System;
#if NET461
using System.DirectoryServices;
#endif
namespace Bit.Core.Models
{
public class LdapConfiguration
{
public string Address { get; set; }
public string Port { get; set; } = "389";
public string Path { get; set; }
public string Username { get; set; }
public EncryptedData Password { get; set; }
public Enums.DirectoryType Type { get; set; } = Enums.DirectoryType.ActiveDirectory;
#if NET461
public DirectoryEntry GetUserDirectoryEntry()
{
return GetPathedDirectoryEntry(SettingsService.Instance.Sync.Ldap.UserPath);
}
public DirectoryEntry GetGroupDirectoryEntry()
{
return GetPathedDirectoryEntry(SettingsService.Instance.Sync.Ldap.GroupPath);
}
public DirectoryEntry GetPathedDirectoryEntry(string pathPrefix = null)
{
var path = Path;
if(!string.IsNullOrWhiteSpace(pathPrefix))
{
path = string.Concat(pathPrefix, ",", path);
}
return GetDirectoryEntry(path);
}
public DirectoryEntry GetBasePathDirectoryEntry()
{
var path = Path.Substring(Path.IndexOf("dc=", StringComparison.InvariantCultureIgnoreCase));
return GetDirectoryEntry(path);
}
public DirectoryEntry GetDirectoryEntry(string path = null)
{
if(Password == null && string.IsNullOrWhiteSpace(Username))
{
return new DirectoryEntry(ServerPath(path));
}
else
{
return new DirectoryEntry(ServerPath(path), Username, Password.DecryptToString(), AuthenticationTypes.None);
}
}
#endif
private string ServerPath(string path)
{
return $"LDAP://{Address}:{Port}/{path}";
}
}
}

View File

@@ -1,19 +0,0 @@
using Bit.Core.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bit.Core.Models
{
public class LoginResult
{
public bool Success { get; set; }
public string ErrorMessage { get; set; }
public bool TwoFactorRequired => TwoFactorProviders != null && TwoFactorProviders.Count > 0;
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders { get; set; }
public string MasterPasswordHash { get; set; }
public List<Organization> Organizations { get; set; }
}
}

View File

@@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bit.Core.Models
{
public class Organization
{
public Organization() { }
public Organization(ProfileOrganizationResponseModel org)
{
Name = org.Name;
Id = org.Id;
}
public string Name { get; set; }
public string Id { get; set; }
}
}

View File

@@ -1,20 +0,0 @@
using Bit.Core.Enums;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bit.Core.Models
{
public class ProfileOrganizationResponseModel
{
public string Id { get; set; }
public string Name { get; set; }
public string Key { get; set; }
public OrganizationUserStatusType Status { get; set; }
public OrganizationUserType Type { get; set; }
public bool Enabled { get; set; }
}
}

View File

@@ -1,20 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bit.Core.Models
{
public class ProfileResponse
{
public string Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string MasterPasswordHint { get; set; }
public string Culture { get; set; }
public bool TwoFactorEnabled { get; set; }
public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; }
}
}

View File

@@ -1,13 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace Bit.Core.Models
{
public class ServerConfiguration
{
public Enums.DirectoryType Type { get; set; } = Enums.DirectoryType.ActiveDirectory;
public LdapConfiguration Ldap { get; set; }
public AzureConfiguration Azure { get; set; }
public GSuiteConfiguration GSuite { get; set; }
}
}

View File

@@ -1,102 +0,0 @@
using Bit.Core.Enums;
namespace Bit.Core.Models
{
public class SyncConfiguration
{
public SyncConfiguration() { }
public SyncConfiguration(DirectoryType type)
{
Ldap = new LdapSyncConfiguration(type);
switch(type)
{
case DirectoryType.ActiveDirectory:
break;
case DirectoryType.AzureActiveDirectory:
break;
case DirectoryType.Other:
break;
case DirectoryType.GSuite:
break;
default:
break;
}
}
/*
* Depending on what server type you are using, filters are be one of the following:
*
* 1. ActiveDirectory or Other
* - LDAP query/filter syntax
* - Read more at: http://bit.ly/2qyLpzW
* - ex. "(&(givenName=John)(|(l=Dallas)(l=Austin)))"
*
* 2. AzureActiveDirectory
* - OData syntax for a Microsoft Graph query parameter '$filter'
* - Read more at http://bit.ly/2q3FOOD
* - ex. "startswith(displayName,'J')"
*
* 3. G Suite
* - Group Filter
* - Custom filtering syntax that allows you to exclude or include a comma separated list of group names.
* - ex. "include:Group A,Sales People,My Other Group"
* or "exclude:Group C,Developers,Some Other Group"
* - User Filter
* - Custom filtering syntax that allows you to exclude or include a comma separated list of group names.
* - Allows you to concatenate a G Suite Admin API user search query to the end of the filter after delimiting
* the include/exclude filter with a pipe (|).
* - Read more at http://bit.ly/2rlTskX
* - ex.
* or "include:joe@company.com,bill@company.com,tom@company.com"
* or "exclude:john@company.com,bill@company.com|orgName=Engineering orgTitle:Manager"
* or "|orgName=Engineering orgTitle:Manager"
*/
public string GroupFilter { get; set; }
public string UserFilter { get; set; }
public bool SyncGroups { get; set; } = true;
public bool SyncUsers { get; set; } = true;
public int IntervalMinutes { get; set; } = 5;
public bool RemoveDisabledUsers { get; set; }
public LdapSyncConfiguration Ldap { get; set; } = new LdapSyncConfiguration();
public class LdapSyncConfiguration
{
public LdapSyncConfiguration() { }
public LdapSyncConfiguration(DirectoryType type)
{
switch(type)
{
case DirectoryType.ActiveDirectory:
CreationDateAttribute = "whenCreated";
RevisionDateAttribute = "whenChanged";
UserEmailPrefixAttribute = "sAMAccountName";
UserPath = "CN=Users";
GroupPath = "CN=Users";
break;
case DirectoryType.Other:
break;
default:
break;
}
}
public string UserPath { get; set; }
public string GroupPath { get; set; }
public string UserObjectClass { get; set; } = "person";
public string GroupObjectClass { get; set; } = "group";
public string MemberAttribute { get; set; } = "member";
public string GroupNameAttribute { get; set; } = "name";
public string UserEmailAttribute { get; set; } = "mail";
public bool EmailPrefixSuffix { get; set; } = false;
public string UserEmailPrefixAttribute { get; set; } = "cn";
public string UserEmailSuffix { get; set; } = "@companyname.com";
public string CreationDateAttribute { get; set; }
public string RevisionDateAttribute { get; set; }
}
}
}

View File

@@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bit.Core.Models
{
public class SyncResult
{
public bool Success { get; set; }
public string ErrorMessage { get; set; }
public List<GroupEntry> Groups { get; set; } = new List<GroupEntry>();
public List<UserEntry> Users { get; set; } = new List<UserEntry>();
}
}

View File

@@ -1,36 +0,0 @@
using Bit.Core.Enums;
using System;
using System.Collections.Generic;
namespace Bit.Core.Models
{
public class TokenRequest
{
public string Email { get; set; }
public string MasterPasswordHash { get; set; }
public string Token { get; set; }
public TwoFactorProviderType? Provider { get; set; }
public bool Remember { get; set; }
public IDictionary<string, string> ToIdentityTokenRequest()
{
var dict = new Dictionary<string, string>
{
{ "grant_type", "password" },
{ "username", Email },
{ "password", MasterPasswordHash },
{ "scope", "api offline_access" },
{ "client_id", "connector" }
};
if(Token != null && Provider.HasValue)
{
dict.Add("TwoFactorToken", Token);
dict.Add("TwoFactorProvider", ((byte)(Provider.Value)).ToString());
dict.Add("TwoFactorRemember", Remember ? "1" : "0");
}
return dict;
}
}
}

View File

@@ -1,22 +0,0 @@
using Bit.Core.Enums;
using Newtonsoft.Json;
using System.Collections.Generic;
namespace Bit.Core.Models
{
public class TokenResponse
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("expires_in")]
public long ExpiresIn { get; set; }
[JsonProperty("refresh_token")]
public string RefreshToken { get; set; }
[JsonProperty("token_type")]
public string TokenType { get; set; }
public Dictionary<TwoFactorProviderType, Dictionary<string, object>> TwoFactorProviders2 { get; set; }
public string PrivateKey { get; set; }
public string TwoFactorToken { get; set; }
public string Key { get; set; }
}
}

View File

@@ -1,8 +0,0 @@
namespace Bit.Core.Models
{
public class TwoFactorEmailRequest
{
public string Email { get; set; }
public string MasterPasswordHash { get; set; }
}
}

View File

@@ -1,322 +0,0 @@
using Bit.Core.Enums;
using Bit.Core.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace Bit.Core.Services
{
public class ApiService
{
private static ApiService _instance;
private ApiService()
{
Client = new HttpClient();
}
public static ApiService Instance
{
get
{
if(_instance == null)
{
_instance = new ApiService();
}
return _instance;
}
}
protected HttpClient Client { get; private set; }
public virtual async Task<ApiResult<TokenResponse>> PostTokenAsync(TokenRequest requestObj)
{
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri(string.Concat(SettingsService.Instance.IdentityBaseUrl, "/connect/token")),
Content = new FormUrlEncodedContent(requestObj.ToIdentityTokenRequest())
};
try
{
var response = await Client.SendAsync(requestMessage).ConfigureAwait(false);
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if(!response.IsSuccessStatusCode)
{
var errorResponse = JObject.Parse(responseContent);
if(errorResponse["TwoFactorProviders2"] != null)
{
return ApiResult<TokenResponse>.Success(new TokenResponse
{
TwoFactorProviders2 = errorResponse["TwoFactorProviders2"]
.ToObject<Dictionary<TwoFactorProviderType, Dictionary<string, object>>>()
}, response.StatusCode);
}
return await HandleErrorAsync<TokenResponse>(response).ConfigureAwait(false);
}
var responseObj = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
return ApiResult<TokenResponse>.Success(responseObj, response.StatusCode);
}
catch
{
return HandledWebException<TokenResponse>();
}
}
public virtual async Task<ApiResult> PostImportAsync(ImportRequest requestObj)
{
var tokenStateResponse = await HandleTokenStateAsync();
if(!tokenStateResponse.Succeeded)
{
return tokenStateResponse;
}
var stringContent = JsonConvert.SerializeObject(requestObj);
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri(string.Concat(SettingsService.Instance.ApiBaseUrl, "/organizations/",
SettingsService.Instance.Organization.Id, "/import")),
Content = new StringContent(stringContent, Encoding.UTF8, "application/json"),
};
requestMessage.Headers.Add("Authorization", $"Bearer {TokenService.Instance.AccessToken}");
try
{
var response = await Client.SendAsync(requestMessage).ConfigureAwait(false);
if(!response.IsSuccessStatusCode)
{
return await HandleErrorAsync(response).ConfigureAwait(false);
}
return ApiResult.Success(response.StatusCode);
}
catch
{
return HandledWebException();
}
}
public virtual async Task<ApiResult<ProfileResponse>> GetProfileAsync()
{
var tokenStateResponse = await HandleTokenStateAsync<ProfileResponse>();
if(!tokenStateResponse.Succeeded)
{
return tokenStateResponse;
}
var requestMessage = new HttpRequestMessage()
{
Method = HttpMethod.Get,
RequestUri = new Uri(string.Concat(SettingsService.Instance.ApiBaseUrl, "/accounts/profile")),
};
requestMessage.Headers.Add("Authorization", $"Bearer {TokenService.Instance.AccessToken}");
try
{
var response = await Client.SendAsync(requestMessage).ConfigureAwait(false);
if(!response.IsSuccessStatusCode)
{
return await HandleErrorAsync<ProfileResponse>(response).ConfigureAwait(false);
}
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var responseObj = JsonConvert.DeserializeObject<ProfileResponse>(responseContent);
return ApiResult<ProfileResponse>.Success(responseObj, response.StatusCode);
}
catch
{
return HandledWebException<ProfileResponse>();
}
}
public virtual async Task<ApiResult> PostTwoFactorSendEmailLoginAsync(TwoFactorEmailRequest requestObj)
{
var stringContent = JsonConvert.SerializeObject(requestObj);
var requestMessage = new HttpRequestMessage()
{
Method = HttpMethod.Post,
RequestUri = new Uri(string.Concat(SettingsService.Instance.ApiBaseUrl, "/two-factor/send-email-login")),
Content = new StringContent(stringContent, Encoding.UTF8, "application/json")
};
try
{
var response = await Client.SendAsync(requestMessage).ConfigureAwait(false);
if(!response.IsSuccessStatusCode)
{
return await HandleErrorAsync(response).ConfigureAwait(false);
}
return ApiResult.Success(response.StatusCode);
}
catch
{
return HandledWebException();
}
}
protected ApiResult HandledWebException()
{
return ApiResult.Failed(HttpStatusCode.BadGateway,
new ApiError { Message = "There is a problem connecting to the server." });
}
protected ApiResult<T> HandledWebException<T>()
{
return ApiResult<T>.Failed(HttpStatusCode.BadGateway,
new ApiError { Message = "There is a problem connecting to the server." });
}
protected async Task<ApiResult> HandleTokenStateAsync()
{
return await HandleTokenStateAsync(
() => ApiResult.Success(HttpStatusCode.OK),
() => HandledWebException(),
(r) => HandleErrorAsync(r));
}
protected async Task<ApiResult<T>> HandleTokenStateAsync<T>()
{
return await HandleTokenStateAsync(
() => ApiResult<T>.Success(default(T), HttpStatusCode.OK),
() => HandledWebException<T>(),
(r) => HandleErrorAsync<T>(r));
}
private async Task<T> HandleTokenStateAsync<T>(Func<T> success, Func<T> webException,
Func<HttpResponseMessage, Task<T>> error)
{
if(TokenService.Instance.AccessTokenNeedsRefresh && !string.IsNullOrWhiteSpace(TokenService.Instance.RefreshToken))
{
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri(string.Concat(SettingsService.Instance.IdentityBaseUrl, "/connect/token")),
Content = new FormUrlEncodedContent(
new Dictionary<string, string>
{
{ "grant_type", "refresh_token" },
{ "client_id", "connector" },
{ "refresh_token", TokenService.Instance.RefreshToken }
})
};
try
{
var response = await Client.SendAsync(requestMessage).ConfigureAwait(false);
if(!response.IsSuccessStatusCode)
{
if(response.StatusCode == HttpStatusCode.BadRequest)
{
response.StatusCode = HttpStatusCode.Unauthorized;
}
return await error.Invoke(response).ConfigureAwait(false);
}
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
TokenService.Instance.AccessToken = tokenResponse.AccessToken;
TokenService.Instance.RefreshToken = tokenResponse.RefreshToken;
}
catch
{
return webException.Invoke();
}
}
return success.Invoke();
}
protected async Task<ApiResult<T>> HandleErrorAsync<T>(HttpResponseMessage response)
{
try
{
var errors = await ParseErrorsAsync(response).ConfigureAwait(false);
return ApiResult<T>.Failed(response.StatusCode, errors.ToArray());
}
catch
{ }
return ApiResult<T>.Failed(response.StatusCode,
new ApiError { Message = "An unknown error has occurred." });
}
protected async Task<ApiResult> HandleErrorAsync(HttpResponseMessage response)
{
try
{
var errors = await ParseErrorsAsync(response).ConfigureAwait(false);
return ApiResult.Failed(response.StatusCode, errors.ToArray());
}
catch
{ }
return ApiResult.Failed(response.StatusCode,
new ApiError { Message = "An unknown error has occurred." });
}
private async Task<List<ApiError>> ParseErrorsAsync(HttpResponseMessage response)
{
var errors = new List<ApiError>();
var statusCode = (int)response.StatusCode;
if(statusCode >= 400 && statusCode <= 500)
{
ErrorResponse errorResponseModel = null;
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if(!string.IsNullOrWhiteSpace(responseContent))
{
var errorResponse = JObject.Parse(responseContent);
if(errorResponse["ErrorModel"] != null && errorResponse["ErrorModel"]["Message"] != null)
{
errorResponseModel = errorResponse["ErrorModel"].ToObject<ErrorResponse>();
}
else if(errorResponse["Message"] != null)
{
errorResponseModel = errorResponse.ToObject<ErrorResponse>();
}
}
if(errorResponseModel != null)
{
if((errorResponseModel.ValidationErrors?.Count ?? 0) > 0)
{
foreach(var valError in errorResponseModel.ValidationErrors)
{
foreach(var errorMessage in valError.Value)
{
errors.Add(new ApiError { Message = errorMessage });
}
}
}
else
{
errors.Add(new ApiError { Message = errorResponseModel.Message });
}
}
}
if(errors.Count == 0)
{
errors.Add(new ApiError { Message = "An unknown error has occurred." });
}
return errors;
}
}
}

View File

@@ -1,155 +0,0 @@
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Utilities;
using System.Linq;
using System.Threading.Tasks;
namespace Bit.Core.Services
{
public class AuthService
{
private static AuthService _instance;
private AuthService() { }
public static AuthService Instance
{
get
{
if(_instance == null)
{
_instance = new AuthService();
}
return _instance;
}
}
public bool Authenticated => !string.IsNullOrWhiteSpace(TokenService.Instance.AccessToken);
public bool OrganizationSet => SettingsService.Instance.Organization != null;
public void LogOut()
{
TokenService.Instance.AccessToken = null;
TokenService.Instance.RefreshToken = null;
}
public async Task<LoginResult> LogInAsync(string email, string masterPassword)
{
var normalizedEmail = email.Trim().ToLower();
var key = Crypto.MakeKeyFromPassword(masterPassword, normalizedEmail);
var request = new TokenRequest
{
Email = normalizedEmail,
MasterPasswordHash = Crypto.HashPasswordBase64(key, masterPassword)
};
var response = await ApiService.Instance.PostTokenAsync(request);
masterPassword = null;
key = null;
var result = new LoginResult();
if(!response.Succeeded)
{
result.Success = false;
result.ErrorMessage = response.Errors.FirstOrDefault()?.Message;
return result;
}
result.Success = true;
if(response.Result.TwoFactorProviders2 != null && response.Result.TwoFactorProviders2.Count > 0)
{
result.TwoFactorProviders = response.Result.TwoFactorProviders2;
result.MasterPasswordHash = request.MasterPasswordHash;
return result;
}
return await ProcessLogInSuccessAsync(response.Result);
}
public async Task<LoginResult> LogInTwoFactorAsync(TwoFactorProviderType type, string token, string email,
string masterPassword)
{
var normalizedEmail = email.Trim().ToLower();
var key = Crypto.MakeKeyFromPassword(masterPassword, normalizedEmail);
var result = await LogInTwoFactorWithHashAsync(type, token, email, Crypto.HashPasswordBase64(key, masterPassword));
key = null;
masterPassword = null;
return result;
}
public async Task<LoginResult> LogInTwoFactorWithHashAsync(TwoFactorProviderType type, string token, string email,
string masterPasswordHash)
{
if(type == TwoFactorProviderType.Email || type == TwoFactorProviderType.Authenticator)
{
token = token.Trim().Replace(" ", "");
}
var request = new TokenRequest
{
Email = email.Trim().ToLower(),
MasterPasswordHash = masterPasswordHash,
Token = token,
Provider = type,
Remember = false
};
var response = await ApiService.Instance.PostTokenAsync(request);
if(!response.Succeeded)
{
var result = new LoginResult();
result.Success = false;
result.ErrorMessage = response.Errors.FirstOrDefault()?.Message;
return result;
}
return await ProcessLogInSuccessAsync(response.Result);
}
private async Task<LoginResult> ProcessLogInSuccessAsync(TokenResponse response)
{
TokenService.Instance.AccessToken = response.AccessToken;
TokenService.Instance.RefreshToken = response.RefreshToken;
var result = new LoginResult();
var profile = await ApiService.Instance.GetProfileAsync();
if(profile.Succeeded)
{
var adminOrgs = profile.Result.Organizations.Where(o =>
o.Status == OrganizationUserStatusType.Confirmed &&
o.Type != OrganizationUserType.User);
if(!adminOrgs.Any())
{
LogOut();
result.Success = false;
result.ErrorMessage = "You are not an admin of any organizations.";
return result;
}
result.Organizations = adminOrgs.Select(o => new Organization(o)).ToList();
if(result.Organizations.Count == 1)
{
SettingsService.Instance.Organization = new Organization(adminOrgs.First());
}
result.Success = true;
return result;
}
else
{
LogOut();
result.Success = false;
result.ErrorMessage = "Could not load profile.";
return result;
}
}
}
}

View File

@@ -1,353 +0,0 @@
using Bit.Core.Models;
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.Graph;
using System.Linq;
using Bit.Core.Utilities;
namespace Bit.Core.Services
{
public class AzureDirectoryService : IDirectoryService
{
private static AzureDirectoryService _instance;
private static GraphServiceClient _graphClient;
private AzureDirectoryService()
{
_graphClient = new GraphServiceClient(new AzureAuthenticationProvider());
}
public static IDirectoryService Instance
{
get
{
if(_instance == null)
{
_instance = new AzureDirectoryService();
}
return _instance;
}
}
public async Task<Tuple<List<GroupEntry>, List<UserEntry>>> GetEntriesAsync(bool force = false)
{
if(!AuthService.Instance.Authenticated || !AuthService.Instance.OrganizationSet)
{
throw new ApplicationException("Not logged in or have an org set.");
}
if(SettingsService.Instance.Server?.Azure == null)
{
throw new ApplicationException("No configuration for directory server.");
}
if(SettingsService.Instance.Sync == null)
{
throw new ApplicationException("No configuration for sync.");
}
List<UserEntry> users = null;
if(SettingsService.Instance.Sync.SyncUsers)
{
users = await GetUsersAsync(force);
}
List<GroupEntry> groups = null;
if(SettingsService.Instance.Sync.SyncGroups)
{
var filter = CreateSetFromFilter(SettingsService.Instance.Sync.GroupFilter);
groups = await GetGroupsAsync(force || (users?.Any(u => !u.Deleted && !u.Disabled) ?? false), filter);
if(filter != null && users != null)
{
users = users.Where(u => u.Disabled || u.Deleted ||
groups.Any(g => g.UserMemberExternalIds.Contains(u.ExternalId))).ToList();
}
}
return new Tuple<List<GroupEntry>, List<UserEntry>>(groups, users);
}
private async static Task<List<GroupEntry>> GetGroupsAsync(bool force, Tuple<bool, HashSet<string>> filter)
{
if(!SettingsService.Instance.Sync.SyncGroups)
{
throw new ApplicationException("Not configured to sync groups.");
}
if(SettingsService.Instance.Server?.Azure == null)
{
throw new ApplicationException("No configuration for directory server.");
}
if(SettingsService.Instance.Sync == null)
{
throw new ApplicationException("No configuration for sync.");
}
if(!AuthService.Instance.Authenticated)
{
throw new ApplicationException("Not authenticated.");
}
var entries = new List<GroupEntry>();
var changedGroupIds = new List<string>();
var getFullResults = SettingsService.Instance.GroupDeltaToken == null || force;
try
{
var delataRequest = _graphClient.Groups.Delta().Request();
if(!getFullResults)
{
delataRequest.QueryOptions.Add(new QueryOption("$deltatoken", SettingsService.Instance.GroupDeltaToken));
}
var groupsDelta = await delataRequest.GetAsync();
while(true)
{
if(getFullResults)
{
foreach(var group in groupsDelta)
{
if(FilterOutResult(filter, group.DisplayName))
{
continue;
}
var entry = await BuildGroupAsync(group);
entries.Add(entry);
}
}
else
{
changedGroupIds.AddRange(groupsDelta.Select(g => g.Id));
}
if(groupsDelta.NextPageRequest == null)
{
object deltaLink;
if(groupsDelta.AdditionalData.TryGetValue("@odata.deltaLink", out deltaLink))
{
var deltaUriQuery = new Uri(deltaLink.ToString()).ParseQueryString();
if(deltaUriQuery["$deltatoken"] != null)
{
SettingsService.Instance.GroupDeltaToken = deltaUriQuery["$deltatoken"];
}
}
break;
}
else
{
groupsDelta = await groupsDelta.NextPageRequest.GetAsync();
}
}
}
catch { }
if(getFullResults || !changedGroupIds.Any())
{
return entries;
}
var groups = await _graphClient.Groups.Request().GetAsync();
while(true)
{
foreach(var group in groups)
{
if(FilterOutResult(filter, group.DisplayName))
{
continue;
}
var entry = await BuildGroupAsync(group);
entries.Add(entry);
}
if(groups.NextPageRequest == null)
{
break;
}
else
{
groups = await groups.NextPageRequest.GetAsync();
}
}
return entries;
}
private async static Task<GroupEntry> BuildGroupAsync(Group group)
{
var entry = new GroupEntry
{
ReferenceId = group.Id,
ExternalId = group.Id,
Name = group.DisplayName
};
var members = await _graphClient.Groups[group.Id].Members.Request().Select("id").GetAsync();
foreach(var member in members)
{
if(member is User)
{
entry.UserMemberExternalIds.Add(member.Id);
}
else if(member is Group)
{
entry.GroupMemberReferenceIds.Add(member.Id);
}
}
return entry;
}
private async static Task<List<UserEntry>> GetUsersAsync(bool force)
{
if(!SettingsService.Instance.Sync.SyncUsers)
{
throw new ApplicationException("Not configured to sync users.");
}
if(SettingsService.Instance.Server?.Azure == null)
{
throw new ApplicationException("No configuration for directory server.");
}
if(SettingsService.Instance.Sync == null)
{
throw new ApplicationException("No configuration for sync.");
}
if(!AuthService.Instance.Authenticated)
{
throw new ApplicationException("Not authenticated.");
}
var entries = new List<UserEntry>();
var filter = CreateSetFromFilter(SettingsService.Instance.Sync.UserFilter);
var userRequest = _graphClient.Users.Delta();
IUserDeltaCollectionPage users = null;
if(!force && SettingsService.Instance.UserDeltaToken != null)
{
try
{
var delataRequest = userRequest.Request();
delataRequest.QueryOptions.Add(new QueryOption("$deltatoken", SettingsService.Instance.UserDeltaToken));
users = await delataRequest.GetAsync();
}
catch
{
users = null;
}
}
if(users == null)
{
users = await userRequest.Request().GetAsync();
}
while(true)
{
foreach(var user in users)
{
var entry = new UserEntry
{
ReferenceId = user.Id,
ExternalId = user.Id,
Email = user.Mail ?? user.UserPrincipalName,
Disabled = !user.AccountEnabled.GetValueOrDefault(true)
};
if(FilterOutResult(filter, entry.Email))
{
continue;
}
if(user.AdditionalData.TryGetValue("@removed", out object deleted) && deleted.ToString().Contains("changed"))
{
entry.Deleted = true;
}
else if(!entry.Disabled && (entry?.Email?.Contains("#") ?? true))
{
continue;
}
entries.Add(entry);
}
if(users.NextPageRequest == null)
{
if(users.AdditionalData.TryGetValue("@odata.deltaLink", out object deltaLink))
{
var deltaUriQuery = new Uri(deltaLink.ToString()).ParseQueryString();
if(deltaUriQuery["$deltatoken"] != null)
{
SettingsService.Instance.UserDeltaToken = deltaUriQuery["$deltatoken"];
}
}
break;
}
else
{
users = await users.NextPageRequest.GetAsync();
}
}
return entries;
}
private static Tuple<bool, HashSet<string>> CreateSetFromFilter(string filter)
{
if(string.IsNullOrWhiteSpace(filter))
{
return null;
}
var parts = filter.Split(':');
if(parts.Length != 2)
{
return null;
}
var exclude = true;
if(string.Equals(parts[0].Trim(), "include", StringComparison.InvariantCultureIgnoreCase))
{
exclude = false;
}
else if(string.Equals(parts[0].Trim(), "exclude", StringComparison.InvariantCultureIgnoreCase))
{
exclude = true;
}
else
{
return null;
}
var list = new HashSet<string>(parts[1].Split(',').Select(p => p.Trim()));
return new Tuple<bool, HashSet<string>>(exclude, list);
}
private static bool FilterOutResult(Tuple<bool, HashSet<string>> filter, string result)
{
if(filter != null)
{
// excluded
if(filter.Item1 && filter.Item2.Contains(result, StringComparer.InvariantCultureIgnoreCase))
{
return true;
}
// included
else if(!filter.Item1 && !filter.Item2.Contains(result, StringComparer.InvariantCultureIgnoreCase))
{
return true;
}
}
return false;
}
}
}

View File

@@ -1,95 +0,0 @@
using Bit.Core.Utilities;
#if NET461
using System.ServiceProcess;
#endif
namespace Bit.Core.Services
{
public class ControllerService
{
private static ControllerService _instance;
private ControllerService()
{
#if NET461
Controller = new ServiceController(Constants.ProgramName);
#endif
}
public static ControllerService Instance
{
get
{
if(_instance == null)
{
_instance = new ControllerService();
}
return _instance;
}
}
#if NET461
public ServiceController Controller { get; private set; }
public ServiceControllerStatus Status
{
get
{
Controller.Refresh();
return Controller.Status;
}
}
public bool Running => Status == ServiceControllerStatus.Running;
public bool Paused => Status == ServiceControllerStatus.Paused;
public bool Stopped => Status == ServiceControllerStatus.Stopped;
public bool Pending =>
Status == ServiceControllerStatus.ContinuePending ||
Status == ServiceControllerStatus.PausePending ||
Status == ServiceControllerStatus.StartPending ||
Status == ServiceControllerStatus.StopPending;
#endif
public string StatusString
{
get
{
#if NET461
return Controller == null ? "Unavailable" : Status.ToString();
#else
return "Unavailable";
#endif
}
}
public bool Start()
{
#if NET461
if(Controller == null || !Stopped)
{
return false;
}
Controller.Start();
return true;
#else
throw new System.Exception("Controller unavailable.");
#endif
}
public bool Stop()
{
#if NET461
if(Controller == null || !Controller.CanStop)
{
return false;
}
Controller.Stop();
return true;
#else
throw new System.Exception("Controller unavailable.");
#endif
}
}
}

View File

@@ -1,306 +0,0 @@
using Bit.Core.Models;
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using Google.Apis.Admin.Directory.directory_v1;
using Google.Apis.Services;
using Google.Apis.Auth.OAuth2;
using System.IO;
using Bit.Core.Utilities;
using System.Linq;
using Google.Apis.Admin.Directory.directory_v1.Data;
using Google.Apis.Requests;
namespace Bit.Core.Services
{
public class GSuiteDirectoryService : IDirectoryService
{
private static GSuiteDirectoryService _instance;
private static DirectoryService _service;
private GSuiteDirectoryService()
{
ICredential creds;
var secretFilePath = Path.Combine(Constants.BaseStoragePath, SettingsService.Instance.Server.GSuite.SecretFile);
using(var stream = new FileStream(secretFilePath, FileMode.Open, FileAccess.Read))
{
var scopes = new List<string>
{
DirectoryService.Scope.AdminDirectoryUserReadonly,
DirectoryService.Scope.AdminDirectoryGroupReadonly,
DirectoryService.Scope.AdminDirectoryGroupMemberReadonly
};
creds = GoogleCredential.FromStream(stream)
.CreateScoped(scopes)
.CreateWithUser(SettingsService.Instance.Server.GSuite.AdminUser);
}
_service = new DirectoryService(new BaseClientService.Initializer
{
HttpClientInitializer = creds,
ApplicationName = Constants.ProgramName
});
}
public static IDirectoryService Instance
{
get
{
if(_instance == null)
{
_instance = new GSuiteDirectoryService();
}
return _instance;
}
}
public async Task<Tuple<List<GroupEntry>, List<UserEntry>>> GetEntriesAsync(bool force = false)
{
if(!AuthService.Instance.Authenticated || !AuthService.Instance.OrganizationSet)
{
throw new ApplicationException("Not logged in or have an org set.");
}
if(SettingsService.Instance.Server?.GSuite == null)
{
throw new ApplicationException("No configuration for directory server.");
}
if(SettingsService.Instance.Sync == null)
{
throw new ApplicationException("No configuration for sync.");
}
List<UserEntry> users = null;
if(SettingsService.Instance.Sync.SyncUsers)
{
users = await GetUsersAsync(force);
}
List<GroupEntry> groups = null;
if(SettingsService.Instance.Sync.SyncGroups)
{
groups = await GetGroupsAsync(force || (users?.Any(u => !u.Deleted && !u.Disabled) ?? false));
}
return new Tuple<List<GroupEntry>, List<UserEntry>>(groups, users);
}
private Task<List<GroupEntry>> GetGroupsAsync(bool force)
{
var entries = new List<GroupEntry>();
var request = _service.Groups.List();
request.Domain = SettingsService.Instance.Server.GSuite.Domain;
request.Customer = SettingsService.Instance.Server.GSuite.Customer;
var pageStreamer = new PageStreamer<Group, GroupsResource.ListRequest, Groups, string>(
(req, token) => req.PageToken = token,
res => res.NextPageToken,
res => res.GroupsValue);
var filter = CreateSetFromFilter(SettingsService.Instance.Sync.GroupFilter);
foreach(var group in pageStreamer.Fetch(request))
{
if(FilterOutResult(filter, group.Name))
{
continue;
}
var entry = BuildGroup(group);
entries.Add(entry);
}
return Task.FromResult(entries);
}
private static GroupEntry BuildGroup(Group group)
{
var entry = new GroupEntry
{
ReferenceId = group.Id,
ExternalId = group.Id,
Name = group.Name
};
var memberRequest = _service.Members.List(group.Id);
var pageStreamer = new PageStreamer<Member, MembersResource.ListRequest, Members, string>(
(req, token) => req.PageToken = token,
res => res.NextPageToken,
res => res.MembersValue);
foreach(var member in pageStreamer.Fetch(memberRequest))
{
if(!member.Role.Equals("member", StringComparison.InvariantCultureIgnoreCase) ||
!member.Status.Equals("active", StringComparison.InvariantCultureIgnoreCase))
{
continue;
}
if(member.Type.Equals("user", StringComparison.InvariantCultureIgnoreCase))
{
entry.UserMemberExternalIds.Add(member.Id);
}
else if(member.Type.Equals("group", StringComparison.InvariantCultureIgnoreCase))
{
entry.GroupMemberReferenceIds.Add(member.Id);
}
}
return entry;
}
private Task<List<UserEntry>> GetUsersAsync(bool force)
{
var entries = new List<UserEntry>();
var query = CreateGSuiteQueryFromFilter(SettingsService.Instance.Sync.UserFilter);
var request = _service.Users.List();
request.Domain = SettingsService.Instance.Server.GSuite.Domain;
request.Customer = SettingsService.Instance.Server.GSuite.Customer;
request.Query = query;
var pageStreamer = new PageStreamer<User, UsersResource.ListRequest, Users, string>(
(req, token) => req.PageToken = token,
res => res.NextPageToken,
res => res.UsersValue);
var filter = CreateSetFromFilter(SettingsService.Instance.Sync.UserFilter);
foreach(var user in pageStreamer.Fetch(request))
{
if(FilterOutResult(filter, user.PrimaryEmail))
{
continue;
}
var entry = BuildUser(user, false);
if(entry != null)
{
entries.Add(entry);
}
}
var deletedRequest = _service.Users.List();
deletedRequest.Domain = SettingsService.Instance.Server.GSuite.Domain;
deletedRequest.Customer = SettingsService.Instance.Server.GSuite.Customer;
deletedRequest.Query = query;
deletedRequest.ShowDeleted = "true";
var deletedPageStreamer = new PageStreamer<User, UsersResource.ListRequest, Users, string>(
(req, token) => req.PageToken = token,
res => res.NextPageToken,
res => res.UsersValue);
foreach(var user in deletedPageStreamer.Fetch(deletedRequest))
{
if(FilterOutResult(filter, user.PrimaryEmail))
{
continue;
}
var entry = BuildUser(user, true);
if(entry != null)
{
entries.Add(entry);
}
}
return Task.FromResult(entries);
}
private UserEntry BuildUser(User user, bool deleted)
{
var entry = new UserEntry
{
ReferenceId = user.Id,
ExternalId = user.Id,
Email = user.PrimaryEmail,
Disabled = user.Suspended.GetValueOrDefault(false),
Deleted = deleted,
CreationDate = user.CreationTime
};
if(string.IsNullOrWhiteSpace(entry.Email) && !entry.Deleted)
{
return null;
}
return entry;
}
private string CreateGSuiteQueryFromFilter(string filter)
{
if(string.IsNullOrWhiteSpace(filter))
{
return null;
}
var mainParts = filter.Split('|');
if(mainParts.Count() < 2 || string.IsNullOrWhiteSpace(mainParts[1]))
{
return null;
}
return mainParts[1].Trim();
}
private Tuple<bool, HashSet<string>> CreateSetFromFilter(string filter)
{
if(string.IsNullOrWhiteSpace(filter))
{
return null;
}
var mainParts = filter.Split('|');
if(mainParts.Count() < 1 || string.IsNullOrWhiteSpace(mainParts[0]))
{
return null;
}
var parts = mainParts[0].Split(':');
if(parts.Count() != 2)
{
return null;
}
var exclude = true;
if(string.Equals(parts[0].Trim(), "include", StringComparison.InvariantCultureIgnoreCase))
{
exclude = false;
}
else if(string.Equals(parts[0].Trim(), "exclude", StringComparison.InvariantCultureIgnoreCase))
{
exclude = true;
}
else
{
return null;
}
var list = new HashSet<string>(parts[1].Split(',').Select(p => p.Trim()));
return new Tuple<bool, HashSet<string>>(exclude, list);
}
private bool FilterOutResult(Tuple<bool, HashSet<string>> filter, string result)
{
if(filter != null)
{
// excluded
if(filter.Item1 && filter.Item2.Contains(result, StringComparer.InvariantCultureIgnoreCase))
{
return true;
}
// included
else if(!filter.Item1 && !filter.Item2.Contains(result, StringComparer.InvariantCultureIgnoreCase))
{
return true;
}
}
return false;
}
}
}

View File

@@ -1,12 +0,0 @@
using Bit.Core.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Bit.Core.Services
{
public interface IDirectoryService
{
Task<Tuple<List<GroupEntry>, List<UserEntry>>> GetEntriesAsync(bool force = false);
}
}

View File

@@ -1,402 +0,0 @@
#if NET461
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Utilities;
using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
namespace Bit.Core.Services
{
public class LdapDirectoryService : IDirectoryService
{
private static LdapDirectoryService _instance;
private LdapDirectoryService() { }
public static IDirectoryService Instance
{
get
{
if(_instance == null)
{
_instance = new LdapDirectoryService();
}
return _instance;
}
}
public async Task<Tuple<List<GroupEntry>, List<UserEntry>>> GetEntriesAsync(bool force = false)
{
if(!AuthService.Instance.Authenticated || !AuthService.Instance.OrganizationSet)
{
throw new ApplicationException("Not logged in or have an org set.");
}
if(SettingsService.Instance.Server?.Ldap == null)
{
throw new ApplicationException("No configuration for directory server.");
}
if(SettingsService.Instance.Sync == null)
{
throw new ApplicationException("No configuration for sync.");
}
List<UserEntry> users = null;
if(SettingsService.Instance.Sync.SyncUsers)
{
users = await GetUsersAsync(force);
}
List<GroupEntry> groups = null;
if(SettingsService.Instance.Sync.SyncGroups)
{
groups = await GetGroupsAsync(force || (users?.Any(u => !u.Deleted && !u.Disabled) ?? false));
}
return new Tuple<List<GroupEntry>, List<UserEntry>>(groups, users);
}
private static Task<List<GroupEntry>> GetGroupsAsync(bool force = false)
{
if(!SettingsService.Instance.Sync.SyncGroups)
{
throw new ApplicationException("Not configured to sync groups.");
}
if(SettingsService.Instance.Server?.Ldap == null)
{
throw new ApplicationException("No configuration for directory server.");
}
if(SettingsService.Instance.Sync == null)
{
throw new ApplicationException("No configuration for sync.");
}
if(!AuthService.Instance.Authenticated)
{
throw new ApplicationException("Not authenticated.");
}
var groupEntry = SettingsService.Instance.Server.Ldap.GetGroupDirectoryEntry();
var originalFilter = BuildBaseFilter(SettingsService.Instance.Sync.Ldap.GroupObjectClass,
SettingsService.Instance.Sync.GroupFilter);
var filter = originalFilter;
var revisionFilter = BuildRevisionFilter(filter, force, SettingsService.Instance.LastGroupSyncDate);
var searchSinceRevision = filter != revisionFilter;
filter = revisionFilter;
Console.WriteLine("Group search: {0} => {1}", groupEntry.Path, filter);
var searcher = new DirectorySearcher(groupEntry, filter);
var result = searcher.FindAll();
var initialSearchGroupIds = new List<string>();
foreach(SearchResult item in result)
{
initialSearchGroupIds.Add(DNFromPath(item.Path));
}
if(searchSinceRevision && !initialSearchGroupIds.Any())
{
return Task.FromResult(new List<GroupEntry>());
}
else if(searchSinceRevision)
{
searcher = new DirectorySearcher(groupEntry, originalFilter);
result = searcher.FindAll();
}
var userEntry = SettingsService.Instance.Server.Ldap.GetUserDirectoryEntry();
var userFilter = BuildBaseFilter(SettingsService.Instance.Sync.Ldap.UserObjectClass,
SettingsService.Instance.Sync.UserFilter);
var userSearcher = new DirectorySearcher(userEntry, userFilter);
var userResult = userSearcher.FindAll();
var userIdsDict = MakeIdIndex(userResult);
var groups = new List<GroupEntry>();
foreach(SearchResult item in result)
{
var group = BuildGroup(item, userIdsDict);
if(group == null)
{
continue;
}
groups.Add(group);
}
return Task.FromResult(groups);
}
private static Dictionary<string, string> MakeIdIndex(SearchResultCollection result)
{
var dict = new Dictionary<string, string>();
foreach(SearchResult item in result)
{
var referenceId = DNFromPath(item.Path);
var externalId = referenceId;
if(item.Properties.Contains("objectGUID") && item.Properties["objectGUID"].Count > 0)
{
externalId = item.Properties["objectGUID"][0].FromGuidToString();
}
dict.Add(referenceId, externalId);
}
return dict;
}
private static GroupEntry BuildGroup(SearchResult item, Dictionary<string, string> userIndex)
{
var group = new GroupEntry
{
ReferenceId = DNFromPath(item.Path)
};
if(group.ReferenceId == null)
{
return null;
}
// External Id
if(item.Properties.Contains("objectGUID") && item.Properties["objectGUID"].Count > 0)
{
group.ExternalId = item.Properties["objectGUID"][0].FromGuidToString();
}
else
{
group.ExternalId = group.ReferenceId;
}
// Name
if(item.Properties.Contains(SettingsService.Instance.Sync.Ldap.GroupNameAttribute) &&
item.Properties[SettingsService.Instance.Sync.Ldap.GroupNameAttribute].Count > 0)
{
group.Name = item.Properties[SettingsService.Instance.Sync.Ldap.GroupNameAttribute][0].ToString();
}
else if(item.Properties.Contains("cn") && item.Properties["cn"].Count > 0)
{
group.Name = item.Properties["cn"][0].ToString();
}
else
{
return null;
}
// Dates
group.CreationDate = item.Properties.ParseDateTime(SettingsService.Instance.Sync.Ldap.CreationDateAttribute);
group.RevisionDate = item.Properties.ParseDateTime(SettingsService.Instance.Sync.Ldap.RevisionDateAttribute);
// Members
if(item.Properties.Contains(SettingsService.Instance.Sync.Ldap.MemberAttribute) &&
item.Properties[SettingsService.Instance.Sync.Ldap.MemberAttribute].Count > 0)
{
foreach(var member in item.Properties[SettingsService.Instance.Sync.Ldap.MemberAttribute])
{
var memberDn = member.ToString();
if(userIndex.ContainsKey(memberDn) && !group.UserMemberExternalIds.Contains(userIndex[memberDn]))
{
group.UserMemberExternalIds.Add(userIndex[memberDn]);
}
else if(!group.GroupMemberReferenceIds.Contains(memberDn))
{
group.GroupMemberReferenceIds.Add(memberDn);
}
}
}
return group;
}
private static Task<List<UserEntry>> GetUsersAsync(bool force = false)
{
if(!SettingsService.Instance.Sync.SyncUsers)
{
throw new ApplicationException("Not configured to sync users.");
}
if(SettingsService.Instance.Server?.Ldap == null)
{
throw new ApplicationException("No configuration for directory server.");
}
if(SettingsService.Instance.Sync == null)
{
throw new ApplicationException("No configuration for sync.");
}
if(!AuthService.Instance.Authenticated)
{
throw new ApplicationException("Not authenticated.");
}
var userEntry = SettingsService.Instance.Server.Ldap.GetUserDirectoryEntry();
var filter = BuildBaseFilter(SettingsService.Instance.Sync.Ldap.UserObjectClass,
SettingsService.Instance.Sync.UserFilter);
filter = BuildRevisionFilter(filter, force, SettingsService.Instance.LastUserSyncDate);
Console.WriteLine("User search: {0} => {1}", userEntry.Path, filter);
var searcher = new DirectorySearcher(userEntry, filter);
var result = searcher.FindAll();
var users = new List<UserEntry>();
foreach(SearchResult item in result)
{
var user = BuildUser(item, false);
if(user == null)
{
continue;
}
users.Add(user);
}
// Deleted users
if(SettingsService.Instance.Server.Type == DirectoryType.ActiveDirectory)
{
var deletedEntry = SettingsService.Instance.Server.Ldap.GetBasePathDirectoryEntry();
var deletedFilter = BuildBaseFilter(SettingsService.Instance.Sync.Ldap.UserObjectClass, "(isDeleted=TRUE)");
deletedFilter = BuildRevisionFilter(deletedFilter, force, SettingsService.Instance.LastUserSyncDate);
var deletedSearcher = new DirectorySearcher(deletedEntry, deletedFilter);
deletedSearcher.Tombstone = true;
var deletedResult = deletedSearcher.FindAll();
foreach(SearchResult item in deletedResult)
{
var user = BuildUser(item, true);
if(user == null)
{
continue;
}
users.Add(user);
}
}
return Task.FromResult(users);
}
private static string BuildBaseFilter(string objectClass, string subFilter)
{
var filter = BuildObjectClassFilter(objectClass);
if(!string.IsNullOrWhiteSpace(subFilter))
{
filter = string.Format("(&{0}{1})", filter, subFilter);
}
return filter;
}
private static string BuildObjectClassFilter(string objectClass)
{
return string.Format("(&(objectClass={0}))", objectClass);
}
private static string BuildRevisionFilter(string baseFilter, bool force, DateTime? lastRevisionDate)
{
if(!force && lastRevisionDate.HasValue &&
!string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.Ldap.RevisionDateAttribute))
{
baseFilter = string.Format("(&{0}({1}>={2}))",
baseFilter,
SettingsService.Instance.Sync.Ldap.RevisionDateAttribute,
lastRevisionDate.Value.ToGeneralizedTimeUTC());
}
return baseFilter;
}
private static UserEntry BuildUser(SearchResult item, bool deleted)
{
var user = new UserEntry
{
ReferenceId = DNFromPath(item.Path),
Deleted = deleted
};
if(user.ReferenceId == null)
{
return null;
}
// External Id
if(item.Properties.Contains("objectGUID") && item.Properties["objectGUID"].Count > 0)
{
user.ExternalId = item.Properties["objectGUID"][0].FromGuidToString();
}
else
{
user.ExternalId = user.ReferenceId;
}
user.Disabled = EntryDisabled(item);
// Email
if(item.Properties.Contains(SettingsService.Instance.Sync.Ldap.UserEmailAttribute) &&
item.Properties[SettingsService.Instance.Sync.Ldap.UserEmailAttribute].Count > 0)
{
user.Email = item.Properties[SettingsService.Instance.Sync.Ldap.UserEmailAttribute][0]
.ToString()
.ToLowerInvariant();
}
if(string.IsNullOrWhiteSpace(user.Email) && SettingsService.Instance.Sync.Ldap.EmailPrefixSuffix &&
item.Properties.Contains(SettingsService.Instance.Sync.Ldap.UserEmailPrefixAttribute) &&
item.Properties[SettingsService.Instance.Sync.Ldap.UserEmailPrefixAttribute].Count > 0 &&
!string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.Ldap.UserEmailSuffix))
{
user.Email = string.Concat(
item.Properties[SettingsService.Instance.Sync.Ldap.UserEmailPrefixAttribute][0].ToString(),
SettingsService.Instance.Sync.Ldap.UserEmailSuffix).ToLowerInvariant();
}
if(string.IsNullOrWhiteSpace(user.Email) && !user.Deleted)
{
return null;
}
// Dates
user.CreationDate = item.Properties.ParseDateTime(SettingsService.Instance.Sync.Ldap.CreationDateAttribute);
user.RevisionDate = item.Properties.ParseDateTime(SettingsService.Instance.Sync.Ldap.RevisionDateAttribute);
return user;
}
private static bool EntryDisabled(SearchResult item)
{
if(!item.Properties.Contains("userAccountControl") || item.Properties["userAccountControl"].Count == 0)
{
return false;
}
UserAccountControl control;
if(!Enum.TryParse(item.Properties["userAccountControl"][0].ToString(), out control))
{
return false;
}
return (control & UserAccountControl.AccountDisabled) == UserAccountControl.AccountDisabled;
}
private static string DNFromPath(string path)
{
var dn = new Uri(path).Segments?.LastOrDefault();
if(dn == null)
{
return null;
}
return WebUtility.UrlDecode(dn);
}
}
}
#endif

View File

@@ -1,266 +0,0 @@
using Bit.Core.Models;
using Bit.Core.Utilities;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Text;
namespace Bit.Core.Services
{
public class SettingsService
{
private static SettingsService _instance;
private static object _locker = new object();
private SettingsModel _settings;
private SettingsService() { }
public static SettingsService Instance
{
get
{
if(_instance == null)
{
_instance = new SettingsService();
}
return _instance;
}
}
private SettingsModel Settings
{
get
{
var filePath = $"{Constants.BaseStoragePath}\\settings.json";
if(_settings == null && File.Exists(filePath))
{
var serializer = new JsonSerializer();
using(var s = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
using(var sr = new StreamReader(s, Encoding.UTF8))
using(var jsonTextReader = new JsonTextReader(sr))
{
_settings = serializer.Deserialize<SettingsModel>(jsonTextReader);
}
return _settings;
}
InitSettings();
return _settings;
}
}
private void SaveSettings()
{
lock(_locker)
{
if(!Directory.Exists(Constants.BaseStoragePath))
{
Directory.CreateDirectory(Constants.BaseStoragePath);
}
var filePath = $"{Constants.BaseStoragePath}\\settings.json";
using(var s = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read))
using(var sw = new StreamWriter(s, Encoding.UTF8))
{
var json = JsonConvert.SerializeObject(_settings, Formatting.Indented);
sw.Write(json);
}
}
}
private void InitSettings()
{
if(_settings == null)
{
_settings = new SettingsModel();
}
}
public string ApiBaseUrl
{
get
{
return Settings.ApiBaseUrl;
}
set
{
InitSettings();
_settings.ApiBaseUrl = value;
SaveSettings();
}
}
public string IdentityBaseUrl
{
get
{
return Settings.IdentityBaseUrl;
}
set
{
InitSettings();
_settings.IdentityBaseUrl = value;
SaveSettings();
}
}
public EncryptedData AccessToken
{
get
{
return Settings.AccessToken;
}
set
{
InitSettings();
_settings.AccessToken = value;
SaveSettings();
}
}
public EncryptedData RefreshToken
{
get
{
return Settings.RefreshToken;
}
set
{
InitSettings();
_settings.RefreshToken = value;
SaveSettings();
}
}
public Organization Organization
{
get
{
return Settings.Organization;
}
set
{
InitSettings();
_settings.Organization = value;
SaveSettings();
}
}
public ServerConfiguration Server
{
get
{
return Settings.Server;
}
set
{
InitSettings();
_settings.Server = value;
SaveSettings();
}
}
public SyncConfiguration Sync
{
get
{
return Settings.Sync;
}
set
{
InitSettings();
_settings.Sync = value;
SaveSettings();
}
}
public DateTime? LastGroupSyncDate
{
get
{
return Settings.LastGroupSyncDate;
}
set
{
InitSettings();
_settings.LastGroupSyncDate = value;
SaveSettings();
}
}
public DateTime? LastUserSyncDate
{
get
{
return Settings.LastUserSyncDate;
}
set
{
InitSettings();
_settings.LastUserSyncDate = value;
SaveSettings();
}
}
public string GroupDeltaToken
{
get
{
return Settings.GroupDeltaToken;
}
set
{
InitSettings();
_settings.GroupDeltaToken = value;
SaveSettings();
}
}
public string UserDeltaToken
{
get
{
return Settings.UserDeltaToken;
}
set
{
InitSettings();
_settings.UserDeltaToken = value;
SaveSettings();
}
}
public string LastSyncHash
{
get
{
return Settings.LastSyncHash;
}
set
{
InitSettings();
_settings.LastSyncHash = value;
SaveSettings();
}
}
public class SettingsModel
{
public string ApiBaseUrl { get; set; } = "https://api.bitwarden.com";
public string IdentityBaseUrl { get; set; } = "https://identity.bitwarden.com";
public EncryptedData AccessToken { get; set; }
public EncryptedData RefreshToken { get; set; }
public ServerConfiguration Server { get; set; } = new ServerConfiguration();
public SyncConfiguration Sync { get; set; } = new SyncConfiguration(Enums.DirectoryType.ActiveDirectory);
public Organization Organization { get; set; } = new Organization();
public DateTime? LastGroupSyncDate { get; set; }
public DateTime? LastUserSyncDate { get; set; }
public string GroupDeltaToken { get; set; }
public string UserDeltaToken { get; set; }
public string LastSyncHash { get; set; }
}
}
}

View File

@@ -1,167 +0,0 @@
using Bit.Core.Models;
using Newtonsoft.Json.Linq;
using System;
using System.Text;
namespace Bit.Core.Services
{
public class TokenService
{
private static TokenService _instance;
private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private string _accessToken;
private dynamic _decodedAccessToken;
private TokenService() { }
public static TokenService Instance
{
get
{
if(_instance == null)
{
_instance = new TokenService();
}
return _instance;
}
}
public string AccessToken
{
get
{
if(_accessToken != null)
{
return _accessToken;
}
var encBytes = SettingsService.Instance.AccessToken;
if(encBytes?.Value != null)
{
_accessToken = Encoding.ASCII.GetString(encBytes.Decrypt());
}
return _accessToken;
}
set
{
_accessToken = value;
if(_accessToken == null)
{
SettingsService.Instance.AccessToken = null;
}
else
{
var bytes = Encoding.ASCII.GetBytes(_accessToken);
SettingsService.Instance.AccessToken = new EncryptedData(bytes);
bytes = null;
}
}
}
public DateTime AccessTokenExpiration
{
get
{
var decoded = DecodeAccessToken();
if(decoded?["exp"] == null)
{
throw new InvalidOperationException("No exp in token.");
}
return _epoc.AddSeconds(Convert.ToDouble(decoded["exp"].Value<long>()));
}
}
public bool AccessTokenExpired => DateTime.UtcNow < AccessTokenExpiration;
public TimeSpan AccessTokenTimeRemaining => AccessTokenExpiration - DateTime.UtcNow;
public bool AccessTokenNeedsRefresh => AccessTokenTimeRemaining.TotalMinutes < 5;
public string AccessTokenUserId => DecodeAccessToken()?["sub"].Value<string>();
public string AccessTokenEmail => DecodeAccessToken()?["email"].Value<string>();
public string AccessTokenName => DecodeAccessToken()?["name"].Value<string>();
public string RefreshToken
{
get
{
var encData = SettingsService.Instance.RefreshToken;
if(encData != null)
{
return Encoding.ASCII.GetString(encData.Decrypt());
}
return null;
}
set
{
if(value == null)
{
SettingsService.Instance.RefreshToken = null;
}
else
{
var bytes = Encoding.ASCII.GetBytes(value);
SettingsService.Instance.RefreshToken = new EncryptedData(bytes);
bytes = null;
}
}
}
public JObject DecodeAccessToken()
{
if(_decodedAccessToken != null)
{
return _decodedAccessToken;
}
if(AccessToken == null)
{
throw new InvalidOperationException($"{nameof(AccessToken)} not found.");
}
var parts = AccessToken.Split('.');
if(parts.Length != 3)
{
throw new InvalidOperationException($"{nameof(AccessToken)} must have 3 parts");
}
var decodedBytes = Base64UrlDecode(parts[1]);
if(decodedBytes == null || decodedBytes.Length < 1)
{
throw new InvalidOperationException($"{nameof(AccessToken)} must have 3 parts");
}
_decodedAccessToken = JObject.Parse(Encoding.UTF8.GetString(decodedBytes, 0, decodedBytes.Length));
return _decodedAccessToken;
}
private static byte[] Base64UrlDecode(string input)
{
var output = input;
// 62nd char of encoding
output = output.Replace('-', '+');
// 63rd char of encoding
output = output.Replace('_', '/');
// Pad with trailing '='s
switch(output.Length % 4)
{
case 0:
// No pad chars in this case
break;
case 2:
// Two pad chars
output += "=="; break;
case 3:
// One pad char
output += "="; break;
default:
throw new InvalidOperationException("Illegal base64url string!");
}
// Standard base64 decoder
return Convert.FromBase64String(output);
}
}
}

View File

@@ -1,27 +0,0 @@
using Bit.Core.Services;
using Microsoft.Graph;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace Bit.Core.Utilities
{
public class AzureAuthenticationProvider : IAuthenticationProvider
{
public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
if(SettingsService.Instance.Server?.Azure == null)
{
throw new ApplicationException("No server configuration.");
}
var authContext = new AuthenticationContext(
$"https://login.windows.net/{SettingsService.Instance.Server.Azure.Tenant}/oauth2/token");
var secret = SettingsService.Instance.Server.Azure.Secret.DecryptToString();
var creds = new ClientCredential(SettingsService.Instance.Server.Azure.Id, secret);
var authResult = await authContext.AcquireTokenAsync("https://graph.microsoft.com/", creds);
request.Headers.Add("Authorization", $"Bearer {authResult.AccessToken}");
}
}
}

View File

@@ -1,17 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bit.Core.Utilities
{
public static class Constants
{
public static string BaseStoragePath = string.Concat(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"\\Bitwarden\\Directory Connector");
public const string ProgramName = "Bitwarden Directory Connector";
}
}

View File

@@ -1,80 +0,0 @@
using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using System;
using System.Text;
namespace Bit.Core.Utilities
{
public static class Crypto
{
public static byte[] MakeKeyFromPassword(string password, string salt)
{
if(password == null)
{
throw new ArgumentNullException(nameof(password));
}
if(salt == null)
{
throw new ArgumentNullException(nameof(salt));
}
var passwordBytes = Encoding.UTF8.GetBytes(password);
var saltBytes = Encoding.UTF8.GetBytes(salt);
var keyBytes = DeriveKey(passwordBytes, saltBytes, 5000);
password = null;
passwordBytes = null;
return keyBytes;
}
public static string MakeKeyFromPasswordBase64(string password, string salt)
{
var key = MakeKeyFromPassword(password, salt);
password = null;
return Convert.ToBase64String(key);
}
public static byte[] HashPassword(byte[] key, string password)
{
if(key == null)
{
throw new ArgumentNullException(nameof(key));
}
if(password == null)
{
throw new ArgumentNullException(nameof(password));
}
var passwordBytes = Encoding.UTF8.GetBytes(password);
var hashBytes = DeriveKey(key, passwordBytes, 1);
password = null;
key = null;
return hashBytes;
}
public static string HashPasswordBase64(byte[] key, string password)
{
var hash = HashPassword(key, password);
password = null;
key = null;
return Convert.ToBase64String(hash);
}
private static byte[] DeriveKey(byte[] password, byte[] salt, int rounds)
{
var generator = new Pkcs5S2ParametersGenerator(new Sha256Digest());
generator.Init(password, salt, rounds);
var key = ((KeyParameter)generator.GenerateDerivedMacParameters(256)).GetKey();
password = null;
return key;
}
}
}

View File

@@ -1,69 +0,0 @@
using System;
using System.Collections.Specialized;
#if NET461
using System.DirectoryServices;
#endif
using System.Globalization;
namespace Bit.Core.Utilities
{
public static class Extensions
{
private const string GeneralizedTimeFormat = "yyyyMMddHHmmss.f'Z'";
public static DateTime ToDateTime(this string generalizedTimeString)
{
return DateTime.ParseExact(generalizedTimeString, GeneralizedTimeFormat, CultureInfo.InvariantCulture);
}
public static string ToGeneralizedTimeUTC(this DateTime date)
{
return date.ToString("yyyyMMddHHmmss.f'Z'");
}
#if NET461
public static DateTime? ParseDateTime(this ResultPropertyCollection collection, string dateKey)
{
DateTime date;
if(collection.Contains(dateKey) && collection[dateKey].Count > 0 &&
DateTime.TryParse(collection[dateKey][0].ToString(), out date))
{
return date;
}
return null;
}
#endif
public static NameValueCollection ParseQueryString(this Uri uri)
{
var queryParameters = new NameValueCollection();
var querySegments = uri.Query.Split('&');
foreach(var segment in querySegments)
{
var parts = segment.Split('=');
if(parts.Length > 0)
{
var key = parts[0].Trim(new char[] { '?', ' ' });
var val = parts[1].Trim();
queryParameters.Add(key, val);
}
}
return queryParameters;
}
public static string FromGuidToString(this object property)
{
var propBytes = property as byte[];
if(propBytes != null)
{
return new Guid(propBytes).ToString();
}
else
{
return property.ToString();
}
}
}
}

View File

@@ -1,18 +0,0 @@
using System.Security.Principal;
namespace Bit.Core.Utilities
{
public static class Helpers
{
public static bool IsAdministrator()
{
#if NET461
var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
#else
return false;
#endif
}
}
}

View File

@@ -1,176 +0,0 @@
using Bit.Core.Models;
using Bit.Core.Services;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace Bit.Core.Utilities
{
public static class Sync
{
public static async Task<SyncResult> SyncAllAsync(bool force = false, bool sendToServer = true)
{
var startingGroupDelta = SettingsService.Instance.GroupDeltaToken;
var startingUserDelta = SettingsService.Instance.UserDeltaToken;
try
{
var now = DateTime.UtcNow;
var entriesResult = await GetDirectoryService().GetEntriesAsync(force);
var groups = entriesResult.Item1;
var users = entriesResult.Item2;
if(groups?.Any() ?? false)
{
FlattenUsersToGroups(groups, null, groups);
}
if(!sendToServer)
{
RestoreDeltas(startingGroupDelta, startingUserDelta);
}
if(!sendToServer || ((groups?.Count ?? 0) == 0 && (users?.Count ?? 0) == 0))
{
return new SyncResult
{
Success = true,
Groups = groups,
Users = users
};
}
var request = new ImportRequest(groups, users);
var json = JsonConvert.SerializeObject(request);
var hash = ComputeHash(string.Concat(SettingsService.Instance.ApiBaseUrl, json));
if(hash == SettingsService.Instance.LastSyncHash)
{
return new SyncResult
{
Success = true,
Groups = groups,
Users = users
};
}
var response = await ApiService.Instance.PostImportAsync(request);
if(response.Succeeded)
{
SettingsService.Instance.LastSyncHash = hash;
if(SettingsService.Instance.Sync.SyncGroups)
{
SettingsService.Instance.LastGroupSyncDate = now;
}
if(SettingsService.Instance.Sync.SyncUsers)
{
SettingsService.Instance.LastUserSyncDate = now;
}
return new SyncResult
{
Success = true,
Groups = groups,
Users = users
};
}
else
{
RestoreDeltas(startingGroupDelta, startingUserDelta);
return new SyncResult
{
Success = false,
ErrorMessage = response.Errors.FirstOrDefault()?.Message
};
}
}
catch(Exception e)
{
RestoreDeltas(startingGroupDelta, startingUserDelta);
return new SyncResult
{
Success = false,
ErrorMessage = e.Message
};
}
}
private static IDirectoryService GetDirectoryService()
{
switch(SettingsService.Instance.Server.Type)
{
case Enums.DirectoryType.AzureActiveDirectory:
return AzureDirectoryService.Instance;
case Enums.DirectoryType.GSuite:
return GSuiteDirectoryService.Instance;
default:
#if NET461
return LdapDirectoryService.Instance;
#else
throw new Exception("LdapDirectoryService not supported.");
#endif
}
}
private static void FlattenUsersToGroups(List<GroupEntry> currentGroups, List<string> currentGroupsUsers,
List<GroupEntry> allGroups)
{
foreach(var group in currentGroups)
{
var groupsInThisGroup = allGroups.Where(g => group.GroupMemberReferenceIds.Contains(g.ReferenceId)).ToList();
var usersInThisGroup = group.UserMemberExternalIds.ToList();
if(currentGroupsUsers != null)
{
foreach(var id in currentGroupsUsers)
{
if(!group.UserMemberExternalIds.Contains(id))
{
group.UserMemberExternalIds.Add(id);
}
}
usersInThisGroup.AddRange(currentGroupsUsers);
}
// Recurse it
FlattenUsersToGroups(groupsInThisGroup, usersInThisGroup, allGroups);
}
}
private static void RestoreDeltas(string groupDelta, string userDelta)
{
if(SettingsService.Instance.Server.Type != Enums.DirectoryType.AzureActiveDirectory)
{
return;
}
SettingsService.Instance.GroupDeltaToken = groupDelta;
SettingsService.Instance.UserDeltaToken = userDelta;
}
private static string ComputeHash(string value)
{
if(value == null)
{
return null;
}
string result = null;
using(var hash = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(value);
var hashBytes = hash.ComputeHash(bytes);
result = Convert.ToBase64String(hashBytes);
}
return result;
}
}
}

View File

@@ -1,78 +0,0 @@
using System;
using System.ComponentModel;
using System.ServiceProcess;
using System.Configuration.Install;
using System.Diagnostics;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
using Bit.Core.Utilities;
namespace Service
{
[RunInstaller(true)]
[DesignerCategory("Code")]
public class Installer : System.Configuration.Install.Installer
{
private IContainer _components = null;
private ServiceProcessInstaller _serviceProcessInstaller;
private ServiceInstaller _serviceInstaller;
public Installer()
{
Init();
}
private void Init()
{
_components = new Container();
_serviceProcessInstaller = new ServiceProcessInstaller();
_serviceInstaller = new ServiceInstaller();
_serviceProcessInstaller.Account = ServiceAccount.LocalSystem;
_serviceProcessInstaller.AfterInstall += new InstallEventHandler(AfterInstalled);
_serviceProcessInstaller.BeforeInstall += new InstallEventHandler(BeforeInstalled);
_serviceInstaller.ServiceName = Constants.ProgramName;
_serviceInstaller.Description = "Sync directory groups and users to your Bitwarden organization.";
Installers.AddRange(new System.Configuration.Install.Installer[] { _serviceProcessInstaller, _serviceInstaller });
}
private void AfterInstalled(object sender, InstallEventArgs e)
{
var info = new DirectoryInfo(Constants.BaseStoragePath);
if(!info.Exists)
{
info.Create();
}
var sec = info.GetAccessControl();
AddPermission(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), sec);
AddPermission(new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), sec);
AddPermission(new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null), sec);
AddPermission(new SecurityIdentifier(WellKnownSidType.CreatorOwnerSid, null), sec);
AddPermission(WindowsIdentity.GetCurrent().User, sec);
sec.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);
info.SetAccessControl(sec);
}
private void AddPermission(IdentityReference sid, DirectorySecurity sec)
{
var rule = new FileSystemAccessRule(
sid,
FileSystemRights.FullControl | FileSystemRights.Write | FileSystemRights.Read,
InheritanceFlags.None,
PropagationFlags.NoPropagateInherit,
AccessControlType.Allow);
sec.AddAccessRule(rule);
}
private void BeforeInstalled(object sender, InstallEventArgs e)
{
if(EventLog.SourceExists(_serviceInstaller.ServiceName))
{
EventLog.DeleteEventSource(_serviceInstaller.ServiceName);
}
}
}
}

View File

@@ -1,29 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading.Tasks;
namespace Service
{
static class Program
{
static void Main()
{
//DebugMode();
ServiceBase.Run(new ServiceBase[]
{
new Service()
});
}
[Conditional("DEBUG")]
private static void DebugMode()
{
Debugger.Launch();
}
}
}

View File

@@ -1,119 +0,0 @@
using Bit.Core.Services;
using Bit.Core.Utilities;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading;
namespace Service
{
[DesignerCategory("Code")]
public class Service : ServiceBase
{
private IContainer _components;
private EventLog _eventLog;
private Timer _timer;
public Service()
{
ServiceName = Constants.ProgramName;
_components = new Container();
_eventLog = new EventLog();
_eventLog.Source = ServiceName;
_eventLog.Log = "Application";
if(!EventLog.SourceExists(_eventLog.Source))
{
EventLog.CreateEventSource(_eventLog.Source, _eventLog.Log);
}
}
protected override void Dispose(bool disposing)
{
if(disposing)
{
_eventLog?.Dispose();
_eventLog = null;
_components?.Dispose();
_components = null;
_timer?.Dispose();
_timer = null;
}
base.Dispose(disposing);
}
protected override void OnStart(string[] args)
{
_eventLog.WriteEntry("Service started!", EventLogEntryType.Information);
if(SettingsService.Instance.Server == null)
{
_eventLog.WriteEntry("Server not configured.", EventLogEntryType.Error);
return;
}
if(SettingsService.Instance.Sync == null)
{
_eventLog.WriteEntry("Sync not configured.", EventLogEntryType.Error);
return;
}
if(!AuthService.Instance.Authenticated || !AuthService.Instance.OrganizationSet)
{
_eventLog.WriteEntry("Not authenticated with proper organization set.", EventLogEntryType.Error);
return;
}
var intervalMinutes = SettingsService.Instance.Sync.IntervalMinutes;
if(intervalMinutes < 5)
{
intervalMinutes = 5;
}
_eventLog.WriteEntry($"Starting timer with {intervalMinutes} minute interval.", EventLogEntryType.Information);
var timerDelegate = new TimerCallback(Callback);
_timer = new Timer(timerDelegate, null, 1000, 60 * 1000 * intervalMinutes);
}
protected override void OnStop()
{
_eventLog.WriteEntry("Service stopped!", EventLogEntryType.Information);
}
private void Callback(object stateInfo)
{
try
{
var sw = Stopwatch.StartNew();
var result = Sync.SyncAllAsync(false, true).GetAwaiter().GetResult();
sw.Stop();
if(result.Success)
{
_eventLog.WriteEntry($"Synced {result.Groups?.Count ?? 0} groups, {result.Users?.Count ?? 0} users. " +
$"The sync took {(int)sw.Elapsed.TotalSeconds} seconds to complete.",
EventLogEntryType.SuccessAudit);
}
else
{
_eventLog.WriteEntry($"Sync failed after {(int)sw.Elapsed.TotalSeconds} seconds: {result.ErrorMessage}.",
EventLogEntryType.FailureAudit);
}
}
catch(ApplicationException e)
{
_eventLog.WriteEntry($"Sync exception: {e.Message}", EventLogEntryType.Error);
}
}
}
}

View File

@@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net461</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.ServiceProcess" />
<Reference Include="System.Configuration.Install" />
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

2
src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
declare function escape(s: string): string;
declare function unescape(s: string): string;

14
src/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bitwarden</title>
<base href="">
</head>
<body>
<app-root>
<div id="loading"><i class="fa fa-spinner fa-spin fa-3x"></i></div>
</app-root>
</body>
</html>

66
src/main.ts Normal file
View File

@@ -0,0 +1,66 @@
import { app, BrowserWindow } from 'electron';
import * as path from 'path';
// import { ElectronMainMessagingService } from 'jslib/electron/services/desktopMainMessaging.service';
import { I18nService } from 'jslib/services/i18n.service';
import { MessagingMain } from './main/messaging.main';
import { ElectronLogService } from 'jslib/electron/services/electronLog.service';
import { ElectronStorageService } from 'jslib/electron/services/electronStorage.service';
import { WindowMain } from 'jslib/electron/window.main';
export class Main {
logService: ElectronLogService;
i18nService: I18nService;
storageService: ElectronStorageService;
windowMain: WindowMain;
messagingMain: MessagingMain;
constructor() {
// Set paths for portable builds
let appDataPath = null;
if (process.env.BITWARDEN_APPDATA_DIR != null) {
appDataPath = process.env.BITWARDEN_APPDATA_DIR;
} else if (process.platform === 'win32' && process.env.PORTABLE_EXECUTABLE_DIR != null) {
appDataPath = path.join(process.env.PORTABLE_EXECUTABLE_DIR, 'bitwarden-appdata');
} else if (process.platform === 'linux' && process.env.SNAP_USER_DATA != null) {
appDataPath = path.join(process.env.SNAP_USER_DATA, 'appdata');
}
if (appDataPath != null) {
app.setPath('userData', appDataPath);
}
app.setPath('logs', path.join(app.getPath('userData'), 'logs'));
const args = process.argv.slice(1);
const watch = args.some((val) => val === '--watch');
if (watch) {
// tslint:disable-next-line
require('electron-reload')(__dirname, {});
}
this.logService = new ElectronLogService(null, app.getPath('userData'));
// this.i18nService = new I18nService('en', './locales/');
this.storageService = new ElectronStorageService();
// this.messagingService = new DesktopMainMessagingService(this);
this.windowMain = new WindowMain(this.storageService);
this.messagingMain = new MessagingMain(this);
}
bootstrap() {
this.windowMain.init().then(async () => {
await this.i18nService.init(app.getLocale());
this.messagingMain.init();
}, (e: any) => {
// tslint:disable-next-line
console.error(e);
});
}
}
const main = new Main();
main.bootstrap();

View File

@@ -0,0 +1,74 @@
import {
app,
ipcMain,
} from 'electron';
import {
deletePassword,
getPassword,
setPassword,
} from 'keytar';
import { Main } from '../main';
const KeytarService = 'Bitwarden';
const SyncInterval = 5 * 60 * 1000; // 5 minutes
export class MessagingMain {
private syncTimeout: NodeJS.Timer;
constructor(private main: Main) { }
init() {
this.scheduleNextSync();
ipcMain.on('messagingService', async (event: any, message: any) => this.onMessage(message));
ipcMain.on('keytar', async (event: any, message: any) => {
try {
let val: string = null;
if (message.action && message.key) {
if (message.action === 'getPassword') {
val = await getPassword(KeytarService, message.key);
} else if (message.action === 'setPassword' && message.value) {
await setPassword(KeytarService, message.key, message.value);
} else if (message.action === 'deletePassword') {
await deletePassword(KeytarService, message.key);
}
}
event.returnValue = val;
} catch {
event.returnValue = null;
}
});
}
onMessage(message: any) {
switch (message.command) {
case 'scheduleNextSync':
this.scheduleNextSync();
break;
case 'updateAppMenu':
// this.main.menuMain.updateApplicationMenuState(message.isAuthenticated, message.isLocked);
break;
default:
break;
}
}
private scheduleNextSync() {
if (this.syncTimeout) {
global.clearTimeout(this.syncTimeout);
}
this.syncTimeout = global.setTimeout(() => {
if (this.main.windowMain.win == null) {
return;
}
this.main.windowMain.win.webContents.send('messagingService', {
command: 'checkSyncVault',
});
}, SyncInterval);
}
}

303
src/package-lock.json generated Normal file
View File

@@ -0,0 +1,303 @@
{
"name": "bitwarden",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "1.0.3"
}
},
"bluebird": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA=="
},
"bluebird-lst": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.5.tgz",
"integrity": "sha512-Ey0bDNys5qpYPhZ/oQ9vOEvD0TYQDTILMXWP2iGfvMg7rSDde+oV4aQQgqRH+CvBFNz2BSDQnPGMUl6LKBUUQA==",
"requires": {
"bluebird": "3.5.1"
}
},
"builder-util-runtime": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.2.0.tgz",
"integrity": "sha512-cROCExnJOJvRD58HHcnrrgyRAoDHGZT0hKox0op7vTuuuRC/1JKMXvSR+Hxy7KWy/aEmKu0HfSqMd4znDEqQsA==",
"requires": {
"bluebird-lst": "1.0.5",
"debug": "3.1.0",
"fs-extra-p": "4.5.2",
"sax": "1.2.4"
}
},
"conf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/conf/-/conf-1.4.0.tgz",
"integrity": "sha512-bzlVWS2THbMetHqXKB8ypsXN4DQ/1qopGwNJi1eYbpwesJcd86FBjFciCQX/YwAhp9bM7NVnPFqZ5LpV7gP0Dg==",
"requires": {
"dot-prop": "4.2.0",
"env-paths": "1.0.0",
"make-dir": "1.2.0",
"pkg-up": "2.0.0",
"write-file-atomic": "2.3.0"
}
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"dot-prop": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
"integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==",
"requires": {
"is-obj": "1.0.1"
}
},
"electron-is-dev": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-0.3.0.tgz",
"integrity": "sha1-FOb9pcaOnk7L7/nM8DfL18BcWv4="
},
"electron-log": {
"version": "2.2.14",
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.14.tgz",
"integrity": "sha512-Rj+XyK4nShe/nv9v1Uks4KEfjtQ6N+eSnx5CLpAjG6rlyUdAflyFHoybcHSLoq9l9pGavclULWS5IXgk8umc2g=="
},
"electron-store": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/electron-store/-/electron-store-1.3.0.tgz",
"integrity": "sha512-r1Pdl5MwpiCxgbsl0qnwv/GABO5+J/JTO16+KyqL+bOITIk9o3cq3Sw69uO9NgPkpfcKeEwxtJFbtbiBlGTiDA==",
"requires": {
"conf": "1.4.0"
}
},
"electron-updater": {
"version": "2.21.4",
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-2.21.4.tgz",
"integrity": "sha512-x6QSbyxgGR3szIOBtFoCJH0TfgB55AWHaXmilNgorfvpnCdEMQEATxEzLOW0JCzzcB5y3vBrawvmMUEdXwutmA==",
"requires": {
"bluebird-lst": "1.0.5",
"builder-util-runtime": "4.2.0",
"electron-is-dev": "0.3.0",
"fs-extra-p": "4.5.2",
"js-yaml": "3.11.0",
"lazy-val": "1.0.3",
"lodash.isequal": "4.5.0",
"semver": "5.5.0",
"source-map-support": "0.5.4"
}
},
"env-paths": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz",
"integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA="
},
"esprima": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
"integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw=="
},
"find-up": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
"requires": {
"locate-path": "2.0.0"
}
},
"fs-extra": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz",
"integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==",
"requires": {
"graceful-fs": "4.1.11",
"jsonfile": "4.0.0",
"universalify": "0.1.1"
}
},
"fs-extra-p": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.5.2.tgz",
"integrity": "sha512-ZYqFpBdy9w7PsK+vB30j+TnHOyWHm/CJbUq1qqoE8tb71m6qgk5Wa7gp3MYQdlGFxb9vfznF+yD4jcl8l+y91A==",
"requires": {
"bluebird-lst": "1.0.5",
"fs-extra": "5.0.0"
}
},
"graceful-fs": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
},
"imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
},
"is-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8="
},
"js-yaml": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz",
"integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==",
"requires": {
"argparse": "1.0.10",
"esprima": "4.0.0"
}
},
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"requires": {
"graceful-fs": "4.1.11"
}
},
"keytar": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/keytar/-/keytar-4.1.0.tgz",
"integrity": "sha512-L3KqiSMtpVitlug4uuI+K5XLne9SAVEFWE8SCQIhQiH0IA/CTbon5v5prVLKK0Ken54o2O8V9HceKagpwJum+Q==",
"requires": {
"nan": "2.5.1"
}
},
"lazy-val": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.3.tgz",
"integrity": "sha512-pjCf3BYk+uv3ZcPzEVM0BFvO9Uw58TmlrU0oG5tTrr9Kcid3+kdKxapH8CjdYmVa2nO5wOoZn2rdvZx2PKj/xg=="
},
"locate-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
"requires": {
"p-locate": "2.0.0",
"path-exists": "3.0.0"
}
},
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"make-dir": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.2.0.tgz",
"integrity": "sha512-aNUAa4UMg/UougV25bbrU4ZaaKNjJ/3/xnvg/twpmKROPdKZPZ9wGgI0opdZzO8q/zUFawoUuixuOv33eZ61Iw==",
"requires": {
"pify": "3.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"nan": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.5.1.tgz",
"integrity": "sha1-1bAWkSUzJql6K77p5hxV2NYDUeI="
},
"p-limit": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz",
"integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==",
"requires": {
"p-try": "1.0.0"
}
},
"p-locate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
"requires": {
"p-limit": "1.2.0"
}
},
"p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
},
"pkg-up": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz",
"integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=",
"requires": {
"find-up": "2.1.0"
}
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
},
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"source-map-support": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.4.tgz",
"integrity": "sha512-PETSPG6BjY1AHs2t64vS2aqAgu6dMIMXJULWFBGbh2Gr8nVLbCFDo6i/RMMvviIQ2h1Z8+5gQhVKSn2je9nmdg==",
"requires": {
"source-map": "0.6.1"
}
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"universalify": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
},
"write-file-atomic": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz",
"integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==",
"requires": {
"graceful-fs": "4.1.11",
"imurmurhash": "0.1.4",
"signal-exit": "3.0.2"
}
}
}
}

20
src/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "bitwarden",
"productName": "Bitwarden Directory Connector",
"description": "A secure and free password manager for all of your devices.",
"version": "1.0.0",
"author": "8bit Solutions LLC <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",
"main": "main.js",
"repository": {
"type": "git",
"url": "https://github.com/bitwarden/desktop"
},
"dependencies": {
"electron-log": "2.2.14",
"electron-store": "1.3.0",
"electron-updater": "2.21.4",
"keytar": "4.1.0"
}
}

75
src/scss/base.scss Normal file
View File

@@ -0,0 +1,75 @@
@import "variables.scss";
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html, body {
height: 100%;
background-color: $background-color-alt2;
font-family: $font-family-sans-serif;
font-size: $font-size-base;
color: $text-color;
line-height: $line-height-base;
}
h1, h2, h3, h4, h5, h6 {
font-family: $font-family-sans-serif;
color: $text-color;
}
p {
margin-bottom: 10px;
}
ul, ol {
margin-bottom: 10px;
}
img {
border: none;
}
a {
color: $brand-primary;
text-decoration: none;
&:hover, &:focus {
color: darken($brand-primary, 6%);
}
}
input, select, textarea, button {
font-size: $font-size-base;
font-family: $font-family-sans-serif;
}
button {
white-space: nowrap;
cursor: pointer;
}
textarea {
resize: vertical;
}
div:not(.modal)::-webkit-scrollbar {
width: 10px;
height: 10px;
}
div:not(.modal)::-webkit-scrollbar-track {
background-color: transparent;
}
div:not(.modal)::-webkit-scrollbar-thumb {
background-color: rgba(100,100,100,.2);
border-radius: 10px;
margin-right: 1px;
&:hover {
background-color: rgba(100,100,100,.4);
}
}

260
src/scss/box.scss Normal file
View File

@@ -0,0 +1,260 @@
@import "variables.scss";
.box {
width: 100%;
.box-header {
margin: 0 10px 5px 10px;
color: $gray-light;
text-transform: uppercase;
button {
background-color: transparent;
border: none;
color: $gray-light;
text-transform: uppercase;
}
}
.box-content {
background: $box-background-color;
border-radius: $border-radius;
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12), 0 1px 5px 0 rgba(0,0,0,0.2);
&.box-content-padded {
padding: 10px 15px;
}
.box-content-row {
display: block;
padding: 10px 15px;
position: relative;
z-index: 1;
&:before {
content: "";
position: absolute;
right: 0;
bottom: 0;
height: 1px;
width: calc(100% - 10px);
border-bottom: 1px solid $box-border-color;
}
&:first-child, &:last-child {
border-radius: $border-radius;
}
&:last-child {
&:before {
border: none;
height: 0;
}
}
&:after {
content: "";
display: table;
clear: both;
}
&:hover, &:focus, &.active {
background-color: $box-background-hover-color;
}
&.pre {
white-space: pre;
overflow-x: auto;
}
.row-label, label {
font-size: $font-size-small;
color: $text-muted;
display: block;
width: 100%;
margin-bottom: 5px;
}
.text, .detail {
display: block;
color: $text-color;
}
.detail {
font-size: $font-size-small;
color: $text-muted;
}
.img-right {
float: right;
margin-left: 10px;
}
.row-main {
flex-grow: 1;
}
&.box-content-row-flex, &.box-content-row-checkbox, &.box-content-row-input,
&.box-content-row-slider, &.box-content-row-multi {
display: flex;
align-items: center;
word-break: break-word;
}
&.box-content-row-multi {
width: 100%;
input:not([type="checkbox"]) {
width: 100%;
}
input + label.sr-only + select {
margin-top: 5px;
}
> a {
padding: 8px 8px 8px 4px;
color: $brand-danger;
margin: 0;
}
}
&.box-content-row-checkbox, &.box-content-row-input, &.box-content-row-slider {
label, .row-label {
font-size: $font-size-base;
color: $text-color;
display: inline;
width: initial;
margin-bottom: 0;
}
> span {
color: $text-muted;
}
> input {
margin: 0 0 0 auto;
padding: 0;
}
> * {
margin-right: 15px;
&:last-child {
margin-right: 0;
}
}
}
&.box-content-row-input {
label {
white-space: nowrap;
}
input {
text-align: right;
}
}
&.box-content-row-slider {
input[type="range"] {
height: 10px;
}
input[type="number"] {
width: 45px;
}
label {
white-space: nowrap;
}
}
input:not([type="checkbox"]), textarea {
border: none;
width: 100%;
background-color: transparent;
&::-webkit-input-placeholder {
color: lighten($gray-light, 35%);
}
&:focus {
outline: none;
}
}
select {
width: 100%;
border: 1px solid darken($border-color-dark, 7%);
border-radius: $border-radius;
}
.action-buttons {
display: flex;
margin-left: 5px;
.row-btn {
cursor: pointer;
padding: 10px 8px;
background: none;
border: none;
color: $brand-primary;
&:hover, &:focus {
color: darken($brand-primary, 10%);
}
&.disabled {
color: $list-icon-color;
&:hover {
color: $list-icon-color;
}
}
&:last-child {
padding-right: 2px !important;
}
}
&.no-pad .row-btn {
padding-top: 0;
padding-bottom: 0;
}
}
select.field-type {
margin: 5px 0 0 25px;
width: calc(100% - 25px);
}
.row-sub-icon {
color: $list-icon-color;
}
.row-sub-label {
margin: 0 15px;
color: $gray-light;
white-space: nowrap;
}
}
&.condensed .box-content-row, .box-content-row.condensed {
padding-top: 5px;
padding-bottom: 5px;
}
&.no-hover .box-content-row, .box-content-row.no-hover {
&:hover, &:focus {
background-color: initial;
}
}
}
.box-footer {
margin: 5px 10px;
font-size: $font-size-small;
color: $text-muted;
}
}

70
src/scss/buttons.scss Normal file
View File

@@ -0,0 +1,70 @@
@import "variables.scss";
.btn, #vault .footer button, .modal-footer button {
background-color: $button-backgound-color;
border-radius: $border-radius;
padding: 7px 15px;
border: 1px solid $button-border-color;
font-size: $font-size-base;
color: $button-color;
white-space: nowrap;
text-align: center;
cursor: pointer;
&.primary {
color: $button-color-primary;
}
&.danger {
color: $button-color-danger;
}
&:hover:not([disabled]) {
cursor: pointer;
background-color: darken($button-backgound-color, 1.5%);
border-color: darken($button-border-color, 17%);
color: darken($button-color, 10%);
&.primary {
color: darken($button-color-primary, 6%);
}
&.danger {
color: darken($button-color-danger, 6%);
}
}
&:focus:not([disabled]) {
cursor: pointer;
background-color: darken($button-backgound-color, 6%);
border-color: darken($button-border-color, 25%);
outline: 0;
}
&[disabled] {
opacity: 0.65;
cursor: default !important;
}
&.block {
display: block;
width: 100%;
}
&.link {
border: none !important;
background: none !important;
&:focus {
text-decoration: underline;
}
}
}
.action-buttons {
.btn {
&:focus {
outline: auto;
}
}
}

View File

@@ -0,0 +1,7 @@
@import "variables.scss";
html.os_windows {
body {
border-top: 1px solid $border-color-dark;
}
}

76
src/scss/list.scss Normal file
View File

@@ -0,0 +1,76 @@
@import "variables.scss";
.list > a {
display: block;
padding: 3px 10px;
background-color: white;
text-decoration: none;
color: $text-color;
position: relative;
z-index: 1;
&:after {
content: "";
display: table;
clear: both;
}
&:before {
content: "";
position: absolute;
right: 0;
bottom: 0;
height: 1px;
width: calc(100% - 10px);
border-bottom: 1px solid $border-color;
}
&:last-child:before {
border: none;
height: 0;
}
&:hover, &:focus, &.active {
background-color: $list-item-hover;
}
&.active {
border-left: 5px solid $brand-primary;
padding-left: 5px;
}
&:focus:not(.active) {
border-left: 5px solid $text-muted;
padding-left: 5px;
}
.text, .detail {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
color: $text-color;
}
.detail {
font-size: $font-size-small;
color: $gray-light;
}
.icon {
display: flex;
justify-content: center;
align-items: center;
float: left;
height: 36px;
width: 34px;
margin-left: -5px;
color: $text-muted;
img {
border-radius: $border-radius;
max-height: 20px;
max-width: 20px;
}
}
}

188
src/scss/misc.scss Normal file
View File

@@ -0,0 +1,188 @@
@import "variables.scss";
small {
font-size: $font-size-small;
}
.text-primary {
color: $brand-primary !important;
}
.text-success {
color: $brand-success !important;
}
.text-muted {
color: $text-muted !important;
}
.text-default {
color: $text-color !important;
}
.text-center {
text-align: center;
}
.no-margin {
margin: 0 !important;
}
p.lead {
font-size: $font-size-large;
margin-bottom: 20px;
font-weight: normal;
}
[hidden] {
display: none !important;
}
.monospaced {
font-family: $font-family-monospace;
}
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
border: 0 !important;
}
.totp {
.totp-code {
font-family: $font-family-monospace;
font-size: 1.1em;
}
.totp-countdown {
margin: 3px 3px 0 0;
display: block;
user-select: none;
.totp-sec {
font-size: 0.85em;
position: absolute;
line-height: 32px;
width: 32px;
text-align: center;
}
svg {
width: 32px;
height: 32px;
transform: rotate(-90deg);
}
.totp-circle {
stroke: $brand-primary;
fill: none;
&.inner {
stroke-width: 3;
stroke-dasharray: 78.6;
stroke-dashoffset: 0;
}
&.outer {
stroke-width: 2;
stroke-dasharray: 88;
stroke-dashoffset: 0;
}
}
}
&.low {
.totp-sec, .totp-code {
color: $brand-danger;
}
.totp-circle {
stroke: $brand-danger;
}
}
}
.password-block {
font-size: $font-size-large;
word-break: break-all;
font-family: $font-family-monospace;
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
.modal-body & {
margin-top: 10px;
}
}
#duo-frame {
background: url('../images/loading.svg') 0 0 no-repeat;
height: 330px;
margin: 0 -150px 15px -150px;
iframe {
width: 100%;
height: 100%;
border: none;
}
}
form, .form {
.form-group {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
label {
display: inline-block;
margin-bottom: 2px;
}
input, select, textarea {
border: 1px solid darken($border-color-dark, 7%);
border-radius: $border-radius;
display: block;
}
}
.checkbox {
position: relative;
display: block;
padding-left: 18px;
label {
margin-bottom: 0;
}
input[type="checkbox"] {
position: absolute;
margin-top: 4px;
margin-left: -18px;
}
}
.help-block {
margin-top: 3px;
color: $text-muted;
display: block;
}
}
app-root > #loading {
display: flex;
text-align: center;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
color: $text-muted;
}

338
src/scss/modal.scss Normal file
View File

@@ -0,0 +1,338 @@
@import "variables.scss";
$white: white;
$black: black;
$line-height-base: 14px;
$border-radius-lg: $border-radius;
// ref: https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss
$grid-breakpoints: ( xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px ) !default;
$zindex-modal-backdrop: 1040 !default;
$zindex-modal: 1050 !default;
// Padding applied to the modal body
$modal-inner-padding: 10px !default;
$modal-dialog-margin: .5rem !default;
$modal-dialog-margin-y-sm-up: 1.75rem !default;
$modal-title-line-height: $line-height-base !default;
$modal-content-bg: $background-color-alt !default;
$modal-content-border-color: rgba($black, .2) !default;
$modal-content-border-width: 1px !default;
$modal-content-box-shadow-xs: none;
$modal-content-box-shadow-sm-up: none;
$modal-backdrop-bg: $black !default;
$modal-backdrop-opacity: .5 !default;
$modal-header-border-color: $border-color-dark !default;
$modal-footer-border-color: $modal-header-border-color !default;
$modal-header-border-width: $modal-content-border-width !default;
$modal-footer-border-width: $modal-header-border-width !default;
$modal-header-padding: 12px !default;
$modal-lg: 800px !default;
$modal-md: 500px !default;
$modal-sm: 300px !default;
$modal-transition: transform .3s ease-out !default;
$close-font-size: $font-size-base * 1.5 !default;
$close-font-weight: bold !default;
$close-color: $black !default;
$close-text-shadow: 0 1px 0 $white !default;
// ref: https://github.com/twbs/bootstrap/blob/v4-dev/scss/mixins/_breakpoints.scss
@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {
$min: breakpoint-min($name, $breakpoints);
@if $min {
@media (min-width: $min) {
@content;
}
}
@else {
@content;
}
}
@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {
$min: map-get($breakpoints, $name);
@return if($min != 0, $min, null);
}
// Custom Added CSS animations
@keyframes modalshow {
0% {
opacity: 0;
transform: translate(0, -25%);
}
100% {
opacity: 1;
transform: translate(0, 0);
}
}
@keyframes backdropshow {
0% {
opacity: 0;
}
100% {
opacity: $modal-backdrop-opacity;
}
}
// ref: https://github.com/twbs/bootstrap/blob/v4-dev/scss/_modal.scss
// .modal-open - body class for killing the scroll
// .modal - container to scroll within
// .modal-dialog - positioning shell for the actual modal
// .modal-content - actual modal w/ bg and corners and stuff
// Kill the scroll on the body
.modal-open {
overflow: hidden;
}
// Container that the modal scrolls within
.modal {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: $zindex-modal;
//display: none;
overflow: hidden;
// Prevent Chrome on Windows from adding a focus outline. For details, see
// https://github.com/twbs/bootstrap/pull/10951.
outline: 0;
// We deliberately don't use `-webkit-overflow-scrolling: touch;` due to a
// gnarly iOS Safari bug: https://bugs.webkit.org/show_bug.cgi?id=158342
// See also https://github.com/twbs/bootstrap/issues/17695
.modal-open & {
overflow-x: hidden;
overflow-y: auto;
}
}
// Shell div to position the modal with bottom padding
.modal-dialog {
position: relative;
width: auto;
margin: $modal-dialog-margin;
// allow clicks to pass through for custom click handling to close modal
pointer-events: none;
// When fading in the modal, animate it to slide down
.modal.fade & {
//@include transition($modal-transition);
//transform: translate(0, -25%);
animation: modalshow 0.3s ease-in;
}
//.modal.show & {
// transform: translate(0, 0);
//}
transform: translate(0, 0);
}
.modal-dialog-centered {
display: flex;
align-items: center;
min-height: calc(100% - (#{$modal-dialog-margin} * 2));
}
// Actual modal
.modal-content {
position: relative;
display: flex;
flex-direction: column;
width: 100%; // Ensure `.modal-content` extends the full width of the parent `.modal-dialog`
// counteract the pointer-events: none; in the .modal-dialog
pointer-events: auto;
background-color: $modal-content-bg;
background-clip: padding-box;
border: $modal-content-border-width solid $modal-content-border-color;
//@include border-radius($border-radius-lg);
//@include box-shadow($modal-content-box-shadow-xs);
border-radius: $border-radius-lg;
box-shadow: $modal-content-box-shadow-xs;
// Remove focus outline from opened modal
outline: 0;
}
// Modal background
.modal-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: $zindex-modal-backdrop;
background-color: $modal-backdrop-bg;
// Fade for backdrop
&.fade {
//opacity: 0;
animation: backdropshow 0.1s ease-in;
}
//&.show {
// opacity: $modal-backdrop-opacity;
//}
opacity: $modal-backdrop-opacity;
}
// Modal header
// Top section of the modal w/ title and dismiss
.modal-header {
display: flex;
align-items: flex-start; // so the close btn always stays on the upper right corner
justify-content: space-between; // Put modal header elements (title and dismiss) on opposite ends
padding: $modal-header-padding $modal-inner-padding;
border-bottom: $modal-header-border-width solid $modal-header-border-color;
//@include border-top-radius($border-radius-lg);
.close {
padding: $modal-header-padding $modal-inner-padding;
// auto on the left force icon to the right even when there is no .modal-title
margin: (-$modal-header-padding) (-$modal-inner-padding) (-$modal-header-padding) auto;
}
h5 {
font-size: $font-size-base;
font-weight: bold;
display: flex;
align-items: center;
.fa {
margin-right: 5px;
}
}
}
// Title text within header
.modal-title {
margin-bottom: 0;
line-height: $modal-title-line-height;
}
// Modal body
// Where all modal content resides (sibling of .modal-header and .modal-footer)
.modal-body {
position: relative;
// Enable `flex-grow: 1` so that the body take up as much space as possible
// when should there be a fixed height on `.modal-dialog`.
flex: 1 1 auto;
padding: $modal-inner-padding;
}
// Footer (for actions)
.modal-footer {
display: flex;
align-items: center; // vertically center
//justify-content: flex-end; // Right align buttons with flex property because text-align doesn't work on flex items
padding: $modal-inner-padding;
border-top: $modal-footer-border-width solid $modal-footer-border-color;
// Easily place margin between footer elements
button {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
.right {
margin-left: auto;
display: flex;
}
}
// Measure scrollbar width for padding body during modal show/hide
.modal-scrollbar-measure {
position: absolute;
top: -9999px;
width: 50px;
height: 50px;
overflow: scroll;
}
// Scale up the modal
@include media-breakpoint-up(sm) {
// Automatically set modal's width for larger viewports
.modal-dialog {
max-width: $modal-md;
margin: $modal-dialog-margin-y-sm-up auto;
}
.modal-dialog-centered {
min-height: calc(100% - (#{$modal-dialog-margin-y-sm-up} * 2));
}
.modal-content {
//@include box-shadow($modal-content-box-shadow-sm-up);
box-shadow: $modal-content-box-shadow-sm-up;
}
.modal-sm {
max-width: $modal-sm;
}
}
@include media-breakpoint-up(lg) {
.modal-lg {
max-width: $modal-lg;
}
}
// ref: https://github.com/twbs/bootstrap/blob/v4-dev/scss/_close.scss
.close {
float: right;
font-size: $close-font-size;
font-weight: $close-font-weight;
line-height: 1;
color: $close-color;
text-shadow: $close-text-shadow;
opacity: .5;
&:hover, &:focus {
color: $close-color;
text-decoration: none;
opacity: .75;
}
// Opinionated: add "hand" cursor to non-disabled .close elements
&:not(:disabled):not(.disabled) {
cursor: pointer;
}
}
// Additional properties for button version
// iOS requires the button element instead of an anchor tag.
// If you want the anchor version, it requires `href="#"`.
// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile
// stylelint-disable property-no-vendor-prefix, selector-no-qualifying-type
button.close {
padding: 0;
background-color: transparent;
border: 0;
-webkit-appearance: none;
}
// stylelint-enable
// box
.modal-content .box {
margin-top: 20px;
&:first-child {
margin-top: 0;
}
}

118
src/scss/pages.scss Normal file
View File

@@ -0,0 +1,118 @@
@import "variables.scss";
#login-page, #lock-page {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
@media (min-height: 500px) {
height: calc(100% + 50px);
margin-top: -50px;
}
@media (min-height: 800px) {
height: calc(100% + 300px);
margin-top: -300px;
}
img {
margin: 0 auto 15px;
width: 282px;
display: block;
}
}
#register-page, #hint-page, #two-factor-page {
padding-top: 20px;
.content {
margin: 0 auto;
}
img {
margin-bottom: 10px;
max-width: 100%;
height: auto;
display: block;
border-radius: $border-radius;
}
}
#login-page, #register-page, #hint-page, #two-factor-page, #lock-page {
.content {
width: 300px;
p {
text-align: center
}
p.lead, h1 {
font-size: $font-size-large;
text-align: center;
margin-bottom: 20px;
font-weight: normal;
}
.box {
margin-bottom: 20px;
&.last {
margin-bottom: 15px;
}
}
.buttons {
display: flex;
margin-bottom: 20px;
button {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
}
.sub-options {
text-align: center;
margin-bottom: 20px;
a {
display: block;
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
}
a.settings-icon {
color: #bbbbbb;
position: absolute;
top: 10px;
left: 10px;
span {
visibility: hidden;
}
&:hover, &:focus {
color: $brand-primary;
text-decoration: none;
span {
visibility: visible;
}
}
}
}
}
#register-page {
.content {
width: 400px;
}
}

75
src/scss/plugins.scss Normal file
View File

@@ -0,0 +1,75 @@
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome.scss";
@import "~angular2-toaster/toaster";
@import "variables.scss";
#toast-container {
.toast-close-button {
right: -0.15em;
}
.toast {
opacity: 1 !important;
background-image: none !important;
border-radius: $border-radius;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.35);
display: flex;
align-items: center;
&:hover {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
}
&:before {
font-family: FontAwesome;
font-size: 25px;
line-height: 20px;
float: left;
color: #ffffff;
padding-right: 10px;
margin: auto 0 auto -36px;
}
.toaster-icon {
display: none;
}
&.toast-danger, &.toast-error {
background-image: none !important;
background-color: $brand-danger;
&:before {
content: "\f0e7";
margin-left: -30px;
}
}
&.toast-warning {
background-image: none !important;
background-color: $brand-warning;
&:before {
content: "\f071";
}
}
&.toast-info {
background-image: none !important;
background-color: $brand-info;
&:before {
content: "\f05a";
}
}
&.toast-success {
background-image: none !important;
background-color: $brand-success;
&:before {
content: "\f00C";
}
}
}
}

11
src/scss/styles.scss Normal file
View File

@@ -0,0 +1,11 @@
@import "variables.scss";
@import "base.scss";
@import "pages.scss";
@import "vault.scss";
@import "list.scss";
@import "box.scss";
@import "buttons.scss";
@import "misc.scss";
@import "modal.scss";
@import "plugins.scss";
@import "environment.scss";

38
src/scss/variables.scss Normal file
View File

@@ -0,0 +1,38 @@
$font-family-sans-serif: 'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;
$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
$font-size-base: 14px;
$font-size-large: 18px;
$font-size-small: 12px;
$text-color: #000000;
$background-color: #efeff4;
$border-color: #f0f0f0;
$border-color-dark: #ddd;
$list-item-hover: #fbfbfb;
$list-icon-color: #c7c7cd;
$border-radius: 3px;
$line-height-base: 1.42857143;
$gray: #555;
$gray-light: #777;
$text-muted: $gray-light;
$brand-primary: #3c8dbc;
$brand-danger: #dd4b39;
$brand-success: #00a65a;
$brand-info: #555555;
$brand-warning: #bf7e16;
$brand-primary-accent: #286090;
$background-color: white;
$background-color-alt: #f9fafc;
$background-color-alt2: #ecf0f5;
$box-background-color: $background-color;
$box-background-hover-color: $list-item-hover;
$box-border-color: $border-color;
$button-border-color: darken($border-color-dark, 12%);
$button-backgound-color: white;
$button-color: lighten($text-color, 40%);
$button-color-primary: darken($brand-primary, 8%);
$button-color-danger: darken($brand-danger, 10%);

269
src/scss/vault.scss Normal file
View File

@@ -0,0 +1,269 @@
@import "variables.scss";
#vault {
height: 100%;
display: flex;
> #groupings, > #items, > #details, > #logo {
display: flex;
flex-direction: column;
.inner-content {
padding: 10px 15px;
}
}
> #groupings {
background-color: $background-color-alt;
width: 22%;
min-width: 175px;
max-width: 250px;
border-right: 1px solid $border-color-dark;
.inner-content {
padding-bottom: 0;
padding-right: 5px;
}
h2 {
color: $gray-light;
text-transform: uppercase;
font-size: $font-size-base;
font-weight: normal;
margin-bottom: 5px;
display: flex;
button {
margin-left: auto;
background: none;
border: none;
color: lighten($gray-light, 30%);
&:hover, &:focus {
color: $gray-light;
cursor: pointer;
}
}
}
ul:not(.fa-ul) {
li {
margin: 0;
padding: 0;
list-style: none;
}
}
ul.fa-ul {
li {
.fa-li {
left: -11px;
top: 8px;
}
a {
padding-left: 12px;
}
&.active .fa-li {
left: 4px;
}
}
}
ul {
padding: 0;
margin: 0 0 15px 0;
li {
a {
padding: 5px 0;
color: $text-color;
display: flex;
align-items: center;
span {
visibility: hidden;
margin-left: auto;
color: lighten($gray-light, 30%);
&:hover {
color: $text-muted;
}
}
&:hover, &:focus {
span {
visibility: visible;
}
}
}
&.active {
background-color: darken($background-color-alt, 5%);
margin-left: -15px;
margin-right: -5px;
padding-left: 15px;
padding-right: 5px;
}
}
}
}
> #items {
background-color: $background-color;
width: 28%;
min-width: 200px;
max-width: 350px;
border-right: 1px solid $border-color-dark;
.no-items {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 0 10px;
.fa {
margin-bottom: 10px;
color: $list-icon-color;
}
}
}
> #details {
background-color: $background-color-alt2;
flex: 1;
min-width: 0;
.inner-content {
min-width: 400px;
}
.box {
max-width: 550px;
margin: 30px auto 0 auto;
&:first-child {
margin-top: 10px;
}
&:last-child {
margin-bottom: 30px;
}
}
> form {
display: flex;
flex-direction: column;
height: 100%;
}
}
> #logo {
flex: 1;
min-width: 0;
.content {
overflow-y: hidden;
overflow-x: auto;
}
.inner-content {
min-width: 320px;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
img {
width: 282px;
opacity: 0.3;
transition: all 1s ease-in-out;
&:hover {
opacity: 1;
}
}
}
.header {
min-height: 44px;
max-height: 44px;
background-color: $brand-primary;
flex: 0 0 auto;
border-bottom: 1px solid darken($brand-primary, 7%);
display: flex;
align-items: center;
&.header-search {
.search {
padding: 0 7px;
width: 100%;
text-align: left;
position: relative;
.fa {
position: absolute;
top: 7px;
left: 15px;
color: lighten($brand-primary, 30%);
}
input {
width: 100%;
margin: 0;
background: darken($brand-primary, 8%);
border: none;
color: white;
padding: 5px 10px 5px 30px;
border-radius: $border-radius;
&:focus {
border-radius: $border-radius;
outline: none;
background: darken($brand-primary, 10%);
}
&::-webkit-input-placeholder {
color: lighten($brand-primary, 35%);
}
}
}
}
}
.content {
flex: 1 1 auto;
position: relative;
overflow: auto;
height: 100%;
}
.footer {
height: 50px;
background-color: $background-color-alt;
flex: 0 0 auto;
border-top: 1px solid $border-color-dark;
display: flex;
align-items: center;
padding: 0 15px;
button {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
.right {
margin-left: auto;
display: flex;
}
}
}

View File

@@ -14,7 +14,8 @@
"jslib/*": [ "jslib/src/*" ],
"@angular/*": [ "node_modules/@angular/*" ],
"angular2-toaster": [ "node_modules/angular2-toaster" ],
"angulartics2": [ "node_modules/angulartics2" ]
"angulartics2": [ "node_modules/angulartics2" ],
"electron": [ "node_modules/electron" ]
}
},
"exclude": [
@@ -22,6 +23,7 @@
"jslib/node_modules",
"dist",
"jslib/dist",
"build"
"build",
"jslib/spec"
]
}

66
webpack.main.js Normal file
View File

@@ -0,0 +1,66 @@
const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const nodeExternals = require('webpack-node-externals');
const common = {
module: {
rules: [
{
test: /\.ts$/,
enforce: 'pre',
loader: 'tslint-loader'
},
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules\/(?!(@bitwarden)\/).*/
},
]
},
plugins: [],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
jslib: path.join(__dirname, 'jslib/src')
}
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'build')
}
};
const main = {
target: 'electron-main',
node: {
__dirname: false,
__filename: false
},
entry: {
'main': './src/main.ts'
},
module: {
rules: [
{
test: /\.node$/,
loader: 'node-loader'
},
]
},
plugins: [
new CleanWebpackPlugin([
path.resolve(__dirname, 'build/*')
]),
new CopyWebpackPlugin([
'./src/package.json',
{ from: './src/images', to: 'images' },
{ from: './src/locales', to: 'locales' },
]),
],
externals: [nodeExternals()]
};
module.exports = merge(common, main);

144
webpack.renderer.js Normal file
View File

@@ -0,0 +1,144 @@
const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const GoogleFontsPlugin = require("google-fonts-webpack-plugin");
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
const isVendorModule = (module) => {
if (!module.context) {
return false;
}
return module.context.indexOf('node_modules') !== -1;
};
const extractCss = new ExtractTextPlugin({
filename: '[name].css',
disable: false,
allChunks: true
});
const common = {
module: {
rules: [
{
test: /\.ts$/,
enforce: 'pre',
loader: 'tslint-loader'
},
{
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
loader: '@ngtools/webpack'
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
exclude: /.*(fontawesome-webfont)\.svg/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'images/',
}
}]
}
]
},
plugins: [],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
jslib: path.join(__dirname, 'jslib/src')
},
symlinks: false,
modules: [path.resolve('node_modules')]
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'build')
}
};
const renderer = {
target: 'electron-renderer',
node: {
__dirname: false
},
entry: {
'app/main': './src/app/main.ts'
},
module: {
rules: [
{
test: /\.(html)$/,
loader: 'html-loader'
},
{
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
exclude: /loading.svg/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/',
publicPath: '../'
}
}]
},
{
test: /\.scss$/,
use: extractCss.extract({
use: [
{
loader: 'css-loader',
},
{
loader: 'sass-loader',
}
],
publicPath: '../'
})
},
]
},
plugins: [
new GoogleFontsPlugin({
fonts: [
{
family: 'Open Sans',
variants: ['300', '300italic', '400', '400italic', '600', '600italic',
'700', '700italic', '800', '800italic'],
subsets: ['cyrillic', 'cyrillic-ext', 'greek', 'greek-ext', 'latin', 'latin-ext']
}
],
formats: ['woff2'],
path: 'fonts/',
filename: 'css/fonts.css'
}),
new AngularCompilerPlugin({
tsConfigPath: 'tsconfig.json',
entryModule: 'src/app/app.module#AppModule',
sourceMap: true
}),
// ref: https://github.com/angular/angular/issues/20357
new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)esm5/,
path.resolve(__dirname, './src')),
new webpack.optimize.CommonsChunkPlugin({
name: 'app/vendor',
chunks: ['app/main'],
minChunks: isVendorModule
}),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
chunks: ['app/vendor', 'app/main']
}),
new webpack.SourceMapDevToolPlugin({
filename: '[name].js.map',
include: ['app/main.js']
}),
extractCss
]
};
module.exports = merge(common, renderer);