Пример #1
0
        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);
        }
Пример #2
0
 // 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.
                }
            }
        }
Пример #4
0
        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);
        }