Forge
csharp3407750f
1using AgentForge.Abstractions;
2using AgentForge.Plugin.Instance.UserManagement.Endpoints;
3using AgentForge.Data;
4using AgentForge.Options;
5using AgentForge.Services;
6using Microsoft.AspNetCore.Builder;
7using Microsoft.Extensions.Configuration;
8using Microsoft.Extensions.DependencyInjection;
9using Microsoft.Extensions.Options;
10
11namespace AgentForge.Plugin.Instance.UserManagement;
12
13public sealed class InstanceUserManagementForgePlugin : IForgePlugin
14{
15 public string Id => "instance-user-management";
16
17 public string DisplayName => "Instance user management (OAuth sign-in policy)";
18
19 public string Tier => "core";
20
21 public void ConfigureServices(IServiceCollection services, IConfiguration configuration) =>
22 services.AddScoped<IForgeOAuthSignInGate, InstanceOAuthSignInGate>();
23
24 public void MapEndpoints(WebApplication app)
25 {
26 var api = app.MapGroup("/api/v1");
27 api.MapInstanceAdminEndpoints();
28 }
29
30 public void RegisterFeatures(ForgeFeatureRegistry registry) => registry.Add("instance_user_management");
31
32 public void RegisterMcpTools(ForgeMcpToolRegistry registry) =>
33 ForgeInstanceAdminMcpTools.Register(registry);
34}
35
36internal sealed class InstanceOAuthSignInGate(
37 ForgeRepository repository,
38 ForgeGitHubOAuthService github,
39 IOptions<ForgeOptions> options) : IForgeOAuthSignInGate
40{
41 public async ValueTask<ForgeOAuthSignInDecision> EvaluateAsync(
42 ForgeOAuthSignInContext context,
43 CancellationToken cancellationToken)
44 {
45 if (!string.Equals(context.Provider, "github", StringComparison.OrdinalIgnoreCase))
46 return ForgeOAuthSignInDecision.Allow();
47
48 var policy = ResolvePolicy(options.Value);
49 return policy switch
50 {
51 "open" => ForgeOAuthSignInDecision.Allow(),
52 "invite_only" => EvaluateInviteOnly(context),
53 "github_org" => await EvaluateGitHubOrgAsync(context, cancellationToken).ConfigureAwait(false),
54 _ => ForgeOAuthSignInDecision.Deny($"Unknown OAuth sign-in policy '{policy}'."),
55 };
56 }
57
58 private static string ResolvePolicy(ForgeOptions forge)
59 {
60 if (!string.IsNullOrWhiteSpace(forge.OAuthSignInPolicy))
61 return forge.OAuthSignInPolicy.Trim().ToLowerInvariant();
62
63 return forge.RequireAuth ? "invite_only" : "open";
64 }
65
66 private ForgeOAuthSignInDecision EvaluateInviteOnly(ForgeOAuthSignInContext context)
67 {
68 var orgSlug = options.Value.DefaultOrgSlug?.Trim();
69 if (string.IsNullOrWhiteSpace(orgSlug))
70 return ForgeOAuthSignInDecision.Deny("invite_only policy requires FORGE_DEFAULT_ORG_SLUG.");
71
72 var org = repository.GetOrgBySlug(orgSlug);
73 if (org is null)
74 return ForgeOAuthSignInDecision.Deny($"Default organization '{orgSlug}' not found.");
75
76 var identity = repository.FindIdentityByOAuthLogin(context.Provider, context.Login);
77 if (identity is not null && repository.IsInstanceAdmin(identity.Id))
78 return ForgeOAuthSignInDecision.Allow();
79
80 if (identity is null || !repository.IsOrgMember(org.Id, identity.Id))
81 return ForgeOAuthSignInDecision.Deny("Sign-in is invite-only. Ask an org admin to add you first.");
82
83 return ForgeOAuthSignInDecision.Allow();
84 }
85
86 private async Task<ForgeOAuthSignInDecision> EvaluateGitHubOrgAsync(
87 ForgeOAuthSignInContext context,
88 CancellationToken cancellationToken)
89 {
90 var githubOrg = options.Value.OAuthGitHubOrg?.Trim();
91 if (string.IsNullOrWhiteSpace(githubOrg))
92 return ForgeOAuthSignInDecision.Deny("github_org policy requires FORGE_OAUTH_GITHUB_ORG.");
93
94 if (string.IsNullOrWhiteSpace(context.GitHubAccessToken))
95 return ForgeOAuthSignInDecision.Deny("GitHub access token missing for org membership check.");
96
97 var isMember = await github.IsMemberOfOrgAsync(
98 githubOrg,
99 context.Login,
100 context.GitHubAccessToken,
101 cancellationToken).ConfigureAwait(false);
102
103 return isMember
104 ? ForgeOAuthSignInDecision.Allow()
105 : ForgeOAuthSignInDecision.Deny($"GitHub user is not a member of organization '{githubOrg}'.");
106 }
107}
108
View only · write via MCP/CIDE