private async Task <bool> DeleteCloudUserIfExists() { var user = await _db.SingleAsync <User>(x => x.AuthKey == _defaultCloudUserName); if (user == null) { return(false); } AuthUtils.ClearUserFromCacheIfExists(_cache, _defaultCloudUserName); await _db.DeleteAsync(user); return(true); }
// Used to invalidate a cached user if they are deleted / updated private void ClearUserFromCacheIfExists(string username) { AuthUtils.ClearUserFromCacheIfExists(_cache, username); }
public void Perform() { if (!IsEnabled()) { _logger.LogError("FCEJ: Job enabled, but matching criterion does not match. This should not happen, silently going back to sleep."); return; } var nodeId = ConfigUtils.GetConfig(_db, ConfigKeys.CloudConnectIdentifier).Result?.Value; var nodeKey = ConfigUtils.GetConfig(_db, ConfigKeys.CloudConnectNodeKey).Result?.Value; var localIdentity = _identityProvider.GetGuid().ToString(); if (nodeId == null || nodeKey == null || localIdentity == null) { _logger.LogCritical("FCEJ: Job enabled, but one of -> (nodeId || nodeKey || localIdentity) were null. Please manually resolve."); return; } var slug = string.Format("unauth/node/{0}/config-pull", nodeId); var request = new RestRequest(slug, Method.POST) { RequestFormat = DataFormat.Json }; var dataMap = new Dictionary <string, string> { { "node_key", nodeKey }, { "install_id", localIdentity } }; // RFC violating no more! request.AddParameter("application/json; charset=utf-8", JsonConvert.SerializeObject(dataMap), ParameterType.RequestBody); var response = _restClient.Execute(request); // Yes, can more or less throw unchecked exceptions. Hangfire will pick it up and mark the job as failed. if (response.ErrorException != null) { throw response.ErrorException; } /* * This is what we have to parse out of the response. * { * "errors": [], * "result": [ * { * "engagement_id": 3, * "username": "******", * "password": "******", * "sync_timestamp": "2018-03-22 23:24:27" * } * ], * "message": null, * "version": "v1" * } */ _logger.LogDebug("FCEJ: Got response from the cloud backend!: " + response.Content); if (response.Content.IsNullOrEmpty()) { _logger.LogError("FCEJ: Got empty or null response from the backend cloud API, this is not meant to happen!"); return; } // World's unsafest cast contender? Taking bets now. var parsedResponse = JsonConvert.DeserializeObject <CloudAPIResponse <List <Engagement> > >(response.Content); var engagements = parsedResponse.result; if (engagements == null) { _logger.LogWarning("FCEJ: List returned by the cloud for engagements was null. This is not meant to happen."); return; } // First, let's remove those users who are no longer in the output. // To do so, let's fetch ALL Cloud users (except the cloud administrative user, we're not messing with it) var currentCloudUsers = _db.Select <User>(x => x.Source == User.SourceTypes.SpecteroCloud && x.AuthKey != AppConfig.CloudConnectDefaultAuthKey); var usersToAllowToPersist = currentCloudUsers.Where(x => engagements.FirstOrDefault(f => f.username == x.AuthKey) != null); foreach (var userToBeRemoved in currentCloudUsers.Except(usersToAllowToPersist)) { _logger.LogInformation("FCEJ: Terminating " + userToBeRemoved.AuthKey + " because cloud no longer has a reference to it."); AuthUtils.ClearUserFromCacheIfExists(_cache, userToBeRemoved.AuthKey); // Order matters, let's pay attention _db.Delete(userToBeRemoved); } foreach (var engagement in engagements) { /* * First see if user already exists, and if details are different. If yes, look it up, and replace it fully. * If not, we insert a brand new user. */ try { var existingUser = _db.Single <User>(x => x.EngagementId == engagement.engagement_id); if (existingUser != null) { if (existingUser.AuthKey == engagement.username && existingUser.Password == engagement.password && existingUser.Cert == engagement.cert && existingUser.CertKey == engagement.cert_key) { continue; } AuthUtils.ClearUserFromCacheIfExists(_cache, existingUser.AuthKey); // Stop allowing cached logins, details are changing. _logger.LogInformation("FCEJ: Updating user " + existingUser.AuthKey + " with new details from the Spectero Cloud."); existingUser.AuthKey = engagement.username; existingUser.Password = engagement.password; // This time it's already encrypted existingUser.Cert = engagement.cert; existingUser.CertKey = engagement.cert_key; existingUser.CloudSyncDate = DateTime.Now; // Is the cert encrypted? existingUser.EncryptCertificate = !engagement.cert_key.IsNullOrEmpty(); _db.Update(existingUser); } else { _logger.LogInformation("FCEJ: Adding new user " + engagement.username + " as directed by the Spectero Cloud."); var user = new User { EngagementId = engagement.engagement_id, AuthKey = engagement.username, Password = engagement.password, // It already comes encrypted, don't use the setter to double encrypt it. Source = User.SourceTypes.SpecteroCloud, Roles = new List <User.Role> { User.Role.HTTPProxy, User.Role.OpenVPN, User.Role.SSHTunnel, User.Role.ShadowSOCKS }, // Only service access roles, no administrative access. CreatedDate = DateTime.Now, Cert = engagement.cert, CertKey = engagement.cert_key, // TODO: The backend currently returns empty strings for these, but one day it'll be useful. FullName = "Spectero Cloud User", EmailAddress = "*****@*****.**", CloudSyncDate = DateTime.Now }; // Is the cert encrypted? user.EncryptCertificate = !engagement.cert_key.IsNullOrEmpty(); _db.Insert(user); } } catch (DbException e) { _logger.LogError(e, "FCEJ: Persistence failed!"); throw; // Throw to let Hangfire know that the job failed. } } }
Connect(HttpContext httpContext, string nodeKey) { var errors = new Dictionary <string, object>(); // Ok, we aren't already connected. Let's go try talking to the backend and set ourselves up. var request = new RestRequest("unauth/node", Method.POST) { RequestFormat = DataFormat.Json }; var generatedPassword = PasswordUtils.GeneratePassword(24, 0); var body = new UnauthNodeAddRequest { InstallId = _identityProvider.GetGuid().ToString() }; var ownIp = await _ipResolver.Resolve(); body.Ip = ownIp.ToString(); body.Port = httpContext.Connection.LocalPort; body.Protocol = "http"; // TODO: When HTTPs support lands, use -> httpContext.Request.Protocol.ToLower() which returns things like http/1.1 (needs further parsing); body.NodeKey = nodeKey; body.AccessToken = _defaultCloudUserName + ":" + generatedPassword; // This is data about *THIS* specific system being contributed to the cloud/CRM. body.SystemData = _apm.GetAllDetails(); body.Version = AppConfig.version; // Ok, we got the user created. Everything is ready, let's send off the request. var serializedBody = JsonConvert.SerializeObject(body); _logger.LogDebug($"We will be sending: {serializedBody}"); request.AddParameter("application/json; charset=utf-8", serializedBody, ParameterType.RequestBody); // We have to ensure this user actually exists before sending off the request. // First, we need to remove any cached representation. AuthUtils.ClearUserFromCacheIfExists(_cache, _defaultCloudUserName); // Check if the cloud connect user exists already. var user = await _db.SingleAsync <User>(x => x.AuthKey == _defaultCloudUserName) ?? new User(); user.AuthKey = _defaultCloudUserName; user.PasswordSetter = generatedPassword; user.EmailAddress = _defaultCloudUserName + $"@spectero.com"; user.FullName = "Spectero Cloud Management User"; user.Roles = new List <User.Role> { User.Role.SuperAdmin }; user.Source = User.SourceTypes.SpecteroCloud; user.CloudSyncDate = DateTime.Now; user.CertKey = PasswordUtils.GeneratePassword(48, 6); var userCertBytes = _cryptoService.IssueUserChain(user.AuthKey, new[] { KeyPurposeID.IdKPClientAuth }, user.CertKey); user.Cert = Convert.ToBase64String(userCertBytes); // Checks if user existed already, or is being newly created. if (user.Id != 0L) { await _db.UpdateAsync(user); } else { user.CreatedDate = DateTime.Now; await _db.InsertAsync(user); } var response = _restClient.Execute(request); if (response.ErrorException != null) { _logger.LogError(response.ErrorException, "CC: Connect attempt to the Spectero Cloud failed!"); errors.Add(Core.Constants.Errors.FAILED_TO_CONNECT_TO_SPECTERO_CLOUD, response.ErrorMessage); await DeleteCloudUserIfExists(); return(false, errors, HttpStatusCode.ServiceUnavailable, null); } CloudAPIResponse <Node> parsedResponse = null; try { // Parse after error checking. parsedResponse = JsonConvert.DeserializeObject <CloudAPIResponse <Node> >(response.Content); } catch (JsonException e) { // The Cloud Backend fed us bogus stuff, let's bail. _logger.LogError(e, "CC: Connect attempt to the Spectero Cloud failed!"); _logger.LogDebug("Cloud API said: " + response.Content); errors.Add(Core.Constants.Errors.FAILED_TO_CONNECT_TO_SPECTERO_CLOUD, e.Message); await DeleteCloudUserIfExists(); return(false, errors, HttpStatusCode.ServiceUnavailable, parsedResponse); } // ReSharper disable once SwitchStatementMissingSomeCases switch (response.StatusCode) { case HttpStatusCode.Created: await ConfigUtils.CreateOrUpdateConfig(_db, ConfigKeys.CloudConnectStatus, true.ToString()); await ConfigUtils.CreateOrUpdateConfig(_db, ConfigKeys.CloudConnectIdentifier, parsedResponse?.result.id.ToString()); await ConfigUtils.CreateOrUpdateConfig(_db, ConfigKeys.CloudConnectNodeKey, nodeKey); break; default: // Likely a 400 or a 409, just show the response as is. errors.Add(Core.Constants.Errors.FAILED_TO_CONNECT_TO_SPECTERO_CLOUD, ""); errors.Add(Core.Constants.Errors.RESPONSE_CODE, response.StatusCode); errors.Add(Core.Constants.Errors.NODE_PERSIST_FAILED, parsedResponse?.errors); _logger.LogDebug("Cloud API said: " + response.Content); await DeleteCloudUserIfExists(); return(false, errors, HttpStatusCode.ServiceUnavailable, parsedResponse); } return(true, errors, HttpStatusCode.OK, parsedResponse); }