Activities of "maliming"

Hi,

Good catch — the post-process workaround needs to drop empty groups after filtering, otherwise a group whose features are all restricted to other providers stays in the result with an empty list. Here is the updated snippet:

[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IFeatureAppService))]
public class MyFeatureAppService : FeatureAppService
{
    public MyFeatureAppService(
        IFeatureManager featureManager,
        IFeatureDefinitionManager featureDefinitionManager,
        IOptions<FeatureManagementOptions> options)
        : base(featureManager, featureDefinitionManager, options)
    {
    }

    public override async Task<GetFeatureListResultDto> GetAsync(string providerName, string providerKey)
    {
        var result = await base.GetAsync(providerName, providerKey);

        foreach (var group in result.Groups)
        {
            var filtered = new List<FeatureDto>();
            foreach (var feature in group.Features)
            {
                var definition = await FeatureDefinitionManager.GetAsync(feature.Name);
                if (!definition.AllowedProviders.Any() || definition.AllowedProviders.Contains(providerName))
                {
                    filtered.Add(feature);
                }
            }
            group.Features = filtered;
        }

        result.Groups = result.Groups.Where(g => g.Features.Any()).ToList();

        return result;
    }
}

The framework fix in https://github.com/abpframework/abp/pull/25462 filters inline during enumeration, so the existing empty-group check in FeatureAppService.GetAsync already handles this — once you upgrade you can drop the override.

Thanks

Hi,

ABP's CAPTCHA is Google reCAPTCHA (v2 and v3) wrapped via the Owl.reCAPTCHA NuGet package. The services are registered as public DI dependencies in the Volo.Abp.Account.Pro.Public.Web module, so you can reuse them on any custom Razor Page. No middleware, no extra module to depend on (your *.Web project already references the Account Pro Web module).

SiteKey / SiteSecret / Version / Score all come from ABP settings (Abp.Account.Captcha.*), the same place the built-in Login/Register pages read from. So your custom page automatically uses the same keys you configured under Settings → Account → reCAPTCHA in the admin UI.

Here is a minimal working example. I built and verified it in a fresh MVC + Basic Theme app on ABP 10.4.

Pages/MyForm.cshtml.cs:

using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Owl.reCAPTCHA;
using Volo.Abp.Account.Public.Web.Security.Recaptcha;
using Volo.Abp.Account.Security.Recaptcha;
using Volo.Abp.Account.Settings;
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
using Volo.Abp.Settings;

namespace MyApp.Web.Pages;

public class MyFormModel : AbpPageModel
{
    [BindProperty]
    public InputModel Input { get; set; }

    public int ReCaptchaVersion { get; set; }

    public IAbpRecaptchaValidatorFactory RecaptchaValidatorFactory =>
        LazyServiceProvider.LazyGetRequiredService<IAbpRecaptchaValidatorFactory>();

    public IOptionsSnapshot<reCAPTCHAOptions> ReCaptchaOptions =>
        LazyServiceProvider.LazyGetRequiredService<IOptionsSnapshot<reCAPTCHAOptions>>();

    public virtual async Task<IActionResult> OnGetAsync()
    {
        ReCaptchaVersion = await SettingProvider.GetAsync<int>(AccountSettingNames.Captcha.Version);
        await ReCaptchaOptions.SetAsync(ReCaptchaVersion == 3 ? reCAPTCHAConsts.V3 : reCAPTCHAConsts.V2);
        return Page();
    }

    public virtual async Task<IActionResult> OnPostAsync()
    {
        ReCaptchaVersion = await SettingProvider.GetAsync<int>(AccountSettingNames.Captcha.Version);
        await ReCaptchaOptions.SetAsync(ReCaptchaVersion == 3 ? reCAPTCHAConsts.V3 : reCAPTCHAConsts.V2);

        var validator = await RecaptchaValidatorFactory.CreateAsync();
        await validator.ValidateAsync(HttpContext.Request.Form[RecaptchaValidatorBase.RecaptchaResponseKey]);

        Alerts.Success("CAPTCHA verified. Message: " + Input.Message);
        return Page();
    }

    public class InputModel
    {
        [Required]
        public string Message { get; set; }
    }
}

Pages/MyForm.cshtml:

