feat(auth): add organization creation control plane slice

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-09-24 14:43:17 +08:00
parent 39a7081ff4
commit 52e4e052a1
15 changed files with 544 additions and 0 deletions

View file

@ -0,0 +1,9 @@
## Boundary
平台管理 API 只处理创建。租户内读写继续由 Organization Membership 和组织角色授权,平台 `SUPER_ADMIN` 不在租户 API 获得隐式通行权。
控制器做请求校验和平台角色校验;应用服务编排现有聚合及仓储。保存 Organization、Membership、Role Binding 和审计均位于同一事务。slug 使用现有模型校验和数据库唯一约束;并发重名由现有 JPA adapter 映射为 409。
审计只持久化组织 ID、操作者、动作、成功结果和 requestId,不记录用户邮箱、密钥或请求体。初始 owner 的 Membership 来源为 MANUAL;创建成功后 Organization authority version 为 1。回滚无需删除数据:本批只有新 API,停用入口或回退应用版本即可,已创建组织保留。
本批只用现有 schema,不新增 Flyway migration。后续成员/角色管理和 Login Connection 控制面分别另行交付、验证与合并。

View file

@ -0,0 +1,19 @@
## Why
R1-B 的只读 Organization API 已合入,但平台管理员还不能创建测试 Organization 并指定初始负责人。没有这一入口,后续成员管理与 Login Connection 控制面无法独立验收。
## What Changes
- 仅 `SUPER_ADMIN` 可创建 Organization,并指定一个已存在、ACTIVE、非系统、未合并的平台账号为初始 owner。
- 在一个事务中创建 Organization、ACTIVE Membership、`ORG_OWNER` role binding 和组织审计记录。
- 返回新组织的基本标识和状态;平台管理员不会自动取得该组织成员身份。
## Non-goals
- 不增加成员、角色、域名或 Login Connection 的管理接口或页面。
- 不开放企业登录、自动识别组织、SCIM、目录同步或 Namespace 授权。
- 不修改现有数据库 schema、默认登录模式或公开 Provider 行为。
## Impact
新增 `POST /api/v1/admin/organizations` 与相应 OpenAPI 类型;使用现有 Organization、Membership、Role Binding 和 audit_log 表。参见 [总体分批计划](../enterprise-identity-platform/rollout-plan.md)。

View file

@ -0,0 +1,20 @@
## ADDED Requirements
### Requirement: Platform administrator creates an organization with an initial owner
系统 SHALL 只允许 `SUPER_ADMIN` 使用唯一 slug、显示名和一个已有且可用的平台账号创建 Organization。创建 SHALL 在单个事务中生成 ACTIVE Organization、该账号的 ACTIVE Membership、`ORG_OWNER` role binding 和组织审计记录。创建者不得因其平台角色自动成为组织成员。
#### Scenario: Successful creation
- **WHEN** 平台管理员指定有效 slug、显示名和 ACTIVE 的初始 owner
- **THEN** 系统创建以上四类记录,返回组织 ID 与状态,并使初始 owner 可以访问组织详情
#### Scenario: Invalid owner or duplicate slug
- **WHEN** 初始 owner 不存在、禁用、为系统账号、已合并,或 slug 已被占用
- **THEN** 系统返回对应的 404 或 409,且不留下部分 Organization、Membership、Role Binding 或成功审计
#### Scenario: Platform role is not tenant membership
- **WHEN** 创建者仅持有 `SUPER_ADMIN` 平台角色而不是新组织成员
- **THEN** 其访问新组织的租户详情接口被拒绝

View file

@ -0,0 +1,11 @@
## Implementation
- [x] 新增平台级 Organization 创建 API,验证初始 owner,原子写入组织、成员、角色和审计。
- [x] 保持平台角色与租户成员权限分离,不开放企业登录与组织同步。
- [x] 从当前分支应用运行结果生成并提交 OpenAPI 类型。
## Verification
- [x] 集成测试覆盖成功、仅 owner 可进入组织详情、非管理员、禁用/不存在 owner 和重复 slug。
- [x] 完整后端测试、Web typecheck/lint 和 OpenSpec strict validation 通过。
- [ ] 检查最终 diff、敏感信息、精确 SHA 与预览环境;经人工评审后再提 PR/合并。

View file

