Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 0 additions & 32 deletions backend/cmd/server/bootstrap/01-default-resources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,38 +102,6 @@ resources:
- name: System
handle: system
description: System resource
- name: Organization Unit
handle: ou
description: Organization unit resource
parent: system
actions:
- name: View
handle: view
description: Read-only access to organization units
- name: User
handle: user
description: User resource
parent: system
actions:
- name: View
handle: view
description: Read-only access to users
- name: Group
handle: group
description: Group resource
parent: system
actions:
- name: View
handle: view
description: Read-only access to groups
- name: User Type
handle: usertype
description: User type resource
parent: system
actions:
- name: View
handle: view
description: Read-only access to user types
---
resource_type: flow
id: 01900000-0000-7000-8000-000000000061
Expand Down
2 changes: 1 addition & 1 deletion docs/content/deployment/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -926,7 +926,7 @@ Controls server-wide security behavior that is not specific to any single authen
| Setting | Default | Description |
|---------|---------|-------------|
| `server.security.jwks_cache_ttl` | `300` | JWKS cache TTL in seconds. Applies to every JWKS consumer in the server (trusted issuer validation, federated OIDC authenticators such as Google, and so on). Fetched signing keys are reused from the in-process cache for this duration before being re-fetched. Plan external-server key rotations with at least this much overlap. Set to `0` to disable caching |
| `server.security.system_permission_prefix` | `""` (empty) | Prefix for system permission strings used in API authorization. When empty, permissions use their base names (for example, `system`, `system:ou`). When set, the prefix is prepended to every system permission (for example, `mgmt:system`, `mgmt:system:ou`). If you set a prefix, update the Console scopes to match. Changes require a server restart |
| `server.security.system_permission_prefix` | `""` (empty) | Prefix for system permission strings used in API authorization. When empty, permissions use their base names (for example, `system`). When set, the prefix is prepended to every system permission (for example, `mgmt:system`). If you set a prefix, update the Console scopes to match. Changes require a server restart |
| `server.security.direct_auth_secret` | `""` (empty) | Secret that gates the Direct API authentication endpoints (`/auth/**` and `/register/passkey/**`) and protected AuthZEN access endpoints (`/access/**`). AuthZEN discovery (`/.well-known/authzen-configuration`) remains public. The protected endpoints are **secure by default**. While this is empty they are blocked with `401`. When set, callers must send the value in the `Direct-Auth-Secret` header; a missing or incorrect value is rejected with `401`. See [Integration Models](../../key-concepts/authentication/integration-models#direct-api) |

:::tip
Expand Down
4 changes: 2 additions & 2 deletions docs/content/guides/trusted-issuer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ window.__THUNDERID_RUNTIME_CONFIG__ = {
http_only: false,
public_url: "https://auth.example.com",
client_id: "FEDERATED_CONSOLE",
scopes: ['openid', 'profile', 'email', 'group', 'ou', 'system', 'system:user', 'system:group', 'system:ou:view', 'system:usertype:view'],
scopes: ['openid', 'profile', 'email', 'group', 'ou', 'system'],
type: "generic",
},
};
Expand Down Expand Up @@ -158,7 +158,7 @@ configuration:
httpOnly: false
publicUrl: "https://auth.example.com"
clientId: "FEDERATED_CONSOLE"
scopes: "['openid', 'profile', 'email', 'group', 'ou', 'system', 'system:user', 'system:group', 'system:ou:view', 'system:usertype:view']"
scopes: "['openid', 'profile', 'email', 'group', 'ou', 'system']"
type: "generic"
```

Expand Down
12 changes: 1 addition & 11 deletions frontend/apps/console/public/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,7 @@ window.__THUNDERID_RUNTIME_CONFIG__ = {
base: '/console',
client_id: 'CONSOLE',
resource_identifier: 'https://localhost:8090/mcp',
scopes: [
'openid',
'profile',
'email',
'ou',
'system',
'system:user',
'system:group',
'system:ou:view',
'system:usertype:view',
],
scopes: ['openid', 'profile', 'email', 'ou', 'system'],
},
// Defaults to the origin this app is served from. Add a `server` block with `public_url`
// (or `hostname`, `port`, `http_only`) to target a different backend.
Expand Down
4 changes: 2 additions & 2 deletions install/helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ configuration:
consoleClient:
path: "/console"
clientId: "CONSOLE"
scopes: "['openid', 'profile', 'email', 'ou', 'system', 'system:user', 'system:group', 'system:ou:view', 'system:usertype:view']"
scopes: "['openid', 'profile', 'email', 'group', 'ou', 'system']"
# Resource indicator (audience) the console sends on token requests. Must match the
# identifier of the System resource server seeded by the bootstrap defaults, which is
# "<server.publicUrl>/mcp". Leave empty to derive it from server.publicUrl (default);
Expand All @@ -216,7 +216,7 @@ configuration:
# httpOnly: false
# publicUrl: "https://cp.example.com"
# clientId: "FEDERATED_CONSOLE"
# scopes: "['openid', 'profile', 'email', 'group', 'ou', 'system', 'system:user', 'system:group', 'system:ou:view', 'system:usertype:view']"
# scopes: "['openid', 'profile', 'email', 'group', 'ou', 'system']"
# # Type of external authorization server. Set to `generic` when the trusted
# # issuer is a generic OIDC provider. When `generic`, the console skips
# # ThunderID-specific bootstrap calls (flow metadata, branding preferences)
Expand Down
2 changes: 1 addition & 1 deletion install/openchoreo/thunderid-oc-resourcetype/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ A second `HTTPRoute` is rendered for that hostname and the Console's
| `runtime.gate.clientBase` | Gate frontend base path | `/gate` |
| `runtime.console.clientBase` | Console frontend base path | `/console` |
| `runtime.console.clientId` | Console OAuth client ID | `CONSOLE` |
| `runtime.console.scopes` | Console OAuth scopes (JSON array string) — the default covers the management scopes the Console requests | `["openid", "profile", "email", "ou", "system", "system:user", "system:group", "system:ou:view", "system:usertype:view"]` |
| `runtime.console.scopes` | Console OAuth scopes (JSON array string). The default covers the management scopes the Console requests | `["openid", "profile", "email", "group", "ou", "system"]` |
| `runtime.jwt.validityPeriod` | JWT validity in seconds | `3600` |
| `runtime.oauth.refreshTokenValidityPeriod` | Refresh token validity in seconds | `86400` |
| `runtime.cache.size` | Maximum in-memory cache entries | `10000` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ spec:
# console:
# clientBase: "/console"
# clientId: "CONSOLE"
# scopes: '["openid", "profile", "email", "ou", "system", "system:user", "system:group", "system:ou:view", "system:usertype:view"]'
# scopes: '["openid", "profile", "email", "group", "ou", "system"]'
# jwt:
# validityPeriod: 3600 # seconds
# oauth:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ spec:
clientId: { type: string, default: "CONSOLE" }
# Must cover the management scopes the Console requests —
# matches frontend/apps/console/public/config.js.
scopes: { type: string, default: "[\"openid\", \"profile\", \"email\", \"ou\", \"system\", \"system:user\", \"system:group\", \"system:ou:view\", \"system:usertype:view\"]" }
scopes: { type: string, default: "[\"openid\", \"profile\", \"email\", \"group\", \"ou\", \"system\"]" }
# Resource indicator (RFC 8707) the Console sends on token requests.
# Must match the System resource_server identifier in declarativeResources.
# Empty (default) derives it as "<serverPublicUrl>/mcp"; set to override.
Expand Down
72 changes: 43 additions & 29 deletions tests/integration/group/group_authz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import (
"net/http"
"testing"

"github.com/thunder-id/thunderid/tests/integration/testutils"
"github.com/stretchr/testify/suite"
"github.com/thunder-id/thunderid/tests/integration/testutils"
)

// GroupAuthzTestSuite validates that group CRUD operations respect OU-scoped authz.
Expand Down Expand Up @@ -63,6 +63,7 @@ type GroupAuthzTestSuite struct {
// Test role and manager
groupMgrRoleID string
groupMgrUserID string
scopedRSID string
targetGroupOU1ID string
deletableGroupOU1ID string
targetGroupOU2ID string
Expand Down Expand Up @@ -126,7 +127,7 @@ func (ts *GroupAuthzTestSuite) SetupSuite() {

// ---- 2. Create user type for user-manager in OU1 ----
schemaOU1ID, err := testutils.CreateUserType(testutils.UserType{
Name: entityTypeOU1Name,
Name: entityTypeOU1Name,
OUID: ts.groupOU1ID,
Schema: map[string]interface{}{
"username": map[string]interface{}{"type": "string"},
Expand All @@ -139,7 +140,7 @@ func (ts *GroupAuthzTestSuite) SetupSuite() {

// ---- 3. Create the user-manager in OU1 ----
userMgrID, err := testutils.CreateUser(testutils.User{
Type: entityTypeOU1Name,
Type: entityTypeOU1Name,
OUID: ts.groupOU1ID,
Attributes: json.RawMessage(fmt.Sprintf(
`{"username": %q, "password": %q, "display_name": "Group Manager"}`,
Expand All @@ -151,7 +152,7 @@ func (ts *GroupAuthzTestSuite) SetupSuite() {

// ---- 3b. Create a plain member user in OU1 (used in membership authz tests) ----
memberOU1ID, err := testutils.CreateUser(testutils.User{
Type: entityTypeOU1Name,
Type: entityTypeOU1Name,
OUID: ts.groupOU1ID,
Attributes: json.RawMessage(fmt.Sprintf(
`{"username": %q, "password": %q, "display_name": "Member OU1"}`,
Expand All @@ -163,7 +164,7 @@ func (ts *GroupAuthzTestSuite) SetupSuite() {

// ---- 3c. Create a user type for OU2 ----
schemaOU2ID, err := testutils.CreateUserType(testutils.UserType{
Name: memberSchemaOU2Name,
Name: memberSchemaOU2Name,
OUID: ts.groupOU2ID,
Schema: map[string]interface{}{
"username": map[string]interface{}{"type": "string"},
Expand All @@ -176,7 +177,7 @@ func (ts *GroupAuthzTestSuite) SetupSuite() {

// ---- 3d. Create a plain member user in OU2 (used in membership authz tests) ----
memberOU2ID, err := testutils.CreateUser(testutils.User{
Type: memberSchemaOU2Name,
Type: memberSchemaOU2Name,
OUID: ts.groupOU2ID,
Attributes: json.RawMessage(fmt.Sprintf(
`{"username": %q, "password": %q, "display_name": "Member OU2"}`,
Expand All @@ -188,36 +189,42 @@ func (ts *GroupAuthzTestSuite) SetupSuite() {

// ---- 4. Create target groups ----
targetOU1ID, err := testutils.CreateGroup(testutils.Group{
Name: "authz-target-ou1",
Description: "Target Group OU1",
OUID: ts.groupOU1ID,
Name: "authz-target-ou1",
Description: "Target Group OU1",
OUID: ts.groupOU1ID,
})
ts.Require().NoError(err, "create target group in OU1")
ts.targetGroupOU1ID = targetOU1ID

deletableID, err := testutils.CreateGroup(testutils.Group{
Name: "authz-deletable-ou1",
Description: "Deletable Group OU1",
OUID: ts.groupOU1ID,
Name: "authz-deletable-ou1",
Description: "Deletable Group OU1",
OUID: ts.groupOU1ID,
})
ts.Require().NoError(err, "create deletable group in OU1")
ts.deletableGroupOU1ID = deletableID

targetOU2ID, err := testutils.CreateGroup(testutils.Group{
Name: "authz-target-ou2",
Description: "Target Group OU2",
OUID: ts.groupOU2ID,
Name: "authz-target-ou2",
Description: "Target Group OU2",
OUID: ts.groupOU2ID,
})
ts.Require().NoError(err, "create target group in OU2")
ts.targetGroupOU2ID = targetOU2ID

// ---- 5. Look up the system resource server seeded by bootstrap ----
systemRSID, err := testutils.GetResourceServerByName("System")
ts.Require().NoError(err, "look up system resource server")
// ---- 5. Create a custom resource server declaring the fine-grained system scopes ----
// The product ships only the root "system" scope; this reproduces "system:ou:view",
// "system:group" and "system:group:view" so the suite can verify resource-level enforcement
// when configured.
const scopedRSIdentifier = "https://authz-test.example.com/group"
systemRSID, err := testutils.CreateSystemScopedResourceServer(
ts.groupOU1ID, "Authz Test RS (group)", scopedRSIdentifier, "ou", "group")
ts.Require().NoError(err, "create scoped resource server")
ts.scopedRSID = systemRSID

// ---- 6. Create a role with system:group permission and assign to the user-manager ----
roleID, err := testutils.CreateRole(testutils.Role{
Name: groupMgrRoleName,
Name: groupMgrRoleName,
OUID: ts.groupOU1ID,
Permissions: []testutils.ResourcePermissions{
{
Expand All @@ -240,6 +247,8 @@ func (ts *GroupAuthzTestSuite) SetupSuite() {
groupMgrUsername,
groupMgrPassword,
true,
"",
scopedRSIdentifier,
)
ts.Require().NoError(err, "obtain group-manager token")
ts.Require().NotEmpty(tokenResp.AccessToken, "group-manager token must be non-empty")
Expand All @@ -252,6 +261,11 @@ func (ts *GroupAuthzTestSuite) SetupSuite() {
// ---------------------------------------------------------------------------

func (ts *GroupAuthzTestSuite) TearDownSuite() {
if ts.scopedRSID != "" {
if err := testutils.DeleteResourceServer(ts.scopedRSID); err != nil {
ts.T().Logf("teardown: delete scoped resource server: %v", err)
}
}
if ts.groupMgrRoleID != "" {
if err := testutils.DeleteRole(ts.groupMgrRoleID); err != nil {
ts.T().Logf("teardown: delete group-manager role: %v", err)
Expand Down Expand Up @@ -379,9 +393,9 @@ func (ts *GroupAuthzTestSuite) TestGetGroupInOtherOU() {
// TestCreateGroupInOwnOU verifies the group-manager can create a group in their own OU.
func (ts *GroupAuthzTestSuite) TestCreateGroupInOwnOU() {
payload, err := json.Marshal(map[string]interface{}{
"ouId": ts.groupOU1ID,
"name": "authz-created-group",
"description": "Created Group",
"ouId": ts.groupOU1ID,
"name": "authz-created-group",
"description": "Created Group",
})
ts.Require().NoError(err)

Expand All @@ -403,9 +417,9 @@ func (ts *GroupAuthzTestSuite) TestCreateGroupInOwnOU() {
// TestCreateGroupInOtherOU verifies the group-manager is denied creating a group in OU2.
func (ts *GroupAuthzTestSuite) TestCreateGroupInOtherOU() {
payload, err := json.Marshal(map[string]interface{}{
"ouId": ts.groupOU2ID,
"name": "authz-denied-group",
"description": "Denied Group",
"ouId": ts.groupOU2ID,
"name": "authz-denied-group",
"description": "Denied Group",
})
ts.Require().NoError(err)

Expand All @@ -419,9 +433,9 @@ func (ts *GroupAuthzTestSuite) TestCreateGroupInOtherOU() {
// TestUpdateGroupInOwnOU verifies the group-manager can update a group in their own OU.
func (ts *GroupAuthzTestSuite) TestUpdateGroupInOwnOU() {
payload, err := json.Marshal(map[string]interface{}{
"ouId": ts.groupOU1ID,
"name": "authz-target-ou1",
"description": "Updated Description",
"ouId": ts.groupOU1ID,
"name": "authz-target-ou1",
"description": "Updated Description",
})
ts.Require().NoError(err)

Expand All @@ -436,7 +450,7 @@ func (ts *GroupAuthzTestSuite) TestUpdateGroupInOwnOU() {
func (ts *GroupAuthzTestSuite) TestUpdateGroupInOtherOU() {
payload, err := json.Marshal(map[string]interface{}{
"ouId": ts.groupOU2ID,
"name": "Should Not Update",
"name": "Should Not Update",
})
ts.Require().NoError(err)

Expand Down
Loading
Loading