@page
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using Owl.reCAPTCHA.v2.TagHelpers
@using Owl.reCAPTCHA.v3.TagHelpers
@using Volo.Abp.Account.Public.Web.Security.Recaptcha
@model MyApp.Web.Pages.MyFormModel

@section scripts {
    @if (Model.ReCaptchaVersion == 3)
    {
        <recaptcha-script-v3 />
        <recaptcha-script-v3-js action="myform" execute="false" />
        <script>
            $("#myForm").submit(function (e) {
                e.preventDefault();
                var form = $(this);
                grecaptcha.reExecute(function (token) {
                    form.find("input[type=hidden][data-captcha=true]").val(token);
                    form[0].submit();
                });
            });
        </script>
    }
    else
    {
        <recaptcha-script-v2 />
        <script>
            function recaptchaCallback(token) {
                $('#@RecaptchaValidatorBase.RecaptchaResponseKey').val(token);
                $('#myForm button').removeAttr("disabled");
            }
        </script>
    }
}

<h2>My Custom Form</h2>

<form id="myForm" method="post">
    <input type="hidden" data-captcha="true"
           name="@RecaptchaValidatorBase.RecaptchaResponseKey"
           id="@RecaptchaValidatorBase.RecaptchaResponseKey" />

    <div class="mb-3">
        <label asp-for="Input.Message" class="form-label"></label>
        <input asp-for="Input.Message" class="form-control" />
        <span asp-validation-for="Input.Message" class="text-danger"></span>
    </div>

    @if (Model.ReCaptchaVersion == 2)
    {
        <div class="mb-3">
            <recaptcha-div-v2 callback="recaptchaCallback" />
        </div>
        <button type="submit" class="btn btn-primary" disabled>Submit</button>
    }
    else
    {
        <button type="submit" class="btn btn-primary">Submit</button>
    }
</form>

A few notes:

  • The Pages/_ViewImports.cshtml generated by the ABP template already has @addTagHelper *, Owl.reCAPTCHA, so the recaptcha tag helpers work out of the box.
  • IAbpRecaptchaValidatorFactory.CreateAsync() returns a V2 or V3 validator based on the Abp.Account.Captcha.Version setting. You don't pick the version manually in code.
  • RecaptchaValidatorBase.RecaptchaResponseKey is the constant "g-recaptcha-response" — the standard hidden field name Google expects. Both the cshtml and the POST handler use this constant so they stay in sync.
  • await ReCaptchaOptions.SetAsync(...) is what injects SiteKey / SiteSecret from ABP settings into Owl.reCAPTCHA at request time. If you forget this, the tag helpers won't render with your configured keys.
  • ValidateAsync throws UserFriendlyException on failure, and ABP's exception filter renders it as an error alert automatically — no try/catch needed in the simple case.
  • For v3, the validator also enforces a score threshold (Abp.Account.Captcha.Score, default 0.5). If your custom page sees ScoreBelowThresholdException, lower this value under Settings → Account → reCAPTCHA → Score (e.g. 0.3), especially when testing on localhost where Google tends to return lower scores.

If you want to read the built-in reference implementations, look at these files in the Volo.Abp.Account.Pro.Public.Web module: Pages/Account/Login.cshtml(.cs), Pages/Account/Register.cshtml(.cs), and Pages/Account/ForgotPassword.cshtml(.cs). The example above is essentially the same pattern as Register.cshtml.

Thanks

Your question ticket has been refunded.

Thanks.

Hi,

Thanks for the report — you're right, this is a framework-level bug. The Feature Management UI/API should respect FeatureDefinition.AllowedProviders to match the runtime behavior in IFeatureChecker. We'll fix it in the next version: https://github.com/abpframework/abp/pull/25462

For now, you can work around it on the backend by overriding FeatureAppService.GetAsync to filter features whose AllowedProviders does not include the current providerName. Add this class to your HttpApi.Host project:

using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Features;
using Volo.Abp.FeatureManagement;

[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IFeatureAppService))]
public class MyFeatureAppService : FeatureAppService
{
    public MyFeatureAppService(
        IFeatureManager featureManager,
        IFeatureDefinitionManager featureDefinitionManager,
        IOptions<FeatureManagementOptions> options)
        : base(featureManager, featureDefinitionManager, options)
    {
    }