@ -0,0 +1,42 @@
package com.iflytek.skillhub.controller.admin;
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
import com.iflytek.skillhub.controller.BaseApiController;
import com.iflytek.skillhub.dto.ApiResponse;
import com.iflytek.skillhub.dto.ApiResponseFactory;
import com.iflytek.skillhub.dto.OrganizationCreateRequest;
import com.iflytek.skillhub.dto.OrganizationCreateResponse;
import com.iflytek.skillhub.service.PlatformOrganizationCreateAppService;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/admin/organizations")
@PreAuthorize("hasRole('SUPER_ADMIN')")
public class PlatformOrganizationController extends BaseApiController {
private final PlatformOrganizationCreateAppService appService;
public PlatformOrganizationController(
PlatformOrganizationCreateAppService appService,
ApiResponseFactory responseFactory
) {
super(responseFactory);
this.appService = appService;
}
@PostMapping
@Operation(operationId = "createOrganization", summary = "Create an organization with an initial owner")
public ApiResponse<OrganizationCreateResponse> create(
@Valid @RequestBody OrganizationCreateRequest request,
@AuthenticationPrincipal PlatformPrincipal principal
) {
return ok("response.success.created", appService.create(request, principal.userId()));
}
}

View file

@ -0,0 +1,11 @@
package com.iflytek.skillhub.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record OrganizationCreateRequest(
@NotBlank @Size(max = 64) String slug,
@NotBlank @Size(max = 128) String displayName,
@NotBlank @Size(max = 128) String initialOwnerUserId
) {
}

View file

@ -0,0 +1,21 @@
package com.iflytek.skillhub.dto;
import com.iflytek.skillhub.domain.organization.Organization;
import com.iflytek.skillhub.domain.organization.OrganizationStatus;
public record OrganizationCreateResponse(
String id,
String slug,
String displayName,
OrganizationStatus status,
long authorityVersion
) {
public static OrganizationCreateResponse from(Organization organization) {
return new OrganizationCreateResponse(
organization.getId(),
organization.getSlug(),
organization.getDisplayName(),
organization.getStatus(),
organization.getAuthorityVersion());
}
}

View file

@ -0,0 +1,80 @@
package com.iflytek.skillhub.service;
import com.iflytek.skillhub.domain.audit.AuditLogService;
import com.iflytek.skillhub.domain.organization.MembershipSourceType;
import com.iflytek.skillhub.domain.organization.Organization;
import com.iflytek.skillhub.domain.organization.OrganizationMembership;
import com.iflytek.skillhub.domain.organization.OrganizationMembershipRepository;
import com.iflytek.skillhub.domain.organization.OrganizationRepository;
import com.iflytek.skillhub.domain.organization.OrganizationRole;
import com.iflytek.skillhub.domain.organization.OrganizationRoleBinding;
import com.iflytek.skillhub.domain.organization.OrganizationRoleBindingRepository;
import com.iflytek.skillhub.domain.shared.exception.DomainConflictException;
import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException;
import com.iflytek.skillhub.domain.user.UserAccount;
import com.iflytek.skillhub.domain.user.UserAccountRepository;
import com.iflytek.skillhub.dto.OrganizationCreateRequest;
import com.iflytek.skillhub.dto.OrganizationCreateResponse;
import com.iflytek.skillhub.observability.RequestIdAccessor;
import java.time.Clock;
import java.time.Instant;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/** Creates the tenant boundary and its first owner as one transaction. */
@Service
public class PlatformOrganizationCreateAppService {
private final OrganizationRepository organizationRepository;
private final OrganizationMembershipRepository membershipRepository;
private final OrganizationRoleBindingRepository roleBindingRepository;
private final UserAccountRepository userAccountRepository;
private final AuditLogService auditLogService;
private final RequestIdAccessor requestIdAccessor;
private final Clock clock;
public PlatformOrganizationCreateAppService(
OrganizationRepository organizationRepository,
OrganizationMembershipRepository membershipRepository,
OrganizationRoleBindingRepository roleBindingRepository,
UserAccountRepository userAccountRepository,
AuditLogService auditLogService,
RequestIdAccessor requestIdAccessor,
Clock clock
) {
this.organizationRepository = organizationRepository;
this.membershipRepository = membershipRepository;
this.roleBindingRepository = roleBindingRepository;
this.userAccountRepository = userAccountRepository;
this.auditLogService = auditLogService;
this.requestIdAccessor = requestIdAccessor;
this.clock = clock;
}
@Transactional
public OrganizationCreateResponse create(OrganizationCreateRequest request, String actorUserId) {
UserAccount owner = userAccountRepository.findById(request.initialOwnerUserId())
.orElseThrow(() -> new DomainNotFoundException("error.organization.owner.not-found"));
if (!owner.isActive() || owner.isSystemAccount() || owner.getMergedToUserId() != null) {
throw new DomainConflictException("error.organization.owner.not-eligible");
}
organizationRepository.findBySlug(request.slug()).ifPresent(existing -> {
throw new DomainConflictException("error.organization.slug.conflict");
});
Instant now = clock.instant();
Organization organization = organizationRepository.save(Organization.create(
request.slug(), request.displayName(), actorUserId, now));
OrganizationMembership membership = OrganizationMembership.provisioned(
organization.getId(), MembershipSourceType.MANUAL, owner.getId(),
owner.getDisplayName(), owner.getEmail(), now);
membership.activate(owner.getId(), now);
membershipRepository.save(membership);
roleBindingRepository.save(OrganizationRoleBinding.grant(
organization.getId(), owner.getId(), OrganizationRole.ORG_OWNER, actorUserId, now));
organization.recordRoleBindingChange(now);
organizationRepository.save(organization);
auditLogService.recordOrganizationCreated(actorUserId, organization.getId(), requestIdAccessor.current());
return OrganizationCreateResponse.from(organization);
}
}

