/// <summary> /// Associates the current employee and terminal in <paramref name="context"/>. /// </summary> /// <param name="requestContext">The request context.</param> /// <remarks>If employee cannot hold multiple sessions on different terminals <see cref="UserAuthorizationException"/> is thrown.</remarks> private static void CreateStaffSession(RequestContext requestContext) { ICommercePrincipal principal = requestContext.GetPrincipal(); string staffId = principal.UserId; // when no terminal is present, there is no need to enforce single terminal use if (principal.IsTerminalAgnostic) { throw new InvalidOperationException("A new session can only be created when channel and terminal are present in the request context."); } if (string.IsNullOrWhiteSpace(staffId)) { throw new InvalidOperationException("AuthorizeMultipleTerminalUse can only be performed if user is known. Missing UserId from CommercePrincipal."); } if (!principal.IsEmployee) { throw new InvalidOperationException("AuthorizeMultipleTerminalUse can only be performed if user is employee."); } GetEmployeeAuthorizedOnStoreDataRequest employeeDataRequest = new GetEmployeeAuthorizedOnStoreDataRequest(staffId); Employee employee = requestContext.Execute <SingleEntityDataServiceResponse <Employee> >(employeeDataRequest).Entity; // managers are not required to keep track of sessions if (employee.Permissions.HasManagerPrivileges) { return; } // helper does most of the hard lifting regarding fallback logic and real time communication StaffRealTimeSecurityValidationHelper staffSecurityHelper = StaffRealTimeSecurityValidationHelper.Create( requestContext, SecurityVerificationType.Authorization, staffId, principal.ChannelId, principal.TerminalId, password: string.Empty); // executes the common set of steps that performs the session creation flow in either HQ, local DB, or HQ with local DB fallback ExecuteWorkWithLocalFallback( staffSecurityHelper.GetSecurityVerificationConfiguration(), staffSecurityHelper, () => { return(CreateEmployeeSessionLocalDatabase(requestContext, employee)); }); // we always need to create the session on local DB so we can enforce session is open, // so if the configuration is set to use RealTime service, also creates the session on local DB if (staffSecurityHelper.GetSecurityVerificationConfiguration() == LogOnConfiguration.RealTimeService) { CreateEmployeeSessionLocalDatabase(requestContext, employee); } RetailLogger.Log.CrtServicesStaffAuthorizationServiceUserSessionStarted(staffId, requestContext.GetTerminal().TerminalId); }
/// <summary> /// Authorizes the retail staff. /// </summary> /// <param name="staffAuthorizationRequest">The retail staff authorization request.</param> /// <returns>The service response.</returns> private static Response AuthorizeStaff(StaffAuthorizationServiceRequest staffAuthorizationRequest) { RequestContext context = staffAuthorizationRequest.RequestContext; ICommercePrincipal principal = context.GetPrincipal(); string staffId = string.IsNullOrWhiteSpace(staffAuthorizationRequest.StaffId) ? principal.UserId : staffAuthorizationRequest.StaffId; long? channelId = principal.IsChannelAgnostic ? null : (long?)principal.ChannelId; long? terminalRecordId = principal.IsTerminalAgnostic ? null : (long?)principal.TerminalId; RetailOperation operation = staffAuthorizationRequest.RetailOperation; Employee employee; if (string.IsNullOrWhiteSpace(staffId)) { throw new UserAuthorizationException(SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_AuthorizationFailed, "UserId is missing from principal."); } if (channelId.HasValue && !principal.IsTerminalAgnostic && !terminalRecordId.HasValue) { throw new UserAuthorizationException(SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_AuthorizationFailed, "When channel identififer is provided on principal, terminal record identfier must also be."); } StaffRealTimeSecurityValidationHelper staffSecurityHelper = StaffRealTimeSecurityValidationHelper.Create( context, SecurityVerificationType.Authorization, staffId, channelId, terminalRecordId, password: string.Empty); // we can only check values against database if the principal is bound to a channel if (!principal.IsChannelAgnostic) { VerifyThatOrgUnitIsPublished(context, channelId.Value); } // for authorization, we always want to go to local DB, we only go to headquarters if we don't have a channel // to access the DB LogOnConfiguration logOnConfiguration = principal.IsChannelAgnostic ? LogOnConfiguration.RealTimeService : LogOnConfiguration.LocalDatabase; // authorize employee based on configuration employee = ExecuteWorkWithLocalFallback( logOnConfiguration, staffSecurityHelper, () => { return(AuthorizeEmployeeLocalDatabase(context, staffSecurityHelper.StaffId, staffAuthorizationRequest.EnforceSessionToBeOpened)); }); // Validates whether the staff can perform the requested operation ValidateEmployeePermissionForOperation(context, operation, employee); return(new StaffAuthorizationServiceResponse(employee)); }
/// <summary> /// Determines whether two helper instances are equivalent. /// </summary> /// <param name="helperFromContext">The helper available from the context.</param> /// <param name="newHelper">The new helper.</param> /// <returns>true if the specified object is equal to the current object; otherwise, false.</returns> private static bool AreEquivalent(StaffRealTimeSecurityValidationHelper helperFromContext, StaffRealTimeSecurityValidationHelper newHelper) { // all parameters must be same // passwords are same or newHelper does not care about authentication // verification type provided in the helper from context must contain the one required in the newHelper return(helperFromContext.StaffId.Equals(newHelper.StaffId, StringComparison.OrdinalIgnoreCase) && helperFromContext.ChannelId == newHelper.ChannelId && helperFromContext.TerminalRecordId == newHelper.TerminalRecordId && helperFromContext.verificationType.HasFlag(newHelper.verificationType) && (newHelper.verificationType == SecurityVerificationType.Authorization || helperFromContext.password == newHelper.password)); }
/// <summary> /// Creates a new instance of the <see cref="StaffRealTimeSecurityValidationHelper"/> class if not already available in the context. /// </summary> /// <param name="context">The request context.</param> /// <param name="verificationType">The verification type to be performed.</param> /// <param name="staffId">The staff identifier.</param> /// <param name="channelId">The channel identifier.</param> /// <param name="terminalRecordId">The terminal record identifier.</param> /// <param name="password">The employee password.</param> /// <returns>The staff security helper instance created.</returns> public static StaffRealTimeSecurityValidationHelper Create(RequestContext context, SecurityVerificationType verificationType, string staffId, long?channelId, long?terminalRecordId, string password) { ThrowIf.Null(context, "context"); ThrowIf.NullOrWhiteSpace(staffId, "staffId"); if (verificationType.HasFlag(SecurityVerificationType.Authentication) && string.IsNullOrWhiteSpace(password)) { throw new ArgumentException("Password must be provided if authentication is required.", "password"); } if (channelId.HasValue && context.GetPrincipal() != null && !context.GetPrincipal().IsTerminalAgnostic&& !terminalRecordId.HasValue) { throw new ArgumentException("Terminal record identifier must be provided whenever channel identifier is provider.", "terminalRecordId"); } StaffRealTimeSecurityValidationHelper helper = new StaffRealTimeSecurityValidationHelper(context, verificationType, staffId, channelId, terminalRecordId, password); StaffRealTimeSecurityValidationHelper helperFromContext = context.GetProperty(StaffSecurityHelperContextCacheKey) as StaffRealTimeSecurityValidationHelper; // within the same context, authentication and authorization validations might occur separatelly // to avoid the penalty of two transaction service calls, we store the result for the context // and check if we can use the stored result for subsequent calls within the same context if (helperFromContext != null) { // if helper is available from context and is equivalent to this one (based on parameters) if (AreEquivalent(helperFromContext, helper)) { // then use cached results from the one from context helper.realTimeStaffVerificationEmployee = helperFromContext.realTimeStaffVerificationEmployee; } } else { // first time staff helper is used, keep it on context context.SetProperty(StaffSecurityHelperContextCacheKey, helper); } return(helper); }
/// <summary> /// Executes work based against RTS or Local Database based on <paramref name="authorizationConfiguration"/>. /// If <paramref name="authorizationConfiguration"/> is <see cref="LogOnConfiguration.RealTimeService"/> then /// business logic is performed using RealTime service. If RealTime is not available and <see cref="Employee"/> is allowed to fallback to local database checks, then /// the delegate <paramref name="localDatabaseAction"/> will be executed. In case <paramref name="authorizationConfiguration"/> is <see cref="LogOnConfiguration.LocalDatabase"/>, /// then authorization is performed solely using local database. /// </summary> /// <param name="authorizationConfiguration">Indicates whether the logic should run, against local database or against the real time service.</param> /// <param name="staffSecurityHelper">The staff security validation helper instance.</param> /// <param name="localDatabaseAction">A delegate for the local execution of the authorization logic.</param> /// <returns>The employee.</returns> private static Employee ExecuteWorkWithLocalFallback(LogOnConfiguration authorizationConfiguration, StaffRealTimeSecurityValidationHelper staffSecurityHelper, Func <Employee> localDatabaseAction) { Employee employee = null; string errorMessage; switch (authorizationConfiguration) { case LogOnConfiguration.RealTimeService: try { employee = staffSecurityHelper.VerifyEmployeeRealTimeService(() => { return(localDatabaseAction()); }); } catch (HeadquarterTransactionServiceException exception) { throw new UserAuthorizationException(SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_HeadquarterTransactionServiceMethodCallFailure, exception, exception.Message) { LocalizedMessage = exception.LocalizedMessage }; } catch (CommerceException exception) { // The error code to be persisted throw new UserAuthorizationException( SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_HeadquarterCommunicationFailure, exception, exception.Message); } catch (Exception exception) { // any exceptions that might happen will cause the authorization to fail throw new UserAuthorizationException(SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_AuthorizationFailed, exception, exception.Message); } break; case LogOnConfiguration.LocalDatabase: employee = localDatabaseAction(); break; default: errorMessage = string.Format( CultureInfo.InvariantCulture, "The authorization configuration value '{0}' is not supported.", authorizationConfiguration); throw new NotSupportedException(errorMessage); } return(employee); }