    public override async Task<GetFeatureListResultDto> GetAsync(string providerName, string providerKey)
    {
        var result = await base.GetAsync(providerName, providerKey);

        foreach (var group in result.Groups)
        {
            var filtered = new List<FeatureDto>();
            foreach (var feature in group.Features)
            {
                var definition = await FeatureDefinitionManager.GetAsync(feature.Name);
                if (!definition.AllowedProviders.Any() || definition.AllowedProviders.Contains(providerName))
                {
                    filtered.Add(feature);
                }
            }
            group.Features = filtered;
        }

        return result;
    }
}

The Edition Features dialog will then hide features that are restricted to other providers.

Thanks

Hi,

Before suggesting any change, I'd like to confirm a few things, because the error message you posted is raised from a very specific code path and the URL you mentioned doesn't match it.

First, one fact about how this works in AI Management: Volo.AIManagement.Workspaces.Workspace.Consume is a resource permission, granted per-workspace instance. The grants are stored in AbpResourcePermissionGrants, which is multi-tenant aware — every grant row carries a TenantId, and queries are automatically filtered by the current tenant. So a grant written under the host context (TenantId = NULL) is not visible when a tenant user checks the permission, and vice versa.

A few things would help me give you a precise answer:

1) The exact request that returns the error. Open browser DevTools → Network tab, reproduce the issue, and tell me which HTTP request returns the Authorization failed!...Consume response. The URL http://localhost:4200/ai-management/workspaces is the workspaces list page, and its route guard / list endpoint requires AIManagement.Workspaces (a regular permission), not Workspace.Consume. The Consume check only fires from endpoints that actually consume a workspace — chat completion, embeddings, OpenAI-compatible chat/model/validation paths, and similar. So something on the page is hitting one of those endpoints — most likely a child component initializing against a workspace, or a click into a specific one. Knowing the exact failing request URL will pin this down.

2) The actual grant rows, the user's tenant, and the workspace config. Please run these on your API database and share the result. Note that ResourceKey stores the workspace Id (GUID as string), not the name — so filter by Name and inspect the rows:

SELECT Id, Name, ProviderName, ProviderKey, TenantId, ResourceName, ResourceKey
FROM AbpResourcePermissionGrants
WHERE Name = 'Volo.AIManagement.Workspaces.Workspace.Consume';

SELECT Id, UserName, TenantId FROM AbpUsers WHERE UserName = 'test2';

SELECT Id, Name, RequiredPermissionName FROM AbpWorkspaces WHERE Name = 'PatientAssistant';

This tells us (a) whether the grants were really persisted — the modal showing the checkbox is not proof, (b) the TenantId on the grant rows (NULL = host, otherwise a tenant), (c) the TenantId of test2, and (d) whether the workspace has a RequiredPermissionName set. If RequiredPermissionName is not null, the check path is different — it goes through a regular permission check (IsGrantedAnyAsync) before the resource permission check, and the Consume grant is bypassed if that one is granted.

3) Where you granted the permission. Were you logged in as the host admin when you opened the workspace's permission modal, or were you operating inside a tenant (for example via Saas → Tenants → "Login with admin")? And the "Patient" role you granted to — is it a host-level role or a tenant-level role? These are separate records in AbpRoles even if they share the same name.

While we're at it, the answers to your other questions:

  • You don't need to clear the permission cache manually — it's invalidated automatically when grants are written.
  • You don't need to restart the API.
  • Logout / login matters in one specific case: the role-based provider key comes from the role claim in the token, and the user-based provider key comes from the sub claim. If you recently assigned a new role to test2 (e.g. just added Patient), test2 needs to re-login so the new role appears in the token; otherwise the role-based grant won't be matched. If you only changed the resource permission on an existing role, re-login is not needed.
  • There's no Angular- or portal-specific configuration required for AI Management workspace consumption. The issue, whatever it turns out to be, is on the API side.

Once I have the three items above I can give you the exact fix.

Thanks

It could be a network issue or a problem with the ABPIO website.

Thanks.

Answer

: )

ok

Hi,