View file

@ -6,6 +6,11 @@ response.success.deleted=Deleted successfully
response.success.published=Published successfully
response.success.revoked=Revoked successfully
response.success.health=Service is up
error.organization.field.required=Organization {0} is required
error.organization.slug.invalid=Organization slug must be 2-64 lowercase letters, numbers, or hyphens
error.organization.slug.conflict=An organization with this slug already exists
error.organization.owner.not-found=Initial organization owner was not found
error.organization.owner.not-eligible=Initial organization owner must be an active user account
validation.namespace.slug.notBlank=Slug cannot be blank
validation.namespace.slug.size=Slug must be between 2 and 64 characters

View file

@ -6,6 +6,11 @@ response.success.deleted=Успешно удалено
response.success.published=Успешно опубликовано
response.success.revoked=Успешно отозвано
response.success.health=Сервис работает
error.organization.field.required=Поле организации {0} обязательно
error.organization.slug.invalid=Идентификатор организации: 2–64 строчные буквы, цифры или дефисы
error.organization.slug.conflict=Организация с таким идентификатором уже существует
error.organization.owner.not-found=Первоначальный владелец организации не найден
error.organization.owner.not-eligible=Первоначальный владелец должен иметь активную учётную запись
validation.namespace.slug.notBlank=Slug не может быть пустым
validation.namespace.slug.size=Slug должен содержать от 2 до 64 символов
validation.namespace.displayName.notBlank=Отображаемое имя не может быть пустым

View file

@ -6,6 +6,11 @@ response.success.deleted=删除成功
response.success.published=发布成功
response.success.revoked=撤销成功
response.success.health=服务正常
error.organization.field.required=组织的{0}不能为空
error.organization.slug.invalid=组织标识须为 2 至 64 位小写字母、数字或连字符
error.organization.slug.conflict=该组织标识已存在
error.organization.owner.not-found=找不到指定的初始组织负责人
error.organization.owner.not-eligible=初始组织负责人必须是正常启用的用户账号
validation.namespace.slug.notBlank=slug 不能为空
validation.namespace.slug.size=slug 长度必须在 2 到 64 个字符之间

View file

@ -0,0 +1,215 @@
package com.iflytek.skillhub.controller.admin;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.iflytek.skillhub.auth.device.DeviceAuthService;
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
import com.iflytek.skillhub.domain.audit.AuditLog;
import com.iflytek.skillhub.domain.organization.Organization;
import com.iflytek.skillhub.domain.organization.OrganizationMembership;
import com.iflytek.skillhub.domain.organization.OrganizationRole;
import com.iflytek.skillhub.domain.organization.OrganizationRoleBinding;
import com.iflytek.skillhub.domain.user.UserAccount;
import com.iflytek.skillhub.domain.user.UserStatus;
import jakarta.persistence.EntityManager;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@Transactional
class PlatformOrganizationControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private EntityManager entityManager;
@MockBean
private DeviceAuthService deviceAuthService;
@Test
void openApiIncludesCreateOrganization() throws Exception {
String specification = mockMvc.perform(get("/v3/api-docs"))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString();
assertThat(specification).contains("\"/api/v1/admin/organizations\"");
assertThat(specification).contains("\"createOrganization\"");
String exportPath = System.getProperty("skillhub.openapi.export.path");
if (exportPath != null) {
Files.writeString(Path.of(exportPath), specification);
}
}
@Test
void create_assignsOnlyTheChosenInitialOwnerAndRecordsAudit() throws Exception {
persistUser("platform-admin");
persistUser("initial-owner");
mockMvc.perform(post("/api/v1/admin/organizations")
.with(csrf())
.with(authentication(auth("platform-admin", "SUPER_ADMIN")))
.contentType(MediaType.APPLICATION_JSON)
.content(request("team-one", "Team One", "initial-owner")))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.slug").value("team-one"))
.andExpect(jsonPath("$.data.status").value("ACTIVE"))
.andExpect(jsonPath("$.data.authorityVersion").value(1));
entityManager.flush();
entityManager.clear();
Organization organization = entityManager.createQuery(
"select o from Organization o where o.slug = :slug", Organization.class)
.setParameter("slug", "team-one")
.getSingleResult();
List<OrganizationMembership> memberships = entityManager.createQuery(
"select m from OrganizationMembership m where m.organizationId = :id",
OrganizationMembership.class)
.setParameter("id", organization.getId())
.getResultList();
assertThat(memberships).singleElement()
.satisfies(member -> {
assertThat(member.getUserId()).isEqualTo("initial-owner");
assertThat(member.getStatus().name()).isEqualTo("ACTIVE");
});
List<OrganizationRoleBinding> roles = entityManager.createQuery(
"select b from OrganizationRoleBinding b where b.organizationId = :id",
OrganizationRoleBinding.class)
.setParameter("id", organization.getId())
.getResultList();
assertThat(roles).singleElement()
.satisfies(binding -> {
assertThat(binding.getUserId()).isEqualTo("initial-owner");
assertThat(binding.getRole()).isEqualTo(OrganizationRole.ORG_OWNER);
});
List<AuditLog> audits = entityManager.createQuery(
"select a from AuditLog a where a.organizationId = :id", AuditLog.class)
.setParameter("id", organization.getId())
.getResultList();
assertThat(audits).singleElement()
.satisfies(audit -> {
assertThat(audit.getActorUserId()).isEqualTo("platform-admin");
assertThat(audit.getAction()).isEqualTo("ORGANIZATION_CREATED");
assertThat(audit.getTargetRef()).isEqualTo(organization.getId());
assertThat(audit.getResult()).isEqualTo("SUCCESS");
});
mockMvc.perform(get("/api/v1/organizations/{id}", organization.getId())
.with(authentication(auth("platform-admin", "SUPER_ADMIN"))))
.andExpect(status().isForbidden());
mockMvc.perform(get("/api/v1/organizations/{id}", organization.getId())
.with(authentication(auth("initial-owner"))))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.roles[0]").value("ORG_OWNER"));
}
@Test
void create_rejectsNonAdminBeforeWriting() throws Exception {
persistUser("initial-owner");
mockMvc.perform(post("/api/v1/admin/organizations")
.with(csrf())
.with(authentication(auth("member")))
.contentType(MediaType.APPLICATION_JSON)
.content(request("no-access", "No Access", "initial-owner")))
.andExpect(status().isForbidden());
assertThat(organizationCount()).isZero();
}
@Test
void create_rejectsIneligibleOwnerAndDuplicateSlug() throws Exception {
persistUser("platform-admin");
persistUser("initial-owner");
UserAccount disabled = persistUser("disabled-owner");
disabled.setStatus(UserStatus.DISABLED);
UserAccount merged = persistUser("merged-owner");
merged.setMergedToUserId("initial-owner");
entityManager.persist(UserAccount.systemAccount(
"system-owner", "System", null, ""));
entityManager.flush();
mockMvc.perform(post("/api/v1/admin/organizations")
.with(csrf())
.with(authentication(auth("platform-admin", "SUPER_ADMIN")))
.contentType(MediaType.APPLICATION_JSON)
.content(request("disabled-team", "Disabled Team", "disabled-owner")))
.andExpect(status().isConflict());
mockMvc.perform(post("/api/v1/admin/organizations")
.with(csrf())
.with(authentication(auth("platform-admin", "SUPER_ADMIN")))
.contentType(MediaType.APPLICATION_JSON)
.content(request("missing-team", "Missing Team", "missing-owner")))
.andExpect(status().isNotFound());
for (String ineligible : List.of("merged-owner", "system-owner")) {
mockMvc.perform(post("/api/v1/admin/organizations")
.with(csrf())
.with(authentication(auth("platform-admin", "SUPER_ADMIN")))
.contentType(MediaType.APPLICATION_JSON)
.content(request(ineligible, "Ineligible Team", ineligible)))
.andExpect(status().isConflict());
}
assertThat(organizationCount()).isZero();
mockMvc.perform(post("/api/v1/admin/organizations")
.with(csrf())
.with(authentication(auth("platform-admin", "SUPER_ADMIN")))
.contentType(MediaType.APPLICATION_JSON)
.content(request("team-one", "Team One", "initial-owner")))
.andExpect(status().isOk());
mockMvc.perform(post("/api/v1/admin/organizations")
.with(csrf())
.with(authentication(auth("platform-admin", "SUPER_ADMIN")))
.contentType(MediaType.APPLICATION_JSON)
.content(request("team-one", "Other Name", "initial-owner")))
.andExpect(status().isConflict());
assertThat(organizationCount()).isEqualTo(1);
}
private UserAccount persistUser(String userId) {
UserAccount user = new UserAccount(userId, userId, userId + "@example.com", "");
entityManager.persist(user);
entityManager.flush();
return user;
}
private long organizationCount() {
entityManager.flush();
return entityManager.createQuery("select count(o) from Organization o", Long.class)
.getSingleResult();
}
private String request(String slug, String displayName, String owner) {
return "{\"slug\":\"" + slug + "\",\"displayName\":\"" + displayName
+ "\",\"initialOwnerUserId\":\"" + owner + "\"}";
}
private UsernamePasswordAuthenticationToken auth(String userId, String... roles) {
Set<String> platformRoles = Set.of(roles);
PlatformPrincipal principal = new PlatformPrincipal(
userId, userId, userId + "@example.com", "", "github", platformRoles);
List<SimpleGrantedAuthority> authorities = platformRoles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.toList();
return new UsernamePasswordAuthenticationToken(principal, null, authorities);
}
}