Thanks for the clear repro. Confirmed it's a LeptonX issue with the custom-select overlay — the native <select> is hidden, so Blazorise's is-invalid class never reaches the visible proxy element, and the .invalid-feedback text stays hidden because it's no longer a sibling of the marked element.

While we ship a proper fix in the next release, please drop this small CSS into your project's global stylesheet (e.g. wwwroot/global-styles.css, or any css that gets bundled after the LeptonX theme):

.custom-select-wrapper:has(> select.form-select.is-invalid) .custom-select-display {
    border-color: var(--bs-form-invalid-border-color, #c00d49) !important;
}
.custom-select-wrapper:has(> select.form-select.is-invalid) ~ .invalid-feedback {
    display: block;
}
.custom-select-wrapper:has(> select.form-select.is-valid) .custom-select-display {
    border-color: var(--bs-form-valid-border-color, #4fbf67) !important;
}
.custom-select-wrapper:has(> select.form-select.is-valid) ~ .valid-feedback {
    display: block;
}

That gives the proxy a red border and shows the <ValidationError /> message exactly like TextInput does. We'll address it in LeptonX itself so the workaround can be removed in a future release.

Thanks.

Hi,

Both errors you see look like they are coming from the infrastructure layer, not from ABP/OpenIddict:

  • The 502 Bad Gateway is most likely returned by the AKS ingress, not by ABP. You mentioned the CI/CD team did an ingress upgrade — that lines up well with the timing.
  • The 404 WebContentNotFound is the Azure Blob Storage static-website error page. The browser request to https://<tenant1>.<app-domain> is reaching Azure Storage directly, but Storage static-website hosting can only bind one custom domain per storage account — it does not natively support wildcard tenant subdomains. So once the browser lands on <tenant1>.<app-domain>, Storage has no idea what to serve.

That also matches Project B (ABP 9): it does not use domain-based tenant resolution, so its Angular URL stays on a single fixed host and Storage is fine.

About the two extra WildcardDomainsFormat entries

You can drop them — in ABP 10 the wildcard match only compares scheme / host / port, so the signin-oidc / signout-callback-oidc entries are no-ops. They are not the cause of the 502 either; removing them just cleans the code up:

PreConfigure<AbpOpenIddictWildcardDomainOptions>(options =>
{
    options.EnableWildcardDomainSupport = true;
    options.WildcardDomainsFormat.Add(configuration["App:OpenIddictWildcardDomainsFormat"]);
});
"App": {
  "OpenIddictWildcardDomainsFormat": "https://{0}.<app-domain>"
}

Your AuthServer setup (AddDomainTenantResolver + TokenWildcardIssuerValidator + ValidIssuers) is still the correct pattern for ABP 10 — no change needed there.

What to actually fix

  1. Get the AKS ingress checked first. The recent upgrade is the most likely trigger for the 502. Verify the wildcard TLS cert binding, the host routing rule for *.<app-domain>, upstream service selectors, and that X-Forwarded-Host / X-Forwarded-Proto are still being forwarded.
  2. Put Azure Front Door (or Azure CDN) in front of the Storage account. Storage on its own cannot serve a wildcard tenant URL — Front Door can match *.<app-domain> and forward to your Storage static-website endpoint, so every tenant subdomain resolves to the same Angular SPA.

If Front Door is off the table, the practical fallback is the same pattern Project B uses — Header / Cookie / CurrentUser resolvers — so the Angular SPA stays on one fixed host.

To help us pin it down

Could you send over:

  1. A browser DevTools Network tab capture of a failing login. The response headers will show whether the 502 / 404 is coming from the ingress, from Storage, or somewhere else.
  2. AuthServer verbose logs with OpenIddict turned up. Just add this to your Serilog config temporarily:
.MinimumLevel.Override("OpenIddict", LogEventLevel.Verbose)
.MinimumLevel.Override("Microsoft.IdentityModel", LogEventLevel.Verbose)
  1. How <tenant1>.<app-domain> is currently bound — Storage custom domain, Front Door, CDN, or a plain DNS CNAME.

With those we can confirm exactly where the failure happens and tighten the recommendation.

Thanks

Showing 451 to 460 of 12915 entries
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on November 02, 2026, 06:56
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.