View file

@ -30,6 +30,15 @@ public class AuditLog {
@Column(name = "target_id")
private Long targetId;
@Column(name = "organization_id", length = 64)
private String organizationId;
@Column(name = "target_ref", length = 128)
private String targetRef;
@Column(name = "result", length = 32)
private String result;
@Column(name = "request_id", length = 64)
private String requestId;
@ -68,11 +77,27 @@ public class AuditLog {
this.createdAt = createdAt;
}
public static AuditLog organizationSuccess(String actorUserId,
String action,
String organizationId,
String requestId,
Instant createdAt) {
AuditLog log = new AuditLog(actorUserId, action, "ORGANIZATION", null,
requestId, null, null, null, createdAt);
log.organizationId = organizationId;
log.targetRef = organizationId;
log.result = "SUCCESS";
return log;
}
public Long getId() { return id; }
public String getActorUserId() { return actorUserId; }
public String getAction() { return action; }
public String getTargetType() { return targetType; }
public Long getTargetId() { return targetId; }
public String getOrganizationId() { return organizationId; }
public String getTargetRef() { return targetRef; }
public String getResult() { return result; }
public String getRequestId() { return requestId; }
public String getClientIp() { return clientIp; }
public String getUserAgent() { return userAgent; }

View file

@ -42,4 +42,16 @@ public class AuditLogService {
createdAt
));
}
@Transactional
public AuditLog recordOrganizationCreated(String actorUserId,
String organizationId,
String requestId) {
return auditLogRepository.save(AuditLog.organizationSuccess(
actorUserId,
"ORGANIZATION_CREATED",
organizationId,
requestId,
Instant.now(clock)));
}
}

View file

@ -2380,6 +2380,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/admin/organizations": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Create an organization with an initial owner */
post: operations["createOrganization"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/admin/namespaces/{slug}/unfreeze": {
parameters: {
query?: never;
@ -5768,6 +5785,29 @@ export interface components {
id?: number;
status?: string;
};
OrganizationCreateRequest: {
slug: string;
displayName: string;
initialOwnerUserId: string;
};
ApiResponseOrganizationCreateResponse: {
/** Format: int32 */
code?: number;
msg?: string;
data?: components["schemas"]["OrganizationCreateResponse"];
/** Format: date-time */
timestamp?: string;
requestId?: string;
};
OrganizationCreateResponse: {
id?: string;
slug?: string;
displayName?: string;
/** @enum {string} */
status?: "ACTIVE" | "SUSPENDED" | "DECOMMISSIONED";
/** Format: int64 */
authorityVersion?: number;
};
AdminNamespaceDetailResponse: {
/** Format: int64 */
id?: number;
@ -12022,6 +12062,30 @@ export interface operations {
};
};
};
createOrganization: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["OrganizationCreateRequest"];
};
};
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"*/*": components["schemas"]["ApiResponseOrganizationCreateResponse"];
};
};
};
};
unfreezeNamespace_2: {
parameters: {
query?: never;