mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-10 15:00:47 +01:00
Compare commits
3 Commits
fix/authdo
...
fix/codemi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e2b0e58fc | ||
|
|
3acb9f8d3d | ||
|
|
0dd9a156b9 |
@@ -464,50 +464,27 @@ components:
|
||||
type: string
|
||||
type: object
|
||||
AuthtypesAuthDomainConfig:
|
||||
discriminator:
|
||||
mapping:
|
||||
google: '#/components/schemas/AuthtypesAuthDomainConfigGoogle'
|
||||
oidc: '#/components/schemas/AuthtypesAuthDomainConfigOIDC'
|
||||
saml: '#/components/schemas/AuthtypesAuthDomainConfigSAML'
|
||||
propertyName: kind
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/AuthtypesAuthDomainConfigSAML'
|
||||
- $ref: '#/components/schemas/AuthtypesAuthDomainConfigGoogle'
|
||||
- $ref: '#/components/schemas/AuthtypesAuthDomainConfigOIDC'
|
||||
type: object
|
||||
AuthtypesAuthDomainConfigGoogle:
|
||||
- $ref: '#/components/schemas/AuthtypesSamlConfig'
|
||||
- $ref: '#/components/schemas/AuthtypesGoogleConfig'
|
||||
- $ref: '#/components/schemas/AuthtypesOIDCConfig'
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
spec:
|
||||
googleAuthConfig:
|
||||
$ref: '#/components/schemas/AuthtypesGoogleConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AuthtypesAuthDomainConfigOIDC:
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
spec:
|
||||
oidcConfig:
|
||||
$ref: '#/components/schemas/AuthtypesOIDCConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AuthtypesAuthDomainConfigSAML:
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
spec:
|
||||
roleMapping:
|
||||
$ref: '#/components/schemas/AuthtypesRoleMapping'
|
||||
samlConfig:
|
||||
$ref: '#/components/schemas/AuthtypesSamlConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
ssoEnabled:
|
||||
type: boolean
|
||||
ssoType:
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
type: object
|
||||
AuthtypesAuthNProvider:
|
||||
enum:
|
||||
- google
|
||||
- google_auth
|
||||
- saml
|
||||
- email_password
|
||||
- oidc
|
||||
@@ -554,16 +531,12 @@ components:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
orgId:
|
||||
type: string
|
||||
roleMapping:
|
||||
$ref: '#/components/schemas/AuthtypesRoleMapping'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
@@ -628,7 +601,6 @@ components:
|
||||
clientId:
|
||||
type: string
|
||||
clientSecret:
|
||||
format: password
|
||||
type: string
|
||||
domainToAdminEmail:
|
||||
additionalProperties:
|
||||
@@ -640,12 +612,10 @@ components:
|
||||
type: boolean
|
||||
insecureSkipEmailVerified:
|
||||
type: boolean
|
||||
serviceAccountJson:
|
||||
format: password
|
||||
redirectURI:
|
||||
type: string
|
||||
serviceAccountJson:
|
||||
type: string
|
||||
required:
|
||||
- clientId
|
||||
- clientSecret
|
||||
type: object
|
||||
AuthtypesOIDCConfig:
|
||||
properties:
|
||||
@@ -654,7 +624,6 @@ components:
|
||||
clientId:
|
||||
type: string
|
||||
clientSecret:
|
||||
format: password
|
||||
type: string
|
||||
getUserInfo:
|
||||
type: boolean
|
||||
@@ -664,10 +633,6 @@ components:
|
||||
type: string
|
||||
issuerAlias:
|
||||
type: string
|
||||
required:
|
||||
- issuer
|
||||
- clientId
|
||||
- clientSecret
|
||||
type: object
|
||||
AuthtypesOrgSessionContext:
|
||||
properties:
|
||||
@@ -689,15 +654,8 @@ components:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/components/schemas/AuthtypesAuthDomainConfig'
|
||||
enabled:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
roleMapping:
|
||||
$ref: '#/components/schemas/AuthtypesRoleMapping'
|
||||
required:
|
||||
- name
|
||||
- config
|
||||
type: object
|
||||
AuthtypesPostableEmailPasswordSession:
|
||||
properties:
|
||||
@@ -804,18 +762,14 @@ components:
|
||||
properties:
|
||||
attributeMapping:
|
||||
$ref: '#/components/schemas/AuthtypesAttributeMapping'
|
||||
certificate:
|
||||
type: string
|
||||
entityId:
|
||||
type: string
|
||||
insecureSkipAuthNRequestsSigned:
|
||||
type: boolean
|
||||
location:
|
||||
samlCert:
|
||||
type: string
|
||||
samlEntity:
|
||||
type: string
|
||||
samlIdp:
|
||||
type: string
|
||||
required:
|
||||
- entityId
|
||||
- location
|
||||
- certificate
|
||||
type: object
|
||||
AuthtypesSessionContext:
|
||||
properties:
|
||||
@@ -855,12 +809,6 @@ components:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/components/schemas/AuthtypesAuthDomainConfig'
|
||||
enabled:
|
||||
type: boolean
|
||||
roleMapping:
|
||||
$ref: '#/components/schemas/AuthtypesRoleMapping'
|
||||
required:
|
||||
- config
|
||||
type: object
|
||||
AuthtypesUpdatableRole:
|
||||
properties:
|
||||
@@ -10654,6 +10602,275 @@ paths:
|
||||
summary: Update public dashboard
|
||||
tags:
|
||||
- dashboard
|
||||
/api/v1/domains:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint lists all auth domains
|
||||
operationId: ListAuthDomains
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/components/schemas/AuthtypesGettableAuthDomain'
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: List all auth domains
|
||||
tags:
|
||||
- authdomains
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint creates an auth domain
|
||||
operationId: CreateAuthDomain
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthtypesPostableAuthDomain'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/TypesIdentifiable'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: Created
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Create auth domain
|
||||
tags:
|
||||
- authdomains
|
||||
/api/v1/domains/{id}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: This endpoint deletes an auth domain
|
||||
operationId: DeleteAuthDomain
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Delete auth domain
|
||||
tags:
|
||||
- authdomains
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns an auth domain by ID
|
||||
operationId: GetAuthDomain
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AuthtypesGettableAuthDomain'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Get auth domain by ID
|
||||
tags:
|
||||
- authdomains
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint updates an auth domain
|
||||
operationId: UpdateAuthDomain
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthtypesUpdatableAuthDomain'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Update auth domain
|
||||
tags:
|
||||
- authdomains
|
||||
/api/v1/downtime_schedules:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -14456,275 +14673,6 @@ paths:
|
||||
summary: Update user preference
|
||||
tags:
|
||||
- preferences
|
||||
/api/v2/auth_domains:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint lists all auth domains
|
||||
operationId: ListAuthDomains
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/components/schemas/AuthtypesGettableAuthDomain'
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: List all auth domains
|
||||
tags:
|
||||
- authdomains
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint creates an auth domain
|
||||
operationId: CreateAuthDomain
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthtypesPostableAuthDomain'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/TypesIdentifiable'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: Created
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Create auth domain
|
||||
tags:
|
||||
- authdomains
|
||||
/api/v2/auth_domains/{id}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: This endpoint deletes an auth domain
|
||||
operationId: DeleteAuthDomain
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Delete auth domain
|
||||
tags:
|
||||
- authdomains
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns an auth domain by ID
|
||||
operationId: GetAuthDomain
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/AuthtypesGettableAuthDomain'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Get auth domain by ID
|
||||
tags:
|
||||
- authdomains
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint updates an auth domain
|
||||
operationId: UpdateAuthDomain
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthtypesUpdatableAuthDomain'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Update auth domain
|
||||
tags:
|
||||
- authdomains
|
||||
/api/v2/dashboard_views:
|
||||
get:
|
||||
deprecated: false
|
||||
|
||||
@@ -61,37 +61,31 @@ type Channel struct {
|
||||
|
||||
```go
|
||||
type AuthDomain struct {
|
||||
storableAuthDomain *StorableAuthDomain
|
||||
storableAuthDomainConfig *StorableAuthDomainConfig
|
||||
storableAuthDomain *StorableAuthDomain
|
||||
authDomainConfig *AuthDomainConfig
|
||||
}
|
||||
|
||||
type StorableAuthDomain struct {
|
||||
bun.BaseModel `bun:"table:auth_domain"`
|
||||
types.Identifiable
|
||||
Name string `bun:"name"`
|
||||
Data string `bun:"data"` // StorableAuthDomainConfig serialized as JSON
|
||||
Data string `bun:"data"` // AuthDomainConfig serialized as JSON
|
||||
OrgID valuer.UUID `bun:"org_id"`
|
||||
types.TimeAuditable
|
||||
}
|
||||
|
||||
type PostableAuthDomain struct {
|
||||
Name string `json:"name" required:"true"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Config AuthDomainConfig `json:"config" required:"true"`
|
||||
RoleMapping *RoleMapping `json:"roleMapping"`
|
||||
Config AuthDomainConfig `json:"config"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type UpdatableAuthDomain struct {
|
||||
Enabled bool `json:"enabled"` // Name intentionally absent
|
||||
Config AuthDomainConfig `json:"config" required:"true"`
|
||||
RoleMapping *RoleMapping `json:"roleMapping"`
|
||||
type UpdateableAuthDomain struct {
|
||||
Config AuthDomainConfig `json:"config"` // Name intentionally absent
|
||||
}
|
||||
|
||||
type GettableAuthDomain struct {
|
||||
StorableAuthDomain
|
||||
Enabled bool `json:"enabled"`
|
||||
Config AuthDomainConfig `json:"config"`
|
||||
RoleMapping *RoleMapping `json:"roleMapping"`
|
||||
*StorableAuthDomain
|
||||
*AuthDomainConfig
|
||||
AuthNProviderInfo *AuthNProviderInfo `json:"authNProviderInfo"`
|
||||
}
|
||||
```
|
||||
@@ -99,11 +93,11 @@ type GettableAuthDomain struct {
|
||||
Each flavor exists for a concrete reason:
|
||||
|
||||
- `StorableAuthDomain` stores the typed config as an opaque `Data string` column, so the schema does not need to migrate every time a config field is added.
|
||||
- `PostableAuthDomain` carries the config as a structured object (not a string) for the request; `AuthDomainConfig` is a kind/spec envelope.
|
||||
- `UpdatableAuthDomain` excludes `Name` because a domain's name cannot change after creation.
|
||||
- `PostableAuthDomain` carries the config as a structured object (not a string) for the request.
|
||||
- `UpdateableAuthDomain` excludes `Name` because a domain's name cannot change after creation.
|
||||
- `GettableAuthDomain` adds `AuthNProviderInfo`, which is derived at read time and never persisted.
|
||||
|
||||
The core `AuthDomain` holds the two live halves — `storableAuthDomain` and `storableAuthDomainConfig` — and owns business methods such as `Update(updatable)` and `Patch(patchable)`. Conversions use the `New<Output>From<Input>` form: `NewAuthDomainFromPostableAuthDomain`, `NewAuthDomainFromStorableAuthDomain`, `NewGettableAuthDomainFromAuthDomain`.
|
||||
The core `AuthDomain` holds the two live halves — `storableAuthDomain` and `authDomainConfig` — and owns business methods such as `Update(config)`. Conversions use the `New<Output>From<Input>` form: `NewAuthDomainFromConfig`, `NewAuthDomainFromStorableAuthDomain`, `NewGettableAuthDomainFromAuthDomain`.
|
||||
|
||||
## Sum types: the kind/spec envelope
|
||||
|
||||
|
||||
@@ -53,6 +53,10 @@ func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSett
|
||||
}
|
||||
|
||||
func (a *AuthN) LoginURL(ctx context.Context, siteURL *url.URL, authDomain *authtypes.AuthDomain) (string, error) {
|
||||
if authDomain.AuthDomainConfig().AuthNProvider != authtypes.AuthNProviderOIDC {
|
||||
return "", errors.Newf(errors.TypeInternal, authtypes.ErrCodeAuthDomainMismatch, "domain type is not oidc")
|
||||
}
|
||||
|
||||
_, oauth2Config, err := a.oidcProviderAndoauth2Config(ctx, siteURL, authDomain)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -81,11 +85,6 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
oidcConfig, err := authDomain.Config().OIDCConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oidcProvider, oauth2Config, err := a.oidcProviderAndoauth2Config(ctx, state.URL, authDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -107,14 +106,14 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims == nil && oidcConfig.GetUserInfo {
|
||||
if claims == nil && authDomain.AuthDomainConfig().OIDC.GetUserInfo {
|
||||
claims, err = a.claimsFromUserInfo(ctx, oidcProvider, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
emailClaim, ok := claims[oidcConfig.ClaimMapping.Email].(string)
|
||||
emailClaim, ok := claims[authDomain.AuthDomainConfig().OIDC.ClaimMapping.Email].(string)
|
||||
if !ok {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: missing email in claims")
|
||||
}
|
||||
@@ -124,7 +123,7 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: failed to parse email").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
if !oidcConfig.InsecureSkipEmailVerified {
|
||||
if !authDomain.AuthDomainConfig().OIDC.InsecureSkipEmailVerified {
|
||||
emailVerifiedClaim, ok := claims["email_verified"].(bool)
|
||||
if !ok {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: missing email_verified in claims")
|
||||
@@ -136,14 +135,14 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
|
||||
}
|
||||
|
||||
name := ""
|
||||
if nameClaim := oidcConfig.ClaimMapping.Name; nameClaim != "" {
|
||||
if nameClaim := authDomain.AuthDomainConfig().OIDC.ClaimMapping.Name; nameClaim != "" {
|
||||
if n, ok := claims[nameClaim].(string); ok {
|
||||
name = n
|
||||
}
|
||||
}
|
||||
|
||||
var groups []string
|
||||
if groupsClaim := oidcConfig.ClaimMapping.Groups; groupsClaim != "" {
|
||||
if groupsClaim := authDomain.AuthDomainConfig().OIDC.ClaimMapping.Groups; groupsClaim != "" {
|
||||
if claimValue, exists := claims[groupsClaim]; exists {
|
||||
switch g := claimValue.(type) {
|
||||
case []any:
|
||||
@@ -162,7 +161,7 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
|
||||
}
|
||||
|
||||
role := ""
|
||||
if roleClaim := oidcConfig.ClaimMapping.Role; roleClaim != "" {
|
||||
if roleClaim := authDomain.AuthDomainConfig().OIDC.ClaimMapping.Role; roleClaim != "" {
|
||||
if r, ok := claims[roleClaim].(string); ok {
|
||||
role = r
|
||||
}
|
||||
@@ -178,16 +177,11 @@ func (a *AuthN) ProviderInfo(ctx context.Context, authDomain *authtypes.AuthDoma
|
||||
}
|
||||
|
||||
func (a *AuthN) oidcProviderAndoauth2Config(ctx context.Context, siteURL *url.URL, authDomain *authtypes.AuthDomain) (*oidc.Provider, *oauth2.Config, error) {
|
||||
oidcConfig, err := authDomain.Config().OIDCConfig()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
if authDomain.AuthDomainConfig().OIDC.IssuerAlias != "" {
|
||||
ctx = oidc.InsecureIssuerURLContext(ctx, authDomain.AuthDomainConfig().OIDC.IssuerAlias)
|
||||
}
|
||||
|
||||
if oidcConfig.IssuerAlias != "" {
|
||||
ctx = oidc.InsecureIssuerURLContext(ctx, oidcConfig.IssuerAlias)
|
||||
}
|
||||
|
||||
oidcProvider, err := oidc.NewProvider(ctx, oidcConfig.Issuer)
|
||||
oidcProvider, err := oidc.NewProvider(ctx, authDomain.AuthDomainConfig().OIDC.Issuer)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -195,13 +189,13 @@ func (a *AuthN) oidcProviderAndoauth2Config(ctx context.Context, siteURL *url.UR
|
||||
scopes := make([]string, len(defaultScopes))
|
||||
copy(scopes, defaultScopes)
|
||||
|
||||
if authDomain.RoleMapping() != nil && len(authDomain.RoleMapping().GroupMappings) > 0 {
|
||||
if authDomain.AuthDomainConfig().RoleMapping != nil && len(authDomain.AuthDomainConfig().RoleMapping.GroupMappings) > 0 {
|
||||
scopes = append(scopes, "groups")
|
||||
}
|
||||
|
||||
return oidcProvider, &oauth2.Config{
|
||||
ClientID: oidcConfig.ClientID,
|
||||
ClientSecret: oidcConfig.ClientSecret,
|
||||
ClientID: authDomain.AuthDomainConfig().OIDC.ClientID,
|
||||
ClientSecret: authDomain.AuthDomainConfig().OIDC.ClientSecret,
|
||||
Endpoint: oidcProvider.Endpoint(),
|
||||
Scopes: scopes,
|
||||
RedirectURL: (&url.URL{
|
||||
@@ -218,12 +212,7 @@ func (a *AuthN) claimsFromIDToken(ctx context.Context, authDomain *authtypes.Aut
|
||||
return nil, errors.New(errors.TypeNotFound, errors.CodeNotFound, "oidc: no id_token in token response")
|
||||
}
|
||||
|
||||
oidcConfig, err := authDomain.Config().OIDCConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
verifier := provider.Verifier(&oidc.Config{ClientID: oidcConfig.ClientID})
|
||||
verifier := provider.Verifier(&oidc.Config{ClientID: authDomain.AuthDomainConfig().OIDC.ClientID})
|
||||
idToken, err := verifier.Verify(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
return nil, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "oidc: failed to verify token").WithAdditional(err.Error())
|
||||
|
||||
@@ -40,6 +40,10 @@ func New(ctx context.Context, store authtypes.AuthNStore, licensing licensing.Li
|
||||
}
|
||||
|
||||
func (a *AuthN) LoginURL(ctx context.Context, siteURL *url.URL, authDomain *authtypes.AuthDomain) (string, error) {
|
||||
if authDomain.AuthDomainConfig().AuthNProvider != authtypes.AuthNProviderSAML {
|
||||
return "", errors.Newf(errors.TypeInternal, authtypes.ErrCodeAuthDomainMismatch, "saml: domain type is not saml")
|
||||
}
|
||||
|
||||
sp, err := a.serviceProvider(siteURL, authDomain)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -69,11 +73,6 @@ func (a *AuthN) HandleCallback(ctx context.Context, formValues url.Values) (*aut
|
||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
samlConfig, err := authDomain.Config().SamlConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sp, err := a.serviceProvider(state.URL, authDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -102,19 +101,19 @@ func (a *AuthN) HandleCallback(ctx context.Context, formValues url.Values) (*aut
|
||||
}
|
||||
|
||||
name := ""
|
||||
if nameAttribute := samlConfig.AttributeMapping.Name; nameAttribute != "" {
|
||||
if nameAttribute := authDomain.AuthDomainConfig().SAML.AttributeMapping.Name; nameAttribute != "" {
|
||||
if val := assertionInfo.Values.Get(nameAttribute); val != "" {
|
||||
name = val
|
||||
}
|
||||
}
|
||||
|
||||
var groups []string
|
||||
if groupAttribute := samlConfig.AttributeMapping.Groups; groupAttribute != "" {
|
||||
if groupAttribute := authDomain.AuthDomainConfig().SAML.AttributeMapping.Groups; groupAttribute != "" {
|
||||
groups = assertionInfo.Values.GetAll(groupAttribute)
|
||||
}
|
||||
|
||||
role := ""
|
||||
if roleAttribute := samlConfig.AttributeMapping.Role; roleAttribute != "" {
|
||||
if roleAttribute := authDomain.AuthDomainConfig().SAML.AttributeMapping.Role; roleAttribute != "" {
|
||||
if val := assertionInfo.Values.Get(roleAttribute); val != "" {
|
||||
role = val
|
||||
}
|
||||
@@ -132,12 +131,7 @@ func (a *AuthN) ProviderInfo(ctx context.Context, authDomain *authtypes.AuthDoma
|
||||
}
|
||||
|
||||
func (a *AuthN) serviceProvider(siteURL *url.URL, authDomain *authtypes.AuthDomain) (*saml2.SAMLServiceProvider, error) {
|
||||
samlConfig, err := authDomain.Config().SamlConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certStore, err := a.getCertificateStore(samlConfig)
|
||||
certStore, err := a.getCertificateStore(authDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -148,32 +142,32 @@ func (a *AuthN) serviceProvider(siteURL *url.URL, authDomain *authtypes.AuthDoma
|
||||
// The ServiceProviderIssuer is the client id in case of keycloak. Since we set it to the host here, we need to set the client id == host in keycloak.
|
||||
// For AWSSSO, this is the value of Application SAML audience.
|
||||
return &saml2.SAMLServiceProvider{
|
||||
IdentityProviderSSOURL: samlConfig.Location,
|
||||
IdentityProviderIssuer: samlConfig.EntityID,
|
||||
IdentityProviderSSOURL: authDomain.AuthDomainConfig().SAML.SamlIdp,
|
||||
IdentityProviderIssuer: authDomain.AuthDomainConfig().SAML.SamlEntity,
|
||||
ServiceProviderIssuer: siteURL.Host,
|
||||
AssertionConsumerServiceURL: acsURL.String(),
|
||||
SignAuthnRequests: !samlConfig.InsecureSkipAuthNRequestsSigned,
|
||||
SignAuthnRequests: !authDomain.AuthDomainConfig().SAML.InsecureSkipAuthNRequestsSigned,
|
||||
AllowMissingAttributes: true,
|
||||
IDPCertificateStore: certStore,
|
||||
SPKeyStore: dsig.RandomKeyStoreForTest(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *AuthN) getCertificateStore(samlConfig authtypes.SamlConfig) (dsig.X509CertificateStore, error) {
|
||||
func (a *AuthN) getCertificateStore(authDomain *authtypes.AuthDomain) (dsig.X509CertificateStore, error) {
|
||||
certStore := &dsig.MemoryX509CertificateStore{
|
||||
Roots: []*x509.Certificate{},
|
||||
}
|
||||
|
||||
var certBytes []byte
|
||||
if strings.Contains(samlConfig.Certificate, "-----BEGIN CERTIFICATE-----") {
|
||||
block, _ := pem.Decode([]byte(samlConfig.Certificate))
|
||||
if strings.Contains(authDomain.AuthDomainConfig().SAML.SamlCert, "-----BEGIN CERTIFICATE-----") {
|
||||
block, _ := pem.Decode([]byte(authDomain.AuthDomainConfig().SAML.SamlCert))
|
||||
if block == nil {
|
||||
return certStore, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "no valid pem cert found")
|
||||
}
|
||||
|
||||
certBytes = block.Bytes
|
||||
} else {
|
||||
certData, err := base64.StdEncoding.DecodeString(samlConfig.Certificate)
|
||||
certData, err := base64.StdEncoding.DecodeString(authDomain.AuthDomainConfig().SAML.SamlCert)
|
||||
if err != nil {
|
||||
return certStore, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to read certificate: %s", err.Error())
|
||||
}
|
||||
|
||||
@@ -38,14 +38,14 @@ import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
*/
|
||||
export const listAuthDomains = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<ListAuthDomains200>({
|
||||
url: `/api/v2/auth_domains`,
|
||||
url: `/api/v1/domains`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListAuthDomainsQueryKey = () => {
|
||||
return [`/api/v2/auth_domains`] as const;
|
||||
return [`/api/v1/domains`] as const;
|
||||
};
|
||||
|
||||
export const getListAuthDomainsQueryOptions = <
|
||||
@@ -125,7 +125,7 @@ export const createAuthDomain = (
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateAuthDomain201>({
|
||||
url: `/api/v2/auth_domains`,
|
||||
url: `/api/v1/domains`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: authtypesPostableAuthDomainDTO,
|
||||
@@ -208,7 +208,7 @@ export const deleteAuthDomain = (
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v2/auth_domains/${id}`,
|
||||
url: `/api/v1/domains/${id}`,
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
});
|
||||
@@ -287,7 +287,7 @@ export const getAuthDomain = (
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetAuthDomain200>({
|
||||
url: `/api/v2/auth_domains/${id}`,
|
||||
url: `/api/v1/domains/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
@@ -296,7 +296,7 @@ export const getAuthDomain = (
|
||||
export const getGetAuthDomainQueryKey = ({
|
||||
id,
|
||||
}: GetAuthDomainPathParameters) => {
|
||||
return [`/api/v2/auth_domains/${id}`] as const;
|
||||
return [`/api/v1/domains/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetAuthDomainQueryOptions = <
|
||||
@@ -389,7 +389,7 @@ export const updateAuthDomain = (
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v2/auth_domains/${id}`,
|
||||
url: `/api/v1/domains/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: authtypesUpdatableAuthDomainDTO,
|
||||
|
||||
@@ -1861,19 +1861,8 @@ export interface AuthtypesAttributeMappingDTO {
|
||||
role?: string;
|
||||
}
|
||||
|
||||
export enum AuthtypesAuthDomainConfigSAMLDTOKind {
|
||||
saml = 'saml',
|
||||
}
|
||||
export interface AuthtypesSamlConfigDTO {
|
||||
attributeMapping?: AuthtypesAttributeMappingDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
certificate: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
entityId: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -1881,21 +1870,17 @@ export interface AuthtypesSamlConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
location: string;
|
||||
}
|
||||
|
||||
export interface AuthtypesAuthDomainConfigSAMLDTO {
|
||||
samlCert?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @enum saml
|
||||
*/
|
||||
kind: AuthtypesAuthDomainConfigSAMLDTOKind;
|
||||
spec: AuthtypesSamlConfigDTO;
|
||||
samlEntity?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
samlIdp?: string;
|
||||
}
|
||||
|
||||
export enum AuthtypesAuthDomainConfigGoogleDTOKind {
|
||||
google = 'google',
|
||||
}
|
||||
export type AuthtypesGoogleConfigDTODomainToAdminEmail = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -1908,12 +1893,11 @@ export interface AuthtypesGoogleConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
clientId: string;
|
||||
clientId?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format password
|
||||
*/
|
||||
clientSecret: string;
|
||||
clientSecret?: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
@@ -1932,34 +1916,24 @@ export interface AuthtypesGoogleConfigDTO {
|
||||
insecureSkipEmailVerified?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
* @format password
|
||||
*/
|
||||
redirectURI?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
serviceAccountJson?: string;
|
||||
}
|
||||
|
||||
export interface AuthtypesAuthDomainConfigGoogleDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum google
|
||||
*/
|
||||
kind: AuthtypesAuthDomainConfigGoogleDTOKind;
|
||||
spec: AuthtypesGoogleConfigDTO;
|
||||
}
|
||||
|
||||
export enum AuthtypesAuthDomainConfigOIDCDTOKind {
|
||||
oidc = 'oidc',
|
||||
}
|
||||
export interface AuthtypesOIDCConfigDTO {
|
||||
claimMapping?: AuthtypesAttributeMappingDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
clientId: string;
|
||||
clientId?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format password
|
||||
*/
|
||||
clientSecret: string;
|
||||
clientSecret?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -1971,33 +1945,79 @@ export interface AuthtypesOIDCConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
issuer: string;
|
||||
issuer?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
issuerAlias?: string;
|
||||
}
|
||||
|
||||
export interface AuthtypesAuthDomainConfigOIDCDTO {
|
||||
export type AuthtypesRoleMappingDTOGroupMappingsAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type AuthtypesRoleMappingDTOGroupMappings =
|
||||
AuthtypesRoleMappingDTOGroupMappingsAnyOf | null;
|
||||
|
||||
export interface AuthtypesRoleMappingDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum oidc
|
||||
*/
|
||||
kind: AuthtypesAuthDomainConfigOIDCDTOKind;
|
||||
spec: AuthtypesOIDCConfigDTO;
|
||||
defaultRole?: string;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
groupMappings?: AuthtypesRoleMappingDTOGroupMappings;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
useRoleAttribute?: boolean;
|
||||
}
|
||||
|
||||
export type AuthtypesAuthDomainConfigDTO =
|
||||
| AuthtypesAuthDomainConfigSAMLDTO
|
||||
| AuthtypesAuthDomainConfigGoogleDTO
|
||||
| AuthtypesAuthDomainConfigOIDCDTO;
|
||||
|
||||
export enum AuthtypesAuthNProviderDTO {
|
||||
google = 'google',
|
||||
google_auth = 'google_auth',
|
||||
saml = 'saml',
|
||||
email_password = 'email_password',
|
||||
oidc = 'oidc',
|
||||
}
|
||||
export type AuthtypesAuthDomainConfigDTO =
|
||||
| (AuthtypesSamlConfigDTO & {
|
||||
googleAuthConfig?: AuthtypesGoogleConfigDTO;
|
||||
oidcConfig?: AuthtypesOIDCConfigDTO;
|
||||
roleMapping?: AuthtypesRoleMappingDTO;
|
||||
samlConfig?: AuthtypesSamlConfigDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
ssoEnabled?: boolean;
|
||||
ssoType?: AuthtypesAuthNProviderDTO;
|
||||
})
|
||||
| (AuthtypesGoogleConfigDTO & {
|
||||
googleAuthConfig?: AuthtypesGoogleConfigDTO;
|
||||
oidcConfig?: AuthtypesOIDCConfigDTO;
|
||||
roleMapping?: AuthtypesRoleMappingDTO;
|
||||
samlConfig?: AuthtypesSamlConfigDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
ssoEnabled?: boolean;
|
||||
ssoType?: AuthtypesAuthNProviderDTO;
|
||||
})
|
||||
| (AuthtypesOIDCConfigDTO & {
|
||||
googleAuthConfig?: AuthtypesGoogleConfigDTO;
|
||||
oidcConfig?: AuthtypesOIDCConfigDTO;
|
||||
roleMapping?: AuthtypesRoleMappingDTO;
|
||||
samlConfig?: AuthtypesSamlConfigDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
ssoEnabled?: boolean;
|
||||
ssoType?: AuthtypesAuthNProviderDTO;
|
||||
});
|
||||
|
||||
export interface AuthtypesAuthNProviderInfoDTO {
|
||||
/**
|
||||
* @type string,null
|
||||
@@ -2035,31 +2055,6 @@ export interface AuthtypesDeprecatedPostableUserRoleDTO {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type AuthtypesRoleMappingDTOGroupMappingsAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type AuthtypesRoleMappingDTOGroupMappings =
|
||||
AuthtypesRoleMappingDTOGroupMappingsAnyOf | null;
|
||||
|
||||
export interface AuthtypesRoleMappingDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
defaultRole?: string;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
groupMappings?: AuthtypesRoleMappingDTOGroupMappings;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
useRoleAttribute?: boolean;
|
||||
}
|
||||
|
||||
export interface AuthtypesGettableAuthDomainDTO {
|
||||
authNProviderInfo?: AuthtypesAuthNProviderInfoDTO;
|
||||
config?: AuthtypesAuthDomainConfigDTO;
|
||||
@@ -2068,10 +2063,6 @@ export interface AuthtypesGettableAuthDomainDTO {
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -2084,7 +2075,6 @@ export interface AuthtypesGettableAuthDomainDTO {
|
||||
* @type string
|
||||
*/
|
||||
orgId?: string;
|
||||
roleMapping?: AuthtypesRoleMappingDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -2281,16 +2271,11 @@ export interface AuthtypesOrgSessionContextDTO {
|
||||
}
|
||||
|
||||
export interface AuthtypesPostableAuthDomainDTO {
|
||||
config: AuthtypesAuthDomainConfigDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
config?: AuthtypesAuthDomainConfigDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
roleMapping?: AuthtypesRoleMappingDTO;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface AuthtypesPostableEmailPasswordSessionDTO {
|
||||
@@ -2423,12 +2408,7 @@ export interface AuthtypesTransactionDTO {
|
||||
}
|
||||
|
||||
export interface AuthtypesUpdatableAuthDomainDTO {
|
||||
config: AuthtypesAuthDomainConfigDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
roleMapping?: AuthtypesRoleMappingDTO;
|
||||
config?: AuthtypesAuthDomainConfigDTO;
|
||||
}
|
||||
|
||||
export interface AuthtypesUpdatableRoleDTO {
|
||||
@@ -10545,6 +10525,42 @@ export type CreatePublicDashboard201 = {
|
||||
export type UpdatePublicDashboardPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type ListAuthDomains200 = {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
data: AuthtypesGettableAuthDomainDTO[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateAuthDomain201 = {
|
||||
data: TypesIdentifiableDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteAuthDomainPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetAuthDomainPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetAuthDomain200 = {
|
||||
data: AuthtypesGettableAuthDomainDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateAuthDomainPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type ListDowntimeSchedulesParams = {
|
||||
/**
|
||||
* @type boolean,null
|
||||
@@ -11218,42 +11234,6 @@ export type GetUserPreference200 = {
|
||||
export type UpdateUserPreferencePathParameters = {
|
||||
name: string;
|
||||
};
|
||||
export type ListAuthDomains200 = {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
data: AuthtypesGettableAuthDomainDTO[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateAuthDomain201 = {
|
||||
data: TypesIdentifiableDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteAuthDomainPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetAuthDomainPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetAuthDomain200 = {
|
||||
data: AuthtypesGettableAuthDomainDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateAuthDomainPathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type ListDashboardViews200 = {
|
||||
data: DashboardtypesListableDashboardViewDTO;
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
.highlights {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px 16px;
|
||||
padding: 12px 0;
|
||||
|
||||
// Constrain each KeyValueLabel (the grid items) to its cell.
|
||||
:global(.key-value-label) {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.valueBadge {
|
||||
--badge-font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// Truncating text inside a badge
|
||||
.badgeText {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.serviceDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-forest);
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.traceLink {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { LOG_HIGHLIGHTS } from './config';
|
||||
import styles from './LogHighlights.module.scss';
|
||||
|
||||
interface LogHighlightsProps {
|
||||
log: ILog;
|
||||
}
|
||||
|
||||
function LogHighlights({ log }: LogHighlightsProps): JSX.Element | null {
|
||||
const fields = LOG_HIGHLIGHTS.map((field) => ({
|
||||
key: field.key,
|
||||
label: field.label,
|
||||
value: field.render(log),
|
||||
})).filter((field) => field.value != null);
|
||||
|
||||
if (fields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.highlights} data-testid="log-details-highlights">
|
||||
{fields.map((field) => (
|
||||
<KeyValueLabel
|
||||
key={field.key}
|
||||
badgeKey={field.label}
|
||||
badgeValue={field.value}
|
||||
direction="column"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogHighlights;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import styles from './LogHighlights.module.scss';
|
||||
|
||||
interface TraceIdFieldProps {
|
||||
traceId: string;
|
||||
}
|
||||
|
||||
function TraceIdField({ traceId }: TraceIdFieldProps): JSX.Element {
|
||||
return (
|
||||
<Link
|
||||
to={{ pathname: `/trace/${traceId}` }}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.traceLink}
|
||||
title={traceId}
|
||||
>
|
||||
{traceId}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default TraceIdField;
|
||||
102
frontend/src/components/LogDetail/LogHighlights/config.tsx
Normal file
102
frontend/src/components/LogDetail/LogHighlights/config.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Badge, BadgeColor } from '@signozhq/ui/badge';
|
||||
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import styles from './LogHighlights.module.scss';
|
||||
import TraceIdField from './TraceIdField';
|
||||
|
||||
// Severity badge color mirrors the LogStateIndicator bar
|
||||
const SEVERITY_COLOR: Record<string, BadgeColor> = {
|
||||
[LogType.TRACE]: 'forest',
|
||||
[LogType.DEBUG]: 'aqua',
|
||||
[LogType.INFO]: 'robin',
|
||||
[LogType.WARN]: 'amber',
|
||||
[LogType.ERROR]: 'cherry',
|
||||
[LogType.FATAL]: 'sakura',
|
||||
};
|
||||
|
||||
export interface LogHighlightConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
render: (log: ILog) => ReactNode | null;
|
||||
}
|
||||
|
||||
// Resource/attribute lookup (keys like `service.name` live in resources_string,
|
||||
// occasionally attributes_string). Typed loosely as these are string maps.
|
||||
const getAttr = (log: ILog, key: string): string =>
|
||||
(log.resources_string as unknown as Record<string, string>)?.[key] ||
|
||||
(log.attributes_string as unknown as Record<string, string>)?.[key] ||
|
||||
'';
|
||||
|
||||
const valueBadge = (
|
||||
value: string,
|
||||
options?: { prefix?: ReactNode; color?: BadgeColor },
|
||||
): ReactNode => (
|
||||
<Badge color={options?.color ?? 'vanilla'} className={styles.valueBadge}>
|
||||
{options?.prefix}
|
||||
<span className={styles.badgeText} title={value}>
|
||||
{value}
|
||||
</span>
|
||||
</Badge>
|
||||
);
|
||||
|
||||
export const LOG_HIGHLIGHTS: LogHighlightConfig[] = [
|
||||
{
|
||||
key: 'service',
|
||||
label: 'SERVICE',
|
||||
render: (log): ReactNode | null => {
|
||||
const value = getAttr(log, 'service.name');
|
||||
return value
|
||||
? valueBadge(value, {
|
||||
prefix: <span className={styles.serviceDot} />,
|
||||
})
|
||||
: null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'severity',
|
||||
label: 'SEVERITY',
|
||||
render: (log): ReactNode | null => {
|
||||
if (!log.severity_text) {
|
||||
return null;
|
||||
}
|
||||
return valueBadge(log.severity_text, {
|
||||
color: SEVERITY_COLOR[getLogIndicatorType(log)] ?? 'vanilla',
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'namespace',
|
||||
label: 'NAMESPACE',
|
||||
render: (log): ReactNode | null => {
|
||||
const value = getAttr(log, 'service.namespace');
|
||||
return value ? valueBadge(value) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'environment',
|
||||
label: 'ENVIRONMENT',
|
||||
render: (log): ReactNode | null => {
|
||||
const value = getAttr(log, 'deployment.environment');
|
||||
return value ? valueBadge(value) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'traceId',
|
||||
label: 'TRACE ID',
|
||||
render: (log): ReactNode | null => {
|
||||
const traceId = log.trace_id || log.traceId;
|
||||
return traceId ? <TraceIdField traceId={traceId} /> : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'spanId',
|
||||
label: 'SPAN ID',
|
||||
render: (log): ReactNode | null => {
|
||||
const spanId = log.span_id || log.spanID;
|
||||
return spanId ? valueBadge(spanId) : null;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -115,6 +115,45 @@ describe('LogDetail drawer — header (isLogDetailsV2)', () => {
|
||||
expect(screen.queryByText('Open in Explorer')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Highlights for fields present on the log, omitting absent ones', () => {
|
||||
const logWithMeta = {
|
||||
...mockLog,
|
||||
severity_text: 'ERROR',
|
||||
trace_id: 'trace-abc',
|
||||
resources_string: {
|
||||
'service.name': 'checkout',
|
||||
'deployment.environment': 'production',
|
||||
},
|
||||
} as unknown as ILog;
|
||||
|
||||
renderDrawer({ log: logWithMeta });
|
||||
|
||||
const highlights = screen.getByTestId('log-details-highlights');
|
||||
expect(highlights).toHaveTextContent('SEVERITY');
|
||||
expect(highlights).toHaveTextContent('ERROR');
|
||||
expect(highlights).toHaveTextContent('SERVICE');
|
||||
expect(highlights).toHaveTextContent('checkout');
|
||||
expect(highlights).toHaveTextContent('ENVIRONMENT');
|
||||
expect(highlights).toHaveTextContent('production');
|
||||
expect(highlights).toHaveTextContent('TRACE ID');
|
||||
// Absent fields are omitted (no namespace / span id on this log).
|
||||
expect(highlights).not.toHaveTextContent('NAMESPACE');
|
||||
expect(highlights).not.toHaveTextContent('SPAN ID');
|
||||
});
|
||||
|
||||
it('links the trace id highlight to the trace detail in a new tab', () => {
|
||||
const logWithTrace = {
|
||||
...mockLog,
|
||||
trace_id: 'trace-abc',
|
||||
} as unknown as ILog;
|
||||
|
||||
renderDrawer({ log: logWithTrace });
|
||||
|
||||
const link = screen.getByRole('link', { name: 'trace-abc' });
|
||||
expect(link).toHaveAttribute('target', '_blank');
|
||||
expect(link.getAttribute('href')).toContain('/trace/trace-abc');
|
||||
});
|
||||
|
||||
it('navigates to the next / previous log with the Down / Up arrow keys', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const logs = [makeLog('log-0'), makeLog('log-1'), makeLog('log-2')];
|
||||
|
||||
@@ -55,6 +55,7 @@ import { isLogDetailsV2, RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
||||
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
|
||||
import LogDetailsHeader from './LogDetailsHeader/LogDetailsHeader';
|
||||
import { useLogNavigation } from './LogDetailsHeader/useLogNavigation';
|
||||
import LogHighlights from './LogHighlights/LogHighlights';
|
||||
|
||||
import './LogDetails.styles.scss';
|
||||
|
||||
@@ -399,6 +400,8 @@ function LogDetailInner({
|
||||
<div className="log-overflow-shadow"> </div>
|
||||
</div>
|
||||
|
||||
{isLogDetailsV2 && <LogHighlights log={log} />}
|
||||
|
||||
<div className="tabs-and-search">
|
||||
<ToggleGroupSimple
|
||||
type="single"
|
||||
|
||||
@@ -183,15 +183,14 @@ function QuerySearch({
|
||||
isProgrammaticChangeRef.current = true;
|
||||
}
|
||||
|
||||
const changes = view.state.changes({
|
||||
from: 0,
|
||||
to: currentValue.length,
|
||||
insert: value,
|
||||
});
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: currentValue.length,
|
||||
insert: value,
|
||||
},
|
||||
selection: {
|
||||
anchor: value.length,
|
||||
},
|
||||
changes,
|
||||
selection: { anchor: changes.newLength },
|
||||
});
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -301,6 +301,66 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||
dispatchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('does not crash when the expression contains CRLF line breaks (issue #5869)', async () => {
|
||||
const dispatchSpy = jest.spyOn(EditorView.prototype, 'dispatch');
|
||||
const onChange = jest.fn() as jest.MockedFunction<(v: string) => void>;
|
||||
const initialExpression = "service.name = 'frontend'";
|
||||
// Filtering on a multi-line log value (CRLF) used to throw
|
||||
// "RangeError: Selection points outside of document".
|
||||
const crlfExpression = "body CONTAINS 'line1\r\nline2\r\nline3'";
|
||||
|
||||
const baseQueryData = {
|
||||
...initialQueriesMap.logs.builder.queryData[0],
|
||||
filter: { expression: initialExpression },
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<QuerySearch
|
||||
onChange={onChange}
|
||||
queryData={baseQueryData}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
const editorContent = document.querySelector(
|
||||
CM_EDITOR_SELECTOR,
|
||||
) as HTMLElement;
|
||||
expect(editorContent.textContent || '').toBe(initialExpression);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
rerender(
|
||||
<QuerySearch
|
||||
onChange={onChange}
|
||||
queryData={{ ...baseQueryData, filter: { expression: crlfExpression } }}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>,
|
||||
);
|
||||
|
||||
// The programmatic replace dispatched without throwing, and the selection anchor
|
||||
// stayed within the CRLF-normalized document (the bug set it past the end).
|
||||
await waitFor(() => {
|
||||
const spec = dispatchSpy.mock.calls
|
||||
.map(
|
||||
(call) =>
|
||||
call[0] as {
|
||||
selection?: { anchor?: number };
|
||||
changes?: { newLength?: number };
|
||||
},
|
||||
)
|
||||
.find((s) => s?.selection?.anchor != null && s?.changes?.newLength != null);
|
||||
expect(spec).toBeDefined();
|
||||
expect(spec?.selection?.anchor).toBeLessThanOrEqual(
|
||||
spec?.changes?.newLength as number,
|
||||
);
|
||||
});
|
||||
|
||||
dispatchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('fetches key suggestions for metrics even without aggregateAttribute.key when showFilterSuggestionsWithoutMetric is true', async () => {
|
||||
const mockedGetKeys = getKeySuggestions as jest.MockedFunction<
|
||||
typeof getKeySuggestions
|
||||
|
||||
@@ -16,7 +16,7 @@ interface AuthNProvider {
|
||||
function getAuthNProviders(samlEnabled: boolean): AuthNProvider[] {
|
||||
return [
|
||||
{
|
||||
key: AuthtypesAuthNProviderDTO.google,
|
||||
key: AuthtypesAuthNProviderDTO.google_auth,
|
||||
title: 'Google Apps Authentication',
|
||||
description: 'Let members sign-in with a Google workspace account',
|
||||
icon: <SolidGoogle size={37} />,
|
||||
@@ -78,7 +78,6 @@ function AuthnProviderSelector({
|
||||
<Button
|
||||
onClick={(): void => setAuthnProvider(provider.key)}
|
||||
type="primary"
|
||||
data-testid={`authn-provider-configure-${provider.key}`}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
import {
|
||||
AuthtypesAuthNProviderDTO,
|
||||
AuthtypesGettableAuthDomainDTO,
|
||||
AuthtypesGoogleConfigDTO,
|
||||
AuthtypesRoleMappingDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
@@ -22,11 +24,10 @@ import APIError from 'types/api/error';
|
||||
|
||||
import AuthnProviderSelector from './AuthnProviderSelector';
|
||||
import {
|
||||
convertDomainMappingsToRecord,
|
||||
convertGroupMappingsToRecord,
|
||||
FormValues,
|
||||
kindToProvider,
|
||||
prepareConfig,
|
||||
prepareInitialValues,
|
||||
prepareRoleMapping,
|
||||
} from './CreateEdit.utils';
|
||||
import ConfigureGoogleAuthAuthnProvider from './Providers/AuthnGoogleAuth';
|
||||
import ConfigureOIDCAuthnProvider from './Providers/AuthnOIDC';
|
||||
@@ -40,7 +41,7 @@ function configureAuthnProvider(
|
||||
switch (authnProvider) {
|
||||
case 'saml':
|
||||
return <ConfigureSAMLAuthnProvider isCreate={isCreate} />;
|
||||
case 'google':
|
||||
case 'google_auth':
|
||||
return <ConfigureGoogleAuthAuthnProvider isCreate={isCreate} />;
|
||||
case 'oidc':
|
||||
return <ConfigureOIDCAuthnProvider isCreate={isCreate} />;
|
||||
@@ -60,7 +61,7 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const [authnProvider, setAuthnProvider] = useState<
|
||||
AuthtypesAuthNProviderDTO | ''
|
||||
>(kindToProvider(record?.config?.kind));
|
||||
>(record?.config?.ssoType || '');
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const { featureFlags } = useAppContext();
|
||||
@@ -84,6 +85,68 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
const { mutate: updateAuthDomain, isLoading: isUpdating } =
|
||||
useUpdateAuthDomain<AxiosError<RenderErrorResponseDTO>>();
|
||||
|
||||
/**
|
||||
* Prepares Google Auth config for API payload
|
||||
*/
|
||||
const getGoogleAuthConfig = useCallback(():
|
||||
| AuthtypesGoogleConfigDTO
|
||||
| undefined => {
|
||||
const config = form.getFieldValue('googleAuthConfig');
|
||||
if (!config) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const {
|
||||
domainToAdminEmailList,
|
||||
allowedGroups,
|
||||
serviceAccountJson,
|
||||
domainToAdminEmail: _domainToAdminEmail,
|
||||
fetchTransitiveGroupMembership,
|
||||
...rest
|
||||
} = config;
|
||||
const domainToAdminEmail = convertDomainMappingsToRecord(
|
||||
domainToAdminEmailList,
|
||||
);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
...(rest.fetchGroups
|
||||
? {
|
||||
allowedGroups,
|
||||
serviceAccountJson,
|
||||
domainToAdminEmail: domainToAdminEmail ?? {},
|
||||
fetchTransitiveGroupMembership,
|
||||
}
|
||||
: { domainToAdminEmail: {} }),
|
||||
};
|
||||
}, [form]);
|
||||
|
||||
// Prepares role mapping for API payload
|
||||
const getRoleMapping = useCallback((): AuthtypesRoleMappingDTO | undefined => {
|
||||
const roleMapping = form.getFieldValue('roleMapping');
|
||||
if (!roleMapping) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { groupMappingsList, ...rest } = roleMapping;
|
||||
const groupMappings = convertGroupMappingsToRecord(groupMappingsList);
|
||||
|
||||
// Only return roleMapping if there's meaningful content
|
||||
const hasDefaultRole = !!rest.defaultRole;
|
||||
const hasUseRoleAttribute = rest.useRoleAttribute === true;
|
||||
const hasGroupMappings =
|
||||
groupMappings && Object.keys(groupMappings).length > 0;
|
||||
|
||||
if (!hasDefaultRole && !hasUseRoleAttribute && !hasGroupMappings) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
groupMappings: rest.useRoleAttribute ? undefined : (groupMappings ?? {}),
|
||||
};
|
||||
}, [form]);
|
||||
|
||||
const onSubmitHandler = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
await form.validateFields();
|
||||
@@ -95,23 +158,25 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
return;
|
||||
}
|
||||
|
||||
const values = form.getFieldsValue(true) as FormValues;
|
||||
const name = values.name ?? '';
|
||||
const config = prepareConfig(values, authnProvider);
|
||||
const roleMapping = prepareRoleMapping(values);
|
||||
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
const name = form.getFieldValue('name');
|
||||
const googleAuthConfig = getGoogleAuthConfig();
|
||||
const samlConfig = form.getFieldValue('samlConfig');
|
||||
const oidcConfig = form.getFieldValue('oidcConfig');
|
||||
const roleMapping = getRoleMapping();
|
||||
|
||||
if (isCreate) {
|
||||
createAuthDomain(
|
||||
{
|
||||
data: {
|
||||
name,
|
||||
enabled: true,
|
||||
config,
|
||||
roleMapping,
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: authnProvider,
|
||||
googleAuthConfig,
|
||||
samlConfig,
|
||||
oidcConfig,
|
||||
roleMapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -131,9 +196,14 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
{
|
||||
pathParams: { id: record.id },
|
||||
data: {
|
||||
enabled: values.enabled ?? false,
|
||||
config,
|
||||
roleMapping,
|
||||
config: {
|
||||
ssoEnabled: form.getFieldValue('ssoEnabled'),
|
||||
ssoType: authnProvider,
|
||||
googleAuthConfig,
|
||||
samlConfig,
|
||||
oidcConfig,
|
||||
roleMapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -149,6 +219,8 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
authnProvider,
|
||||
createAuthDomain,
|
||||
form,
|
||||
getGoogleAuthConfig,
|
||||
getRoleMapping,
|
||||
handleError,
|
||||
isCreate,
|
||||
|
||||
@@ -171,10 +243,10 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
>
|
||||
<Form
|
||||
name="auth-domain"
|
||||
data-testid="auth-domain-form"
|
||||
initialValues={defaultTo(prepareInitialValues(record), {
|
||||
name: '',
|
||||
enabled: false,
|
||||
ssoEnabled: false,
|
||||
ssoType: '',
|
||||
})}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
@@ -190,22 +262,12 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
{configureAuthnProvider(authnProvider, isCreate)}
|
||||
<section className="action-buttons">
|
||||
{isCreate && (
|
||||
<Button
|
||||
onClick={onBackHandler}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
testId="auth-domain-back"
|
||||
>
|
||||
<Button onClick={onBackHandler} variant="solid" color="secondary">
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
{!isCreate && (
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
testId="auth-domain-cancel"
|
||||
>
|
||||
<Button onClick={onClose} variant="solid" color="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
@@ -214,7 +276,6 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
variant="solid"
|
||||
color="primary"
|
||||
loading={isCreating || isUpdating}
|
||||
testId="auth-domain-save"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
AuthtypesAuthDomainConfigGoogleDTOKind,
|
||||
AuthtypesAuthDomainConfigOIDCDTOKind,
|
||||
AuthtypesAuthDomainConfigSAMLDTOKind,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AuthtypesAuthNProviderDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import {
|
||||
convertDomainMappingsToList,
|
||||
@@ -86,7 +82,8 @@ describe('prepareInitialValues', () => {
|
||||
it('returns empty defaults when no record is provided', () => {
|
||||
expect(prepareInitialValues(undefined)).toStrictEqual({
|
||||
name: '',
|
||||
enabled: false,
|
||||
ssoEnabled: false,
|
||||
ssoType: '',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,20 +91,15 @@ describe('prepareInitialValues', () => {
|
||||
const result = prepareInitialValues({
|
||||
id: 'domain-1',
|
||||
name: 'example.com',
|
||||
enabled: true,
|
||||
config: {
|
||||
kind: AuthtypesAuthDomainConfigSAMLDTOKind.saml,
|
||||
spec: {
|
||||
location: 'https://idp.example.com/sso',
|
||||
entityId: 'urn:example:idp',
|
||||
certificate: 'CERT',
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.saml,
|
||||
roleMapping: {
|
||||
defaultRole: 'VIEWER',
|
||||
useRoleAttribute: false,
|
||||
groupMappings: { admins: 'ADMIN', viewers: 'VIEWER' },
|
||||
},
|
||||
},
|
||||
roleMapping: {
|
||||
defaultRole: 'VIEWER',
|
||||
useRoleAttribute: false,
|
||||
groupMappings: { admins: 'ADMIN', viewers: 'VIEWER' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.roleMapping?.groupMappingsList).toStrictEqual([
|
||||
@@ -120,10 +112,10 @@ describe('prepareInitialValues', () => {
|
||||
const result = prepareInitialValues({
|
||||
id: 'domain-1',
|
||||
name: 'example.com',
|
||||
enabled: true,
|
||||
config: {
|
||||
kind: AuthtypesAuthDomainConfigGoogleDTOKind.google,
|
||||
spec: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.google_auth,
|
||||
googleAuthConfig: {
|
||||
clientId: 'id',
|
||||
clientSecret: 'secret',
|
||||
domainToAdminEmail: { 'example.com': 'admin@example.com' },
|
||||
@@ -140,16 +132,11 @@ describe('prepareInitialValues', () => {
|
||||
const result = prepareInitialValues({
|
||||
id: 'domain-1',
|
||||
name: 'example.com',
|
||||
enabled: true,
|
||||
config: {
|
||||
kind: AuthtypesAuthDomainConfigOIDCDTOKind.oidc,
|
||||
spec: {
|
||||
issuer: 'https://oidc.example.com',
|
||||
clientId: 'id',
|
||||
clientSecret: 'secret',
|
||||
},
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.oidc,
|
||||
roleMapping: { defaultRole: 'VIEWER', useRoleAttribute: true },
|
||||
},
|
||||
roleMapping: { defaultRole: 'VIEWER', useRoleAttribute: true },
|
||||
});
|
||||
|
||||
expect(result.roleMapping?.groupMappingsList).toStrictEqual([]);
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
AuthtypesAuthDomainConfigDTO,
|
||||
AuthtypesAuthDomainConfigGoogleDTOKind,
|
||||
AuthtypesAuthDomainConfigOIDCDTOKind,
|
||||
AuthtypesAuthDomainConfigSAMLDTOKind,
|
||||
AuthtypesAuthNProviderDTO,
|
||||
AuthtypesGettableAuthDomainDTO,
|
||||
AuthtypesGoogleConfigDTO,
|
||||
AuthtypesOIDCConfigDTO,
|
||||
@@ -11,29 +6,11 @@ import {
|
||||
AuthtypesSamlConfigDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
/**
|
||||
* Maps the config envelope's per-variant kind to the provider enum driving the
|
||||
* create/edit UI.
|
||||
*/
|
||||
export function kindToProvider(
|
||||
kind?: AuthtypesAuthDomainConfigDTO['kind'],
|
||||
): AuthtypesAuthNProviderDTO | '' {
|
||||
switch (kind) {
|
||||
case AuthtypesAuthDomainConfigSAMLDTOKind.saml:
|
||||
return AuthtypesAuthNProviderDTO.saml;
|
||||
case AuthtypesAuthDomainConfigGoogleDTOKind.google:
|
||||
return AuthtypesAuthNProviderDTO.google;
|
||||
case AuthtypesAuthDomainConfigOIDCDTOKind.oidc:
|
||||
return AuthtypesAuthNProviderDTO.oidc;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Form values interface for internal use (includes array-based fields for UI)
|
||||
export interface FormValues {
|
||||
name?: string;
|
||||
enabled?: boolean;
|
||||
ssoEnabled?: boolean;
|
||||
ssoType?: string;
|
||||
googleAuthConfig?: AuthtypesGoogleConfigDTO & {
|
||||
domainToAdminEmailList?: Array<{ domain?: string; adminEmail?: string }>;
|
||||
};
|
||||
@@ -130,141 +107,33 @@ export function prepareInitialValues(
|
||||
if (!record) {
|
||||
return {
|
||||
name: '',
|
||||
enabled: false,
|
||||
ssoEnabled: false,
|
||||
ssoType: '',
|
||||
};
|
||||
}
|
||||
|
||||
const { config } = record;
|
||||
const config = record.config ?? {};
|
||||
return {
|
||||
name: record.name,
|
||||
enabled: record.enabled,
|
||||
samlConfig:
|
||||
config?.kind === AuthtypesAuthDomainConfigSAMLDTOKind.saml
|
||||
? config.spec
|
||||
: undefined,
|
||||
oidcConfig:
|
||||
config?.kind === AuthtypesAuthDomainConfigOIDCDTOKind.oidc
|
||||
? config.spec
|
||||
: undefined,
|
||||
googleAuthConfig:
|
||||
config?.kind === AuthtypesAuthDomainConfigGoogleDTOKind.google
|
||||
? {
|
||||
...config.spec,
|
||||
domainToAdminEmailList: convertDomainMappingsToList(
|
||||
config.spec.domainToAdminEmail,
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
roleMapping: record.roleMapping
|
||||
ssoEnabled: config.ssoEnabled,
|
||||
ssoType: config.ssoType,
|
||||
samlConfig: config.samlConfig ?? undefined,
|
||||
oidcConfig: config.oidcConfig ?? undefined,
|
||||
googleAuthConfig: config.googleAuthConfig
|
||||
? {
|
||||
...record.roleMapping,
|
||||
...config.googleAuthConfig,
|
||||
domainToAdminEmailList: convertDomainMappingsToList(
|
||||
config.googleAuthConfig.domainToAdminEmail,
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
roleMapping: config.roleMapping
|
||||
? {
|
||||
...config.roleMapping,
|
||||
groupMappingsList: convertGroupMappingsToList(
|
||||
record.roleMapping.groupMappings,
|
||||
config.roleMapping.groupMappings,
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares Google Auth config for API payload
|
||||
*/
|
||||
export function prepareGoogleAuthConfig(
|
||||
values: FormValues,
|
||||
): AuthtypesGoogleConfigDTO | undefined {
|
||||
const config = values.googleAuthConfig;
|
||||
if (!config) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const {
|
||||
domainToAdminEmailList,
|
||||
allowedGroups,
|
||||
serviceAccountJson,
|
||||
domainToAdminEmail: _domainToAdminEmail,
|
||||
fetchTransitiveGroupMembership,
|
||||
...rest
|
||||
} = config;
|
||||
const domainToAdminEmail = convertDomainMappingsToRecord(
|
||||
domainToAdminEmailList,
|
||||
);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
...(rest.fetchGroups
|
||||
? {
|
||||
allowedGroups,
|
||||
serviceAccountJson,
|
||||
domainToAdminEmail: domainToAdminEmail ?? {},
|
||||
fetchTransitiveGroupMembership,
|
||||
}
|
||||
: { domainToAdminEmail: {} }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares role mapping for API payload; only returned when there is
|
||||
* meaningful content.
|
||||
*/
|
||||
export function prepareRoleMapping(
|
||||
values: FormValues,
|
||||
): AuthtypesRoleMappingDTO | undefined {
|
||||
const roleMapping = values.roleMapping;
|
||||
if (!roleMapping) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { groupMappingsList, ...rest } = roleMapping;
|
||||
const groupMappings = convertGroupMappingsToRecord(groupMappingsList);
|
||||
|
||||
const hasDefaultRole = !!rest.defaultRole;
|
||||
const hasUseRoleAttribute = rest.useRoleAttribute === true;
|
||||
const hasGroupMappings =
|
||||
groupMappings && Object.keys(groupMappings).length > 0;
|
||||
|
||||
if (!hasDefaultRole && !hasUseRoleAttribute && !hasGroupMappings) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
groupMappings: rest.useRoleAttribute ? undefined : (groupMappings ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the kind/spec config envelope for API payload; the inverse of
|
||||
* prepareInitialValues.
|
||||
*/
|
||||
export function prepareConfig(
|
||||
values: FormValues,
|
||||
provider: AuthtypesAuthNProviderDTO | '',
|
||||
): AuthtypesAuthDomainConfigDTO | undefined {
|
||||
switch (provider) {
|
||||
case AuthtypesAuthNProviderDTO.saml:
|
||||
return values.samlConfig
|
||||
? {
|
||||
kind: AuthtypesAuthDomainConfigSAMLDTOKind.saml,
|
||||
spec: values.samlConfig,
|
||||
}
|
||||
: undefined;
|
||||
case AuthtypesAuthNProviderDTO.google: {
|
||||
const spec = prepareGoogleAuthConfig(values);
|
||||
return spec
|
||||
? {
|
||||
kind: AuthtypesAuthDomainConfigGoogleDTOKind.google,
|
||||
spec,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
case AuthtypesAuthNProviderDTO.oidc:
|
||||
return values.oidcConfig
|
||||
? {
|
||||
kind: AuthtypesAuthDomainConfigOIDCDTOKind.oidc,
|
||||
spec: values.oidcConfig,
|
||||
}
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,11 +91,7 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
{ required: true, message: 'Domain is required', whitespace: true },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
id="google-domain"
|
||||
disabled={!isCreate}
|
||||
testId="google-auth-domain"
|
||||
/>
|
||||
<Input id="google-domain" disabled={!isCreate} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
@@ -113,7 +109,7 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
{ required: true, message: 'Client ID is required', whitespace: true },
|
||||
]}
|
||||
>
|
||||
<Input id="google-client-id" testId="google-auth-client-id" />
|
||||
<Input id="google-client-id" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
@@ -135,7 +131,7 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input id="google-client-secret" testId="google-auth-client-secret" />
|
||||
<Input id="google-client-secret" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
@@ -147,7 +143,6 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
>
|
||||
<Checkbox
|
||||
id="google-skip-email-verification"
|
||||
testId="google-auth-skip-email-verified"
|
||||
onChange={(checked: boolean): void => {
|
||||
form.setFieldValue(
|
||||
['googleAuthConfig', 'insecureSkipEmailVerified'],
|
||||
@@ -185,10 +180,7 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
<Collapse.Panel
|
||||
key="workspace-groups"
|
||||
header={
|
||||
<div
|
||||
className="authn-provider__collapse-header"
|
||||
data-testid="google-auth-workspace-groups-header"
|
||||
>
|
||||
<div className="authn-provider__collapse-header">
|
||||
{expandedSection !== 'workspace-groups' ? (
|
||||
<ChevronRight size={16} />
|
||||
) : (
|
||||
@@ -229,7 +221,6 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
>
|
||||
<Checkbox
|
||||
id="google-fetch-groups"
|
||||
testId="google-auth-fetch-groups"
|
||||
onChange={(checked: boolean): void => {
|
||||
form.setFieldValue(['googleAuthConfig', 'fetchGroups'], checked);
|
||||
}}
|
||||
@@ -260,7 +251,6 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
>
|
||||
<AntdInput.TextArea
|
||||
id="google-service-account-json"
|
||||
data-testid="google-auth-service-account-json"
|
||||
rows={3}
|
||||
placeholder="Paste service account JSON"
|
||||
className="authn-provider__textarea"
|
||||
@@ -280,7 +270,6 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
>
|
||||
<Checkbox
|
||||
id="google-transitive-membership"
|
||||
testId="google-auth-transitive-membership"
|
||||
onChange={(checked: boolean): void => {
|
||||
form.setFieldValue(
|
||||
['googleAuthConfig', 'fetchTransitiveGroupMembership'],
|
||||
@@ -310,10 +299,7 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
name={['googleAuthConfig', 'allowedGroups']}
|
||||
className="authn-provider__form-item"
|
||||
>
|
||||
<EmailTagInput
|
||||
placeholder="Type a group email and press Enter"
|
||||
testId="google-auth-allowed-groups"
|
||||
/>
|
||||
<EmailTagInput placeholder="Type a group email and press Enter" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,7 @@ function ConfigureSAMLAuthnProvider({
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['samlConfig', 'location']}
|
||||
name={['samlConfig', 'samlIdp']}
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{
|
||||
@@ -98,7 +98,7 @@ function ConfigureSAMLAuthnProvider({
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['samlConfig', 'entityId']}
|
||||
name={['samlConfig', 'samlEntity']}
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{
|
||||
@@ -120,7 +120,7 @@ function ConfigureSAMLAuthnProvider({
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['samlConfig', 'certificate']}
|
||||
name={['samlConfig', 'samlCert']}
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{
|
||||
|
||||
@@ -9,14 +9,12 @@ interface EmailTagInputProps {
|
||||
value?: string[];
|
||||
onChange?: (value: string[]) => void;
|
||||
placeholder?: string;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
function EmailTagInput({
|
||||
value = [],
|
||||
onChange,
|
||||
placeholder = 'Type an email and press Enter',
|
||||
testId,
|
||||
}: EmailTagInputProps): JSX.Element {
|
||||
const [validationError, setValidationError] = useState('');
|
||||
|
||||
@@ -36,7 +34,7 @@ function EmailTagInput({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="email-tag-input" data-testid={testId}>
|
||||
<div className="email-tag-input">
|
||||
<Tooltip
|
||||
title={validationError}
|
||||
open={!!validationError}
|
||||
|
||||
@@ -74,7 +74,6 @@ function RoleMappingSection({
|
||||
role="button"
|
||||
aria-expanded={expanded}
|
||||
aria-controls="role-mapping-content"
|
||||
data-testid="role-mapping-header"
|
||||
>
|
||||
{!expanded ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
|
||||
<div className="role-mapping-section__collapse-header-text">
|
||||
@@ -139,7 +138,6 @@ function RoleMappingSection({
|
||||
>
|
||||
<Checkbox
|
||||
id="use-role-attribute"
|
||||
testId="role-mapping-use-role-attribute"
|
||||
onChange={(checked: boolean): void => {
|
||||
form.setFieldValue([...fieldNamePrefix, 'useRoleAttribute'], checked);
|
||||
}}
|
||||
@@ -168,20 +166,13 @@ function RoleMappingSection({
|
||||
{(fields, { add, remove }): JSX.Element => (
|
||||
<div className="role-mapping-section__items">
|
||||
{fields.map((field) => (
|
||||
<div
|
||||
key={field.key}
|
||||
className="role-mapping-section__row"
|
||||
data-testid="role-mapping-row"
|
||||
>
|
||||
<div key={field.key} className="role-mapping-section__row">
|
||||
<Form.Item
|
||||
name={[field.name, 'groupName']}
|
||||
className="role-mapping-section__field role-mapping-section__field--group"
|
||||
rules={[{ required: true, message: 'Group name is required' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="IDP Group Name"
|
||||
testId="role-mapping-group-name"
|
||||
/>
|
||||
<Input placeholder="IDP Group Name" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
@@ -208,7 +199,6 @@ function RoleMappingSection({
|
||||
className="role-mapping-section__remove-btn"
|
||||
onClick={(): void => remove(field.name)}
|
||||
aria-label="Remove mapping"
|
||||
testId="role-mapping-remove"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</Button>
|
||||
@@ -222,7 +212,6 @@ function RoleMappingSection({
|
||||
add({ groupName: '', role: SIGNOZ_VIEWER_ROLE })
|
||||
}
|
||||
prefix={<Plus size={14} />}
|
||||
testId="role-mapping-add"
|
||||
>
|
||||
Add Group Mapping
|
||||
</Button>
|
||||
|
||||
@@ -31,7 +31,7 @@ function SSOEnforcementToggle({
|
||||
useUpdateAuthDomain<AxiosError<RenderErrorResponseDTO>>();
|
||||
|
||||
const onChangeHandler = (checked: boolean): void => {
|
||||
if (!record.id || !record.config) {
|
||||
if (!record.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -41,9 +41,14 @@ function SSOEnforcementToggle({
|
||||
{
|
||||
pathParams: { id: record.id },
|
||||
data: {
|
||||
enabled: checked,
|
||||
config: record.config,
|
||||
roleMapping: record.roleMapping,
|
||||
config: {
|
||||
ssoEnabled: checked,
|
||||
ssoType: record.config?.ssoType,
|
||||
googleAuthConfig: record.config?.googleAuthConfig,
|
||||
oidcConfig: record.config?.oidcConfig,
|
||||
samlConfig: record.config?.samlConfig,
|
||||
roleMapping: record.config?.roleMapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -60,12 +65,7 @@ function SSOEnforcementToggle({
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch
|
||||
disabled={isLoading}
|
||||
value={isChecked}
|
||||
onChange={onChangeHandler}
|
||||
testId="auth-domain-enforce-sso"
|
||||
/>
|
||||
<Switch disabled={isLoading} value={isChecked} onChange={onChangeHandler} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('AuthDomain', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('reflects the enabled state in each row toggle', async () => {
|
||||
it('reflects ssoEnabled state from nested config in each row toggle', async () => {
|
||||
server.use(
|
||||
rest.get(AUTH_DOMAINS_LIST_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockDomainsListResponse)),
|
||||
@@ -68,9 +68,9 @@ describe('AuthDomain', () => {
|
||||
render(<AuthDomain />);
|
||||
|
||||
// mockDomainsListResponse rows:
|
||||
// [0] signoz.io → enabled: true
|
||||
// [1] example.com → enabled: false
|
||||
// [2] corp.io → enabled: true
|
||||
// [0] signoz.io → config.ssoEnabled: true
|
||||
// [1] example.com → config.ssoEnabled: false
|
||||
// [2] corp.io → config.ssoEnabled: true
|
||||
const switches = await screen.findAllByRole('switch');
|
||||
expect(switches).toHaveLength(3);
|
||||
expect(switches[0]).toBeChecked();
|
||||
|
||||
@@ -112,7 +112,9 @@ describe('CreateEdit — save payload correctness', () => {
|
||||
await waitFor(() => expect(capturedPayload).not.toBeNull());
|
||||
|
||||
expect(capturedPayload).toMatchObject({
|
||||
roleMapping: expect.objectContaining({ groupMappings: {} }),
|
||||
config: expect.objectContaining({
|
||||
roleMapping: expect.objectContaining({ groupMappings: {} }),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -159,7 +161,7 @@ describe('CreateEdit — save payload correctness', () => {
|
||||
|
||||
expect(capturedPayload).toMatchObject({
|
||||
config: expect.objectContaining({
|
||||
spec: expect.objectContaining({
|
||||
googleAuthConfig: expect.objectContaining({
|
||||
domainToAdminEmail: {},
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -188,8 +188,8 @@ describe('CreateEdit — role mapping uses API roles', () => {
|
||||
|
||||
// SSO role mapping matches roles by name, so the payload carries the
|
||||
// role *name*, not the opaque id.
|
||||
expect(payload.get().roleMapping.defaultRole).toBe(editorRole.name);
|
||||
expect(payload.get().roleMapping.defaultRole).not.toBe(editorRole.id);
|
||||
expect(payload.get().config.roleMapping.defaultRole).toBe(editorRole.name);
|
||||
expect(payload.get().config.roleMapping.defaultRole).not.toBe(editorRole.id);
|
||||
});
|
||||
|
||||
it('defaults a fresh role mapping to the signoz-viewer role name', async () => {
|
||||
@@ -221,8 +221,8 @@ describe('CreateEdit — role mapping uses API roles', () => {
|
||||
|
||||
await waitFor(() => expect(payload.get()).not.toBeNull());
|
||||
|
||||
expect(payload.get().roleMapping.defaultRole).toBe(viewerRole.name);
|
||||
expect(payload.get().roleMapping.defaultRole).not.toBe(viewerRole.id);
|
||||
expect(payload.get().config.roleMapping.defaultRole).toBe(viewerRole.name);
|
||||
expect(payload.get().config.roleMapping.defaultRole).not.toBe(viewerRole.id);
|
||||
});
|
||||
|
||||
it('still defaults to signoz-viewer when the roles fetch returns empty', async () => {
|
||||
@@ -249,7 +249,7 @@ describe('CreateEdit — role mapping uses API roles', () => {
|
||||
await waitFor(() => expect(payload.get()).not.toBeNull());
|
||||
|
||||
// The Form.Item initialValue (signoz-viewer) survives an empty roles list.
|
||||
expect(payload.get().roleMapping.defaultRole).toBe(viewerRole.name);
|
||||
expect(payload.get().config.roleMapping.defaultRole).toBe(viewerRole.name);
|
||||
});
|
||||
|
||||
it('loads a stored role mapping by role name and round-trips it on save', async () => {
|
||||
@@ -280,8 +280,8 @@ describe('CreateEdit — role mapping uses API roles', () => {
|
||||
|
||||
await waitFor(() => expect(payload.get()).not.toBeNull());
|
||||
|
||||
expect(payload.get().roleMapping.defaultRole).toBe(editorRole.name);
|
||||
expect(payload.get().roleMapping.groupMappings).toStrictEqual({
|
||||
expect(payload.get().config.roleMapping.defaultRole).toBe(editorRole.name);
|
||||
expect(payload.get().config.roleMapping.groupMappings).toStrictEqual({
|
||||
'admin-group': 'signoz-admin',
|
||||
'dev-team': 'signoz-editor',
|
||||
viewers: 'signoz-viewer',
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import {
|
||||
AuthtypesAuthDomainConfigGoogleDTO,
|
||||
AuthtypesGettableAuthDomainDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AuthtypesGettableAuthDomainDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import CreateEdit from '../CreateEdit/CreateEdit';
|
||||
import {
|
||||
@@ -51,10 +48,11 @@ jest.mock('@signozhq/ui/button', () => ({
|
||||
|
||||
type SavedPayload = {
|
||||
config: {
|
||||
kind?: string;
|
||||
spec?: Record<string, unknown>;
|
||||
googleAuthConfig?: Record<string, unknown>;
|
||||
samlConfig?: Record<string, unknown>;
|
||||
oidcConfig?: Record<string, unknown>;
|
||||
roleMapping?: Record<string, unknown>;
|
||||
};
|
||||
roleMapping?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
async function submitForm(
|
||||
@@ -83,7 +81,7 @@ describe('CreateEdit — payload sanitization', () => {
|
||||
it('sends core fields and omits workspace fields when fetchGroups is not set', async () => {
|
||||
const payload = await submitForm(mockGoogleAuthDomain);
|
||||
|
||||
const g = payload.config.spec;
|
||||
const g = payload.config.googleAuthConfig;
|
||||
expect(g?.clientId).toBe('test-client-id');
|
||||
expect(g?.clientSecret).toBe('test-client-secret');
|
||||
expect(g?.allowedGroups).toBeUndefined();
|
||||
@@ -93,20 +91,18 @@ describe('CreateEdit — payload sanitization', () => {
|
||||
});
|
||||
|
||||
it('strips workspace fields when fetchGroups is false', async () => {
|
||||
const googleConfig =
|
||||
mockGoogleAuthWithWorkspaceGroups.config as AuthtypesAuthDomainConfigGoogleDTO;
|
||||
const payload = await submitForm({
|
||||
...mockGoogleAuthWithWorkspaceGroups,
|
||||
config: {
|
||||
...googleConfig,
|
||||
spec: {
|
||||
...googleConfig.spec,
|
||||
...mockGoogleAuthWithWorkspaceGroups.config,
|
||||
googleAuthConfig: {
|
||||
...mockGoogleAuthWithWorkspaceGroups.config?.googleAuthConfig,
|
||||
fetchGroups: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const g = payload.config.spec;
|
||||
const g = payload.config.googleAuthConfig;
|
||||
expect(g?.fetchGroups).toBe(false);
|
||||
expect(g?.allowedGroups).toBeUndefined();
|
||||
expect(g?.serviceAccountJson).toBeUndefined();
|
||||
@@ -117,7 +113,7 @@ describe('CreateEdit — payload sanitization', () => {
|
||||
it('includes all workspace fields when fetchGroups is true', async () => {
|
||||
const payload = await submitForm(mockGoogleAuthWithWorkspaceGroups);
|
||||
|
||||
const g = payload.config.spec;
|
||||
const g = payload.config.googleAuthConfig;
|
||||
expect(g?.fetchGroups).toBe(true);
|
||||
expect(g?.serviceAccountJson).toBe('{"type": "service_account"}');
|
||||
expect(g?.fetchTransitiveGroupMembership).toBe(true);
|
||||
@@ -135,10 +131,10 @@ describe('CreateEdit — payload sanitization', () => {
|
||||
it('sends core and attributeMapping fields', async () => {
|
||||
const payload = await submitForm(mockSamlWithAttributeMapping);
|
||||
|
||||
const s = payload.config.spec;
|
||||
expect(s?.location).toBe('https://idp.saml-attrs.com/sso');
|
||||
expect(s?.entityId).toBe('urn:saml-attrs:idp');
|
||||
expect(s?.certificate).toBe('MOCK_CERTIFICATE_ATTRS');
|
||||
const s = payload.config.samlConfig;
|
||||
expect(s?.samlIdp).toBe('https://idp.saml-attrs.com/sso');
|
||||
expect(s?.samlEntity).toBe('urn:saml-attrs:idp');
|
||||
expect(s?.samlCert).toBe('MOCK_CERTIFICATE_ATTRS');
|
||||
expect(s?.insecureSkipAuthNRequestsSigned).toBe(true);
|
||||
|
||||
const attr = s?.attributeMapping as Record<string, unknown>;
|
||||
@@ -152,7 +148,7 @@ describe('CreateEdit — payload sanitization', () => {
|
||||
it('sends all fields including claimMapping', async () => {
|
||||
const payload = await submitForm(mockOidcWithClaimMapping);
|
||||
|
||||
const o = payload.config.spec;
|
||||
const o = payload.config.oidcConfig;
|
||||
expect(o?.issuer).toBe('https://oidc.claims.com');
|
||||
expect(o?.issuerAlias).toBe('https://alias.claims.com');
|
||||
expect(o?.clientId).toBe('claims-client-id');
|
||||
@@ -172,21 +168,24 @@ describe('CreateEdit — payload sanitization', () => {
|
||||
it('strips groupMappings when useRoleAttribute is true', async () => {
|
||||
const payload = await submitForm({
|
||||
...mockDomainWithRoleMapping,
|
||||
roleMapping: {
|
||||
...mockDomainWithRoleMapping.roleMapping,
|
||||
useRoleAttribute: true,
|
||||
config: {
|
||||
...mockDomainWithRoleMapping.config,
|
||||
roleMapping: {
|
||||
...mockDomainWithRoleMapping.config?.roleMapping,
|
||||
useRoleAttribute: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload.roleMapping?.useRoleAttribute).toBe(true);
|
||||
expect(payload.roleMapping?.groupMappings).toBeUndefined();
|
||||
expect(payload.config.roleMapping?.useRoleAttribute).toBe(true);
|
||||
expect(payload.config.roleMapping?.groupMappings).toBeUndefined();
|
||||
});
|
||||
|
||||
it('sends groupMappings when useRoleAttribute is false', async () => {
|
||||
const payload = await submitForm(mockDomainWithRoleMapping);
|
||||
|
||||
expect(payload.roleMapping?.useRoleAttribute).toBe(false);
|
||||
expect(payload.roleMapping?.groupMappings).toStrictEqual({
|
||||
expect(payload.config.roleMapping?.useRoleAttribute).toBe(false);
|
||||
expect(payload.config.roleMapping?.groupMappings).toStrictEqual({
|
||||
'admin-group': 'signoz-admin',
|
||||
'dev-team': 'signoz-editor',
|
||||
viewers: 'signoz-viewer',
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('SSOEnforcementToggle', () => {
|
||||
isDefaultChecked={false}
|
||||
record={{
|
||||
...mockGoogleAuthDomain,
|
||||
enabled: false,
|
||||
config: { ...mockGoogleAuthDomain.config, ssoEnabled: false },
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
@@ -95,8 +95,9 @@ describe('SSOEnforcementToggle', () => {
|
||||
expect(mockUpdateAPI).toHaveBeenCalledTimes(1);
|
||||
expect(mockUpdateAPI).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
enabled: false,
|
||||
config: mockGoogleAuthDomain.config,
|
||||
config: expect.objectContaining({
|
||||
ssoEnabled: false,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import {
|
||||
AuthtypesAuthDomainConfigGoogleDTOKind,
|
||||
AuthtypesAuthDomainConfigOIDCDTOKind,
|
||||
AuthtypesAuthDomainConfigSAMLDTOKind,
|
||||
AuthtypesAuthNProviderDTO,
|
||||
AuthtypesGettableAuthDomainDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
// API Endpoints
|
||||
export const AUTH_DOMAINS_LIST_ENDPOINT = '*/api/v2/auth_domains';
|
||||
export const AUTH_DOMAINS_CREATE_ENDPOINT = '*/api/v2/auth_domains';
|
||||
export const AUTH_DOMAINS_UPDATE_ENDPOINT = '*/api/v2/auth_domains/:id';
|
||||
export const AUTH_DOMAINS_DELETE_ENDPOINT = '*/api/v2/auth_domains/:id';
|
||||
export const AUTH_DOMAINS_LIST_ENDPOINT = '*/api/v1/domains';
|
||||
export const AUTH_DOMAINS_CREATE_ENDPOINT = '*/api/v1/domains';
|
||||
export const AUTH_DOMAINS_UPDATE_ENDPOINT = '*/api/v1/domains/:id';
|
||||
export const AUTH_DOMAINS_DELETE_ENDPOINT = '*/api/v1/domains/:id';
|
||||
|
||||
// Mock Auth Domain with Google Auth
|
||||
export const mockGoogleAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-1',
|
||||
name: 'signoz.io',
|
||||
enabled: true,
|
||||
config: {
|
||||
kind: AuthtypesAuthDomainConfigGoogleDTOKind.google,
|
||||
spec: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.google_auth,
|
||||
googleAuthConfig: {
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
},
|
||||
@@ -32,13 +30,13 @@ export const mockGoogleAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
export const mockSamlAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-2',
|
||||
name: 'example.com',
|
||||
enabled: false,
|
||||
config: {
|
||||
kind: AuthtypesAuthDomainConfigSAMLDTOKind.saml,
|
||||
spec: {
|
||||
location: 'https://idp.example.com/sso',
|
||||
entityId: 'urn:example:idp',
|
||||
certificate: 'MOCK_CERTIFICATE',
|
||||
ssoEnabled: false,
|
||||
ssoType: AuthtypesAuthNProviderDTO.saml,
|
||||
samlConfig: {
|
||||
samlIdp: 'https://idp.example.com/sso',
|
||||
samlEntity: 'urn:example:idp',
|
||||
samlCert: 'MOCK_CERTIFICATE',
|
||||
},
|
||||
},
|
||||
authNProviderInfo: {
|
||||
@@ -50,10 +48,10 @@ export const mockSamlAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
export const mockOidcAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-3',
|
||||
name: 'corp.io',
|
||||
enabled: true,
|
||||
config: {
|
||||
kind: AuthtypesAuthDomainConfigOIDCDTOKind.oidc,
|
||||
spec: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.oidc,
|
||||
oidcConfig: {
|
||||
issuer: 'https://oidc.corp.io',
|
||||
clientId: 'oidc-client-id',
|
||||
clientSecret: 'oidc-client-secret',
|
||||
@@ -68,22 +66,22 @@ export const mockOidcAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
export const mockDomainWithRoleMapping: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-4',
|
||||
name: 'enterprise.com',
|
||||
enabled: true,
|
||||
config: {
|
||||
kind: AuthtypesAuthDomainConfigSAMLDTOKind.saml,
|
||||
spec: {
|
||||
location: 'https://idp.enterprise.com/sso',
|
||||
entityId: 'urn:enterprise:idp',
|
||||
certificate: 'MOCK_CERTIFICATE',
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.saml,
|
||||
samlConfig: {
|
||||
samlIdp: 'https://idp.enterprise.com/sso',
|
||||
samlEntity: 'urn:enterprise:idp',
|
||||
samlCert: 'MOCK_CERTIFICATE',
|
||||
},
|
||||
},
|
||||
roleMapping: {
|
||||
defaultRole: 'signoz-editor',
|
||||
useRoleAttribute: false,
|
||||
groupMappings: {
|
||||
'admin-group': 'signoz-admin',
|
||||
'dev-team': 'signoz-editor',
|
||||
viewers: 'signoz-viewer',
|
||||
roleMapping: {
|
||||
defaultRole: 'signoz-editor',
|
||||
useRoleAttribute: false,
|
||||
groupMappings: {
|
||||
'admin-group': 'signoz-admin',
|
||||
'dev-team': 'signoz-editor',
|
||||
viewers: 'signoz-viewer',
|
||||
},
|
||||
},
|
||||
},
|
||||
authNProviderInfo: {
|
||||
@@ -96,18 +94,18 @@ export const mockDomainWithDirectRoleAttribute: AuthtypesGettableAuthDomainDTO =
|
||||
{
|
||||
id: 'domain-5',
|
||||
name: 'direct-role.com',
|
||||
enabled: true,
|
||||
config: {
|
||||
kind: AuthtypesAuthDomainConfigOIDCDTOKind.oidc,
|
||||
spec: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.oidc,
|
||||
oidcConfig: {
|
||||
issuer: 'https://oidc.direct-role.com',
|
||||
clientId: 'direct-role-client-id',
|
||||
clientSecret: 'direct-role-client-secret',
|
||||
},
|
||||
},
|
||||
roleMapping: {
|
||||
defaultRole: 'signoz-viewer',
|
||||
useRoleAttribute: true,
|
||||
roleMapping: {
|
||||
defaultRole: 'signoz-viewer',
|
||||
useRoleAttribute: true,
|
||||
},
|
||||
},
|
||||
authNProviderInfo: {
|
||||
relayStatePath: 'api/v1/sso/relay/domain-5',
|
||||
@@ -118,10 +116,10 @@ export const mockDomainWithDirectRoleAttribute: AuthtypesGettableAuthDomainDTO =
|
||||
export const mockOidcWithClaimMapping: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-6',
|
||||
name: 'oidc-claims.com',
|
||||
enabled: true,
|
||||
config: {
|
||||
kind: AuthtypesAuthDomainConfigOIDCDTOKind.oidc,
|
||||
spec: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.oidc,
|
||||
oidcConfig: {
|
||||
issuer: 'https://oidc.claims.com',
|
||||
issuerAlias: 'https://alias.claims.com',
|
||||
clientId: 'claims-client-id',
|
||||
@@ -145,13 +143,13 @@ export const mockOidcWithClaimMapping: AuthtypesGettableAuthDomainDTO = {
|
||||
export const mockSamlWithAttributeMapping: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-7',
|
||||
name: 'saml-attrs.com',
|
||||
enabled: true,
|
||||
config: {
|
||||
kind: AuthtypesAuthDomainConfigSAMLDTOKind.saml,
|
||||
spec: {
|
||||
location: 'https://idp.saml-attrs.com/sso',
|
||||
entityId: 'urn:saml-attrs:idp',
|
||||
certificate: 'MOCK_CERTIFICATE_ATTRS',
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.saml,
|
||||
samlConfig: {
|
||||
samlIdp: 'https://idp.saml-attrs.com/sso',
|
||||
samlEntity: 'urn:saml-attrs:idp',
|
||||
samlCert: 'MOCK_CERTIFICATE_ATTRS',
|
||||
insecureSkipAuthNRequestsSigned: true,
|
||||
attributeMapping: {
|
||||
name: 'user_display_name',
|
||||
@@ -170,10 +168,10 @@ export const mockGoogleAuthWithWorkspaceGroups: AuthtypesGettableAuthDomainDTO =
|
||||
{
|
||||
id: 'domain-8',
|
||||
name: 'google-groups.com',
|
||||
enabled: true,
|
||||
config: {
|
||||
kind: AuthtypesAuthDomainConfigGoogleDTOKind.google,
|
||||
spec: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.google_auth,
|
||||
googleAuthConfig: {
|
||||
clientId: 'google-groups-client-id',
|
||||
clientSecret: 'google-groups-client-secret',
|
||||
insecureSkipEmailVerified: false,
|
||||
@@ -220,7 +218,7 @@ export const mockUpdateSuccessResponse = {
|
||||
status: 'success',
|
||||
data: {
|
||||
...mockGoogleAuthDomain,
|
||||
enabled: false,
|
||||
config: { ...mockGoogleAuthDomain.config, ssoEnabled: false },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HTMLAttributes, useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Plus, Trash2, X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
@@ -26,7 +26,7 @@ import './AuthDomain.styles.scss';
|
||||
import '../../IngestionSettings/IngestionSettings.styles.scss';
|
||||
|
||||
export const SSOType = new Map<string, string>([
|
||||
['google', 'Google Auth'],
|
||||
['google_auth', 'Google Auth'],
|
||||
['saml', 'SAML'],
|
||||
['email_password', 'Email Password'],
|
||||
['oidc', 'OIDC'],
|
||||
@@ -121,8 +121,8 @@ function AuthDomain(): JSX.Element {
|
||||
},
|
||||
{
|
||||
title: 'Enforce SSO',
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
dataIndex: ['config', 'ssoEnabled'],
|
||||
key: 'ssoEnabled',
|
||||
width: 80,
|
||||
render: (
|
||||
value: boolean,
|
||||
@@ -157,15 +157,13 @@ function AuthDomain(): JSX.Element {
|
||||
className="auth-domain-list-action-link"
|
||||
onClick={(): void => setRecord(record)}
|
||||
variant="link"
|
||||
testId="auth-domain-configure"
|
||||
>
|
||||
Configure {SSOType.get(record.config?.kind || '')}
|
||||
Configure {SSOType.get(record.config?.ssoType || '')}
|
||||
</Button>
|
||||
<Button
|
||||
className="auth-domain-list-action-link delete"
|
||||
onClick={(): void => showDeleteModal(record)}
|
||||
variant="link"
|
||||
testId="auth-domain-delete"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
@@ -179,9 +177,7 @@ function AuthDomain(): JSX.Element {
|
||||
return (
|
||||
<div className="auth-domain">
|
||||
<section className="auth-domain-header">
|
||||
<h3 className="auth-domain-title" data-testid="auth-domain-title">
|
||||
Authenticated Domains
|
||||
</h3>
|
||||
<h3 className="auth-domain-title">Authenticated Domains</h3>
|
||||
<Button
|
||||
prefix={<Plus size="md" />}
|
||||
onClick={(): void => {
|
||||
@@ -190,7 +186,6 @@ function AuthDomain(): JSX.Element {
|
||||
variant="solid"
|
||||
size="sm"
|
||||
color="primary"
|
||||
testId="auth-domain-add"
|
||||
>
|
||||
Add Domain
|
||||
</Button>
|
||||
@@ -200,14 +195,7 @@ function AuthDomain(): JSX.Element {
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={authDomainListResponse?.data}
|
||||
onRow={(
|
||||
record: AuthtypesGettableAuthDomainDTO,
|
||||
): HTMLAttributes<HTMLElement> =>
|
||||
// data-* attributes are valid row props but absent from the antd typing
|
||||
({
|
||||
'data-testid': `auth-domain-row-${record.name}`,
|
||||
}) as unknown as HTMLAttributes<HTMLElement>
|
||||
}
|
||||
onRow={undefined}
|
||||
loading={
|
||||
isLoadingAuthDomainListResponse || isFetchingAuthDomainListResponse
|
||||
}
|
||||
@@ -240,7 +228,6 @@ function AuthDomain(): JSX.Element {
|
||||
onClick={hideDeleteModal}
|
||||
className="cancel-btn"
|
||||
prefix={<X size={16} />}
|
||||
testId="auth-domain-delete-cancel"
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
@@ -250,7 +237,6 @@ function AuthDomain(): JSX.Element {
|
||||
onClick={handleDeleteDomain}
|
||||
className="delete-btn"
|
||||
loading={isLoading}
|
||||
testId="auth-domain-delete-confirm"
|
||||
>
|
||||
Delete Domain
|
||||
</Button>,
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/auth_domains", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.List), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v1/domains", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.List), handler.OpenAPIDef{
|
||||
ID: "ListAuthDomains",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "List all auth domains",
|
||||
@@ -27,7 +27,7 @@ func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/auth_domains", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Create), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v1/domains", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Create), handler.OpenAPIDef{
|
||||
ID: "CreateAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Create auth domain",
|
||||
@@ -44,7 +44,7 @@ func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/auth_domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Get), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v1/domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Get), handler.OpenAPIDef{
|
||||
ID: "GetAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Get auth domain by ID",
|
||||
@@ -61,7 +61,7 @@ func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/auth_domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Update), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v1/domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Update), handler.OpenAPIDef{
|
||||
ID: "UpdateAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Update auth domain",
|
||||
@@ -78,7 +78,7 @@ func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/auth_domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Delete), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v1/domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Delete), handler.OpenAPIDef{
|
||||
ID: "DeleteAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Delete auth domain",
|
||||
|
||||
@@ -59,11 +59,12 @@ func (a *AuthN) LoginURL(ctx context.Context, siteURL *url.URL, authDomain *auth
|
||||
return "", err
|
||||
}
|
||||
|
||||
oauth2Config, err := a.oauth2Config(siteURL, authDomain, oidcProvider)
|
||||
if err != nil {
|
||||
return "", err
|
||||
if authDomain.AuthDomainConfig().AuthNProvider != authtypes.AuthNProviderGoogleAuth {
|
||||
return "", errors.Newf(errors.TypeInternal, authtypes.ErrCodeAuthDomainMismatch, "domain type is not google")
|
||||
}
|
||||
|
||||
oauth2Config := a.oauth2Config(siteURL, authDomain, oidcProvider)
|
||||
|
||||
return oauth2Config.AuthCodeURL(
|
||||
authtypes.NewState(siteURL, authDomain.StorableAuthDomain().ID).URL.String(),
|
||||
oauth2.SetAuthURLParam("hd", authDomain.StorableAuthDomain().Name),
|
||||
@@ -92,16 +93,7 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
|
||||
return nil, err
|
||||
}
|
||||
|
||||
googleConfig, err := authDomain.Config().GoogleConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oauth2Config, err := a.oauth2Config(state.URL, authDomain, oidcProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oauth2Config := a.oauth2Config(state.URL, authDomain, oidcProvider)
|
||||
token, err := oauth2Config.Exchange(ctx, query.Get("code"))
|
||||
if err != nil {
|
||||
var retrieveError *oauth2.RetrieveError
|
||||
@@ -119,7 +111,7 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "google: no id_token in token response")
|
||||
}
|
||||
|
||||
verifier := oidcProvider.Verifier(&oidc.Config{ClientID: googleConfig.ClientID})
|
||||
verifier := oidcProvider.Verifier(&oidc.Config{ClientID: authDomain.AuthDomainConfig().Google.ClientID})
|
||||
idToken, err := verifier.Verify(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
a.settings.Logger().ErrorContext(ctx, "google: failed to verify token", errors.Attr(err))
|
||||
@@ -143,7 +135,7 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
|
||||
return nil, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "google: unexpected hd claim")
|
||||
}
|
||||
|
||||
if !googleConfig.InsecureSkipEmailVerified {
|
||||
if !authDomain.AuthDomainConfig().Google.InsecureSkipEmailVerified {
|
||||
if !claims.EmailVerified {
|
||||
a.settings.Logger().ErrorContext(ctx, "google: email is not verified", slog.String("email", claims.Email))
|
||||
return nil, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "google: email is not verified")
|
||||
@@ -156,14 +148,14 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
|
||||
}
|
||||
|
||||
var groups []string
|
||||
if googleConfig.FetchGroups {
|
||||
groups, err = a.fetchGoogleWorkspaceGroups(ctx, claims.Email, googleConfig)
|
||||
if authDomain.AuthDomainConfig().Google.FetchGroups {
|
||||
groups, err = a.fetchGoogleWorkspaceGroups(ctx, claims.Email, authDomain.AuthDomainConfig().Google)
|
||||
if err != nil {
|
||||
a.settings.Logger().ErrorContext(ctx, "google: could not fetch groups", errors.Attr(err))
|
||||
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "google: could not fetch groups").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
allowedGroups := googleConfig.AllowedGroups
|
||||
allowedGroups := authDomain.AuthDomainConfig().Google.AllowedGroups
|
||||
if len(allowedGroups) > 0 {
|
||||
groups = filterGroups(groups, allowedGroups)
|
||||
if len(groups) == 0 {
|
||||
@@ -181,15 +173,10 @@ func (a *AuthN) ProviderInfo(ctx context.Context, authDomain *authtypes.AuthDoma
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AuthN) oauth2Config(siteURL *url.URL, authDomain *authtypes.AuthDomain, provider *oidc.Provider) (*oauth2.Config, error) {
|
||||
googleConfig, err := authDomain.Config().GoogleConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (a *AuthN) oauth2Config(siteURL *url.URL, authDomain *authtypes.AuthDomain, provider *oidc.Provider) *oauth2.Config {
|
||||
return &oauth2.Config{
|
||||
ClientID: googleConfig.ClientID,
|
||||
ClientSecret: googleConfig.ClientSecret,
|
||||
ClientID: authDomain.AuthDomainConfig().Google.ClientID,
|
||||
ClientSecret: authDomain.AuthDomainConfig().Google.ClientSecret,
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: scopes,
|
||||
RedirectURL: (&url.URL{
|
||||
@@ -197,10 +184,10 @@ func (a *AuthN) oauth2Config(siteURL *url.URL, authDomain *authtypes.AuthDomain,
|
||||
Host: siteURL.Host,
|
||||
Path: path.Join(a.globalConfig.ExternalPath(), redirectPath),
|
||||
}).String(),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AuthN) fetchGoogleWorkspaceGroups(ctx context.Context, userEmail string, config authtypes.GoogleConfig) ([]string, error) {
|
||||
func (a *AuthN) fetchGoogleWorkspaceGroups(ctx context.Context, userEmail string, config *authtypes.GoogleConfig) ([]string, error) {
|
||||
adminEmail := config.GetAdminEmailForDomain(userEmail)
|
||||
if adminEmail == "" {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "no admin email configured for domain of %s", userEmail)
|
||||
|
||||
@@ -26,7 +26,7 @@ func (getter *getter) OnBeforeRoleDelete(ctx context.Context, orgID valuer.UUID,
|
||||
|
||||
referencedBy := make([]string, 0)
|
||||
for _, domain := range domains {
|
||||
for _, mappedRole := range domain.RoleMapping().RoleNames() {
|
||||
for _, mappedRole := range domain.AuthDomainConfig().RoleMapping.RoleNames() {
|
||||
if mappedRole == roleName {
|
||||
referencedBy = append(referencedBy, domain.StorableAuthDomain().Name)
|
||||
break
|
||||
|
||||
@@ -38,7 +38,7 @@ func (handler *handler) Create(rw http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
authDomain, err := authtypes.NewAuthDomainFromPostableAuthDomain(body, valuer.MustNewUUID(claims.OrgID))
|
||||
authDomain, err := authtypes.NewAuthDomainFromConfig(body.Name, &body.Config, valuer.MustNewUUID(claims.OrgID))
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
@@ -154,7 +154,7 @@ func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = authDomain.Update(body)
|
||||
err = authDomain.Update(&body.Config)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
|
||||
@@ -33,7 +33,7 @@ func (module *module) Get(ctx context.Context, id valuer.UUID) (*authtypes.AuthD
|
||||
}
|
||||
|
||||
func (module *module) GetAuthNProviderInfo(ctx context.Context, domain *authtypes.AuthDomain) *authtypes.AuthNProviderInfo {
|
||||
if callbackAuthN, ok := module.authNs[domain.Kind()].(authn.CallbackAuthN); ok {
|
||||
if callbackAuthN, ok := module.authNs[domain.AuthDomainConfig().AuthNProvider].(authn.CallbackAuthN); ok {
|
||||
return callbackAuthN.ProviderInfo(ctx, domain)
|
||||
}
|
||||
return &authtypes.AuthNProviderInfo{}
|
||||
@@ -72,7 +72,7 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
|
||||
stats := make(map[string]any)
|
||||
|
||||
for _, domain := range domains {
|
||||
key := "authdomain." + domain.Kind().StringValue() + ".count"
|
||||
key := "authdomain." + domain.AuthDomainConfig().AuthNProvider.StringValue() + ".count"
|
||||
if value, ok := stats[key]; ok {
|
||||
stats[key] = value.(int64) + 1
|
||||
} else {
|
||||
@@ -86,7 +86,7 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
|
||||
}
|
||||
|
||||
func (module *module) validateRoleMapping(ctx context.Context, domain *authtypes.AuthDomain) error {
|
||||
roleNames := domain.RoleMapping().RoleNames()
|
||||
roleNames := domain.AuthDomainConfig().RoleMapping.RoleNames()
|
||||
if len(roleNames) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func (handler *handler) CreateSessionByGoogleCallback(rw http.ResponseWriter, re
|
||||
|
||||
values := req.URL.Query()
|
||||
|
||||
redirectURL, err := handler.module.CreateCallbackAuthNSession(ctx, authtypes.AuthNProviderGoogle, values)
|
||||
redirectURL, err := handler.module.CreateCallbackAuthNSession(ctx, authtypes.AuthNProviderGoogleAuth, values)
|
||||
if err != nil {
|
||||
http.Redirect(rw, req, handler.getRedirectURLFromErr(err), http.StatusSeeOther)
|
||||
return
|
||||
|
||||
@@ -152,7 +152,7 @@ func (module *module) CreateCallbackAuthNSession(ctx context.Context, authNProvi
|
||||
return "", err
|
||||
}
|
||||
|
||||
roleMapping := authDomain.RoleMapping()
|
||||
roleMapping := authDomain.AuthDomainConfig().RoleMapping
|
||||
|
||||
roleAttributeExists := false
|
||||
if roleMapping != nil && roleMapping.UseRoleAttribute && callbackIdentity.Role != "" {
|
||||
@@ -215,11 +215,11 @@ func (module *module) getOrgSessionContext(ctx context.Context, org *types.Organ
|
||||
return authtypes.NewOrgSessionContext(org.ID, org.Name).AddPasswordAuthNSupport(authtypes.AuthNProviderEmailPassword), nil
|
||||
}
|
||||
|
||||
if !authDomain.Enabled() {
|
||||
if !authDomain.AuthDomainConfig().SSOEnabled {
|
||||
return authtypes.NewOrgSessionContext(org.ID, org.Name).AddPasswordAuthNSupport(authtypes.AuthNProviderEmailPassword), nil
|
||||
}
|
||||
|
||||
provider, err := getProvider[authn.CallbackAuthN](authDomain.Kind(), module.authNs)
|
||||
provider, err := getProvider[authn.CallbackAuthN](authDomain.AuthDomainConfig().AuthNProvider, module.authNs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -233,7 +233,7 @@ func (module *module) getOrgSessionContext(ctx context.Context, org *types.Organ
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return authtypes.NewOrgSessionContext(org.ID, org.Name).AddCallbackAuthNSupport(authDomain.Kind(), loginURL), nil
|
||||
return authtypes.NewOrgSessionContext(org.ID, org.Name).AddCallbackAuthNSupport(authDomain.AuthDomainConfig().AuthNProvider, loginURL), nil
|
||||
}
|
||||
|
||||
func getProvider[T authn.AuthN](authNProvider authtypes.AuthNProvider, authNs map[authtypes.AuthNProvider]authn.AuthN) (T, error) {
|
||||
|
||||
@@ -22,6 +22,6 @@ func NewAuthNs(ctx context.Context, providerSettings factory.ProviderSettings, s
|
||||
|
||||
return map[authtypes.AuthNProvider]authn.AuthN{
|
||||
authtypes.AuthNProviderEmailPassword: emailPasswordAuthN,
|
||||
authtypes.AuthNProviderGoogle: googleCallbackAuthN,
|
||||
authtypes.AuthNProviderGoogleAuth: googleCallbackAuthN,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -237,7 +237,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddDashboardTuplesFactory(sqlstore),
|
||||
sqlmigration.NewRestructureSavedViewSpecFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddSavedViewTuplesFactory(sqlstore),
|
||||
sqlmigration.NewRestructureAuthDomainConfigFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type restructureAuthDomainConfig struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
type restructureAuthDomainRow struct {
|
||||
bun.BaseModel `bun:"table:auth_domain"`
|
||||
|
||||
ID string `bun:"id"`
|
||||
Data string `bun:"data"`
|
||||
}
|
||||
|
||||
// The legacy document keyed the discriminator as ssoType with the chosen
|
||||
// provider's config in a sibling field; the restructured document is
|
||||
// {enabled, config: {kind, spec}, roleMapping} with renamed saml spec keys.
|
||||
var legacySSOTypeToKind = map[string]string{
|
||||
"google_auth": "google",
|
||||
"saml": "saml",
|
||||
"oidc": "oidc",
|
||||
}
|
||||
|
||||
var legacySSOTypeToConfigKey = map[string]string{
|
||||
"google_auth": "googleAuthConfig",
|
||||
"saml": "samlConfig",
|
||||
"oidc": "oidcConfig",
|
||||
}
|
||||
|
||||
var legacySamlKeyToKey = map[string]string{
|
||||
"samlEntity": "entityId",
|
||||
"samlIdp": "location",
|
||||
"samlCert": "certificate",
|
||||
}
|
||||
|
||||
func NewRestructureAuthDomainConfigFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("restructure_auth_domain_config"),
|
||||
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &restructureAuthDomainConfig{sqlstore: sqlstore, logger: ps.Logger}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (migration *restructureAuthDomainConfig) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *restructureAuthDomainConfig) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
rows := make([]*restructureAuthDomainRow, 0)
|
||||
if err := tx.NewSelect().Model(&rows).Scan(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
legacy := make(map[string]json.RawMessage)
|
||||
if err := json.Unmarshal([]byte(row.Data), &legacy); err != nil {
|
||||
migration.logger.WarnContext(ctx, "skipping auth domain with unreadable data", slog.String("auth_domain_id", row.ID), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
|
||||
ssoTypeRaw, ok := legacy["ssoType"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var ssoType string
|
||||
if err := json.Unmarshal(ssoTypeRaw, &ssoType); err != nil {
|
||||
migration.logger.WarnContext(ctx, "skipping auth domain with unreadable ssoType", slog.String("auth_domain_id", row.ID), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
|
||||
kind, ok := legacySSOTypeToKind[ssoType]
|
||||
if !ok {
|
||||
migration.logger.WarnContext(ctx, "skipping auth domain with unknown ssoType", slog.String("auth_domain_id", row.ID), slog.String("sso_type", ssoType))
|
||||
continue
|
||||
}
|
||||
|
||||
spec, ok := legacy[legacySSOTypeToConfigKey[ssoType]]
|
||||
if !ok || string(spec) == "null" {
|
||||
migration.logger.WarnContext(ctx, "skipping auth domain with missing provider config", slog.String("auth_domain_id", row.ID), slog.String("sso_type", ssoType))
|
||||
continue
|
||||
}
|
||||
|
||||
if ssoType == "saml" {
|
||||
samlSpec := make(map[string]json.RawMessage)
|
||||
if err := json.Unmarshal(spec, &samlSpec); err != nil {
|
||||
migration.logger.WarnContext(ctx, "skipping auth domain with unreadable saml config", slog.String("auth_domain_id", row.ID), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
|
||||
for legacyKey, key := range legacySamlKeyToKey {
|
||||
if value, ok := samlSpec[legacyKey]; ok {
|
||||
samlSpec[key] = value
|
||||
delete(samlSpec, legacyKey)
|
||||
}
|
||||
}
|
||||
|
||||
if spec, err = json.Marshal(samlSpec); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if ssoType == "google_auth" {
|
||||
googleSpec := make(map[string]json.RawMessage)
|
||||
if err := json.Unmarshal(spec, &googleSpec); err != nil {
|
||||
migration.logger.WarnContext(ctx, "skipping auth domain with unreadable google config", slog.String("auth_domain_id", row.ID), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
|
||||
delete(googleSpec, "redirectURI")
|
||||
|
||||
if spec, err = json.Marshal(googleSpec); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
kindRaw, err := json.Marshal(kind)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := json.Marshal(map[string]json.RawMessage{
|
||||
"kind": kindRaw,
|
||||
"spec": spec,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
restructured := map[string]json.RawMessage{
|
||||
"enabled": json.RawMessage("false"),
|
||||
"config": config,
|
||||
}
|
||||
if enabled, ok := legacy["ssoEnabled"]; ok {
|
||||
restructured["enabled"] = enabled
|
||||
}
|
||||
if roleMapping, ok := legacy["roleMapping"]; ok && string(roleMapping) != "null" {
|
||||
restructured["roleMapping"] = roleMapping
|
||||
}
|
||||
|
||||
newData, err := json.Marshal(restructured)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.NewUpdate().
|
||||
Model((*restructureAuthDomainRow)(nil)).
|
||||
Set("data = ?", string(newData)).
|
||||
Where("id = ?", row.ID).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *restructureAuthDomainConfig) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -16,7 +16,7 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
AuthNProviderGoogle = AuthNProvider{valuer.NewString("google")}
|
||||
AuthNProviderGoogleAuth = AuthNProvider{valuer.NewString("google_auth")}
|
||||
AuthNProviderSAML = AuthNProvider{valuer.NewString("saml")}
|
||||
AuthNProviderEmailPassword = AuthNProvider{valuer.NewString("email_password")}
|
||||
AuthNProviderOIDC = AuthNProvider{valuer.NewString("oidc")}
|
||||
@@ -158,7 +158,7 @@ func (typ *Identity) ToClaims() Claims {
|
||||
|
||||
func (AuthNProvider) Enum() []any {
|
||||
return []any{
|
||||
AuthNProviderGoogle,
|
||||
AuthNProviderGoogleAuth,
|
||||
AuthNProviderSAML,
|
||||
AuthNProviderEmailPassword,
|
||||
AuthNProviderOIDC,
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
@@ -29,58 +28,9 @@ var (
|
||||
ErrCodeAuthDomainAlreadyExists = errors.MustNewCode("auth_domain_already_exists")
|
||||
)
|
||||
|
||||
// authDomainConfigVariants is the single registry of authn provider kinds:
|
||||
// UnmarshalJSON, JSONSchemaOneOf and the discriminator mapping all derive from
|
||||
// it, so a new provider is one entry here plus its authn registration.
|
||||
var authDomainConfigVariants = []authDomainConfigVariant{
|
||||
{
|
||||
kind: AuthNProviderSAML,
|
||||
decodeSpec: func(data []byte) (any, error) {
|
||||
spec := SamlConfig{}
|
||||
if err := json.Unmarshal(data, &spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return spec, nil
|
||||
},
|
||||
schema: authDomainConfigSAML{},
|
||||
schemaRef: "#/components/schemas/AuthtypesAuthDomainConfigSAML",
|
||||
},
|
||||
{
|
||||
kind: AuthNProviderGoogle,
|
||||
decodeSpec: func(data []byte) (any, error) {
|
||||
spec := GoogleConfig{}
|
||||
if err := json.Unmarshal(data, &spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return spec, nil
|
||||
},
|
||||
schema: authDomainConfigGoogle{},
|
||||
schemaRef: "#/components/schemas/AuthtypesAuthDomainConfigGoogle",
|
||||
},
|
||||
{
|
||||
kind: AuthNProviderOIDC,
|
||||
decodeSpec: func(data []byte) (any, error) {
|
||||
spec := OIDCConfig{}
|
||||
if err := json.Unmarshal(data, &spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return spec, nil
|
||||
},
|
||||
schema: authDomainConfigOIDC{},
|
||||
schemaRef: "#/components/schemas/AuthtypesAuthDomainConfigOIDC",
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
_ jsonschema.OneOfExposer = AuthDomainConfig{}
|
||||
_ jsonschema.Preparer = AuthDomainConfig{}
|
||||
)
|
||||
|
||||
type GettableAuthDomain struct {
|
||||
StorableAuthDomain
|
||||
Enabled bool `json:"enabled"`
|
||||
Config AuthDomainConfig `json:"config"`
|
||||
RoleMapping *RoleMapping `json:"roleMapping"`
|
||||
AuthNProviderInfo *AuthNProviderInfo `json:"authNProviderInfo"`
|
||||
}
|
||||
|
||||
@@ -89,16 +39,12 @@ type AuthNProviderInfo struct {
|
||||
}
|
||||
|
||||
type PostableAuthDomain struct {
|
||||
Name string `json:"name" required:"true"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Config AuthDomainConfig `json:"config" required:"true"`
|
||||
RoleMapping *RoleMapping `json:"roleMapping"`
|
||||
Config AuthDomainConfig `json:"config"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type UpdatableAuthDomain struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Config AuthDomainConfig `json:"config" required:"true"`
|
||||
RoleMapping *RoleMapping `json:"roleMapping"`
|
||||
Config AuthDomainConfig `json:"config"`
|
||||
}
|
||||
|
||||
type StorableAuthDomain struct {
|
||||
@@ -111,46 +57,150 @@ type StorableAuthDomain struct {
|
||||
types.TimeAuditable
|
||||
}
|
||||
|
||||
// StorableAuthDomainConfig is the JSON document persisted in StorableAuthDomain.Data.
|
||||
type StorableAuthDomainConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Config AuthDomainConfig `json:"config"`
|
||||
RoleMapping *RoleMapping `json:"roleMapping"`
|
||||
}
|
||||
|
||||
// TODO: the oneOf emitted by JSONSchemaOneOf is not the shape OpenAPI wants
|
||||
// for a discriminated union. OpenAPI's discriminator requires every oneOf
|
||||
// branch to be a $ref to a named component and a sibling property whose value
|
||||
// selects the variant. ssoType is already discriminator-shaped, but the
|
||||
// variant payload lives in a sibling field (samlConfig / googleAuthConfig /
|
||||
// oidcConfig) instead of being the payload itself, so no discriminator can
|
||||
// be attached. Refactor AuthDomainConfig into an envelope (see
|
||||
// ruletypes.RuleThresholdData for the pattern) where the chosen config is
|
||||
// the payload and ssoType is the discriminator.
|
||||
type AuthDomainConfig struct {
|
||||
Kind AuthNProvider `json:"kind" required:"true"`
|
||||
Spec any `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
// authDomainConfigSAML is the OpenAPI schema for an AuthDomainConfig with kind=saml.
|
||||
type authDomainConfigSAML struct {
|
||||
Kind AuthNProvider `json:"kind" description:"The kind of authn provider." required:"true"`
|
||||
Spec SamlConfig `json:"spec" description:"The saml configuration." required:"true"`
|
||||
}
|
||||
|
||||
// authDomainConfigGoogle is the OpenAPI schema for an AuthDomainConfig with kind=google.
|
||||
type authDomainConfigGoogle struct {
|
||||
Kind AuthNProvider `json:"kind" description:"The kind of authn provider." required:"true"`
|
||||
Spec GoogleConfig `json:"spec" description:"The google auth configuration." required:"true"`
|
||||
}
|
||||
|
||||
// authDomainConfigOIDC is the OpenAPI schema for an AuthDomainConfig with kind=oidc.
|
||||
type authDomainConfigOIDC struct {
|
||||
Kind AuthNProvider `json:"kind" description:"The kind of authn provider." required:"true"`
|
||||
Spec OIDCConfig `json:"spec" description:"The oidc configuration." required:"true"`
|
||||
}
|
||||
|
||||
type authDomainConfigVariant struct {
|
||||
kind AuthNProvider
|
||||
decodeSpec func(data []byte) (any, error)
|
||||
schema any
|
||||
schemaRef string
|
||||
SSOEnabled bool `json:"ssoEnabled"`
|
||||
AuthNProvider AuthNProvider `json:"ssoType"`
|
||||
SAML *SamlConfig `json:"samlConfig"`
|
||||
Google *GoogleConfig `json:"googleAuthConfig"`
|
||||
OIDC *OIDCConfig `json:"oidcConfig"`
|
||||
RoleMapping *RoleMapping `json:"roleMapping"`
|
||||
}
|
||||
|
||||
type AuthDomain struct {
|
||||
storableAuthDomain *StorableAuthDomain
|
||||
storableAuthDomainConfig *StorableAuthDomainConfig
|
||||
storableAuthDomain *StorableAuthDomain
|
||||
authDomainConfig *AuthDomainConfig
|
||||
}
|
||||
|
||||
func NewAuthDomainFromConfig(name string, config *AuthDomainConfig, orgID valuer.UUID) (*AuthDomain, error) {
|
||||
data, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewAuthDomain(name, string(data), orgID)
|
||||
}
|
||||
|
||||
func NewAuthDomain(name string, data string, orgID valuer.UUID) (*AuthDomain, error) {
|
||||
storableAuthDomain := &StorableAuthDomain{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
Name: name,
|
||||
Data: data,
|
||||
OrgID: orgID,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
return NewAuthDomainFromStorableAuthDomain(storableAuthDomain)
|
||||
}
|
||||
|
||||
func NewAuthDomainFromStorableAuthDomain(storableAuthDomain *StorableAuthDomain) (*AuthDomain, error) {
|
||||
authDomainConfig := new(AuthDomainConfig)
|
||||
if err := json.Unmarshal([]byte(storableAuthDomain.Data), authDomainConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AuthDomain{
|
||||
storableAuthDomain: storableAuthDomain,
|
||||
authDomainConfig: authDomainConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewGettableAuthDomainFromAuthDomain(authDomain *AuthDomain, authNProviderInfo *AuthNProviderInfo) *GettableAuthDomain {
|
||||
return &GettableAuthDomain{
|
||||
StorableAuthDomain: *authDomain.StorableAuthDomain(),
|
||||
Config: *authDomain.AuthDomainConfig(),
|
||||
AuthNProviderInfo: authNProviderInfo,
|
||||
}
|
||||
}
|
||||
|
||||
func (typ *AuthDomain) StorableAuthDomain() *StorableAuthDomain {
|
||||
return typ.storableAuthDomain
|
||||
}
|
||||
|
||||
func (typ *AuthDomain) AuthDomainConfig() *AuthDomainConfig {
|
||||
return typ.authDomainConfig
|
||||
}
|
||||
|
||||
func (typ *AuthDomain) Update(config *AuthDomainConfig) error {
|
||||
data, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
typ.authDomainConfig = config
|
||||
typ.storableAuthDomain.Data = string(data)
|
||||
typ.storableAuthDomain.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (typ *PostableAuthDomain) UnmarshalJSON(data []byte) error {
|
||||
type Alias PostableAuthDomain
|
||||
|
||||
var temp Alias
|
||||
if err := json.Unmarshal(data, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !authDomainNameRegex.MatchString(temp.Name) {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidName, "invalid domain name %s", temp.Name)
|
||||
}
|
||||
|
||||
*typ = PostableAuthDomain(temp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (typ *AuthDomainConfig) UnmarshalJSON(data []byte) error {
|
||||
type Alias AuthDomainConfig
|
||||
|
||||
var temp Alias
|
||||
if err := json.Unmarshal(data, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch temp.AuthNProvider {
|
||||
case AuthNProviderGoogleAuth:
|
||||
if temp.Google == nil {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "google auth config is required")
|
||||
}
|
||||
|
||||
case AuthNProviderSAML:
|
||||
if temp.SAML == nil {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "saml config is required")
|
||||
}
|
||||
|
||||
case AuthNProviderOIDC:
|
||||
if temp.OIDC == nil {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "oidc config is required")
|
||||
}
|
||||
|
||||
default:
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "invalid authn provider %q", temp.AuthNProvider.StringValue())
|
||||
}
|
||||
|
||||
*typ = AuthDomainConfig(temp)
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (AuthDomainConfig) JSONSchemaOneOf() []any {
|
||||
return []any{
|
||||
SamlConfig{},
|
||||
GoogleConfig{},
|
||||
OIDCConfig{},
|
||||
}
|
||||
}
|
||||
|
||||
type AuthDomainStore interface {
|
||||
@@ -178,230 +228,3 @@ type AuthDomainStore interface {
|
||||
// Delete by orgID and id.
|
||||
Delete(context.Context, valuer.UUID, valuer.UUID) error
|
||||
}
|
||||
|
||||
func NewAuthDomainFromPostableAuthDomain(postableAuthDomain *PostableAuthDomain, orgID valuer.UUID) (*AuthDomain, error) {
|
||||
storableAuthDomainConfig := &StorableAuthDomainConfig{
|
||||
Enabled: postableAuthDomain.Enabled,
|
||||
Config: postableAuthDomain.Config,
|
||||
RoleMapping: postableAuthDomain.RoleMapping,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(storableAuthDomainConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AuthDomain{
|
||||
storableAuthDomain: &StorableAuthDomain{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
Name: postableAuthDomain.Name,
|
||||
Data: string(data),
|
||||
OrgID: orgID,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
},
|
||||
storableAuthDomainConfig: storableAuthDomainConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewAuthDomainFromStorableAuthDomain(storableAuthDomain *StorableAuthDomain) (*AuthDomain, error) {
|
||||
storableAuthDomainConfig := new(StorableAuthDomainConfig)
|
||||
if err := json.Unmarshal([]byte(storableAuthDomain.Data), storableAuthDomainConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AuthDomain{
|
||||
storableAuthDomain: storableAuthDomain,
|
||||
storableAuthDomainConfig: storableAuthDomainConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewGettableAuthDomainFromAuthDomain(authDomain *AuthDomain, authNProviderInfo *AuthNProviderInfo) *GettableAuthDomain {
|
||||
return &GettableAuthDomain{
|
||||
StorableAuthDomain: *authDomain.StorableAuthDomain(),
|
||||
Enabled: authDomain.Enabled(),
|
||||
Config: authDomain.Config(),
|
||||
RoleMapping: authDomain.RoleMapping(),
|
||||
AuthNProviderInfo: authNProviderInfo,
|
||||
}
|
||||
}
|
||||
|
||||
// JSONSchemaOneOf returns the oneOf variants for the AuthDomainConfig discriminated union.
|
||||
// Each variant represents a different authn provider kind with its corresponding spec schema.
|
||||
func (AuthDomainConfig) JSONSchemaOneOf() []any {
|
||||
oneOf := make([]any, len(authDomainConfigVariants))
|
||||
for i, variant := range authDomainConfigVariants {
|
||||
oneOf[i] = variant.schema
|
||||
}
|
||||
|
||||
return oneOf
|
||||
}
|
||||
|
||||
// PrepareJSONSchema marks the schema with x-signoz-discriminator;
|
||||
// signoz.attachDiscriminators promotes it to a real OpenAPI 3
|
||||
// discriminator after reflection.
|
||||
func (AuthDomainConfig) PrepareJSONSchema(schema *jsonschema.Schema) error {
|
||||
if schema.ExtraProperties == nil {
|
||||
schema.ExtraProperties = map[string]any{}
|
||||
}
|
||||
|
||||
mapping := make(map[string]string, len(authDomainConfigVariants))
|
||||
for _, variant := range authDomainConfigVariants {
|
||||
mapping[variant.kind.StringValue()] = variant.schemaRef
|
||||
}
|
||||
|
||||
schema.ExtraProperties["x-signoz-discriminator"] = map[string]any{
|
||||
"propertyName": "kind",
|
||||
"mapping": mapping,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (typ *AuthDomainConfig) UnmarshalJSON(data []byte) error {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "failed to unmarshal auth domain config")
|
||||
}
|
||||
|
||||
kindData, ok := raw["kind"]
|
||||
if !ok {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "kind is required")
|
||||
}
|
||||
|
||||
var kind AuthNProvider
|
||||
if err := json.Unmarshal(kindData, &kind); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "failed to unmarshal kind")
|
||||
}
|
||||
|
||||
specData, ok := raw["spec"]
|
||||
if !ok {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "spec is required")
|
||||
}
|
||||
|
||||
for _, variant := range authDomainConfigVariants {
|
||||
if variant.kind != kind {
|
||||
continue
|
||||
}
|
||||
|
||||
spec, err := variant.decodeSpec(specData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
typ.Kind = kind
|
||||
typ.Spec = spec
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "invalid authn provider %q", kind.StringValue())
|
||||
}
|
||||
|
||||
func (config AuthDomainConfig) SamlConfig() (SamlConfig, error) {
|
||||
spec, ok := config.Spec.(SamlConfig)
|
||||
if !ok {
|
||||
return SamlConfig{}, errors.Newf(errors.TypeInternal, ErrCodeAuthDomainMismatch, "auth domain config is not saml")
|
||||
}
|
||||
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
func (config AuthDomainConfig) GoogleConfig() (GoogleConfig, error) {
|
||||
spec, ok := config.Spec.(GoogleConfig)
|
||||
if !ok {
|
||||
return GoogleConfig{}, errors.Newf(errors.TypeInternal, ErrCodeAuthDomainMismatch, "auth domain config is not google")
|
||||
}
|
||||
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
func (config AuthDomainConfig) OIDCConfig() (OIDCConfig, error) {
|
||||
spec, ok := config.Spec.(OIDCConfig)
|
||||
if !ok {
|
||||
return OIDCConfig{}, errors.Newf(errors.TypeInternal, ErrCodeAuthDomainMismatch, "auth domain config is not oidc")
|
||||
}
|
||||
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
func (typ *AuthDomain) StorableAuthDomain() *StorableAuthDomain {
|
||||
return typ.storableAuthDomain
|
||||
}
|
||||
|
||||
func (typ *AuthDomain) Enabled() bool {
|
||||
return typ.storableAuthDomainConfig.Enabled
|
||||
}
|
||||
|
||||
func (typ *AuthDomain) Kind() AuthNProvider {
|
||||
return typ.storableAuthDomainConfig.Config.Kind
|
||||
}
|
||||
|
||||
func (typ *AuthDomain) Config() AuthDomainConfig {
|
||||
return typ.storableAuthDomainConfig.Config
|
||||
}
|
||||
|
||||
func (typ *AuthDomain) RoleMapping() *RoleMapping {
|
||||
return typ.storableAuthDomainConfig.RoleMapping
|
||||
}
|
||||
|
||||
func (typ *AuthDomain) Update(updatableAuthDomain *UpdatableAuthDomain) error {
|
||||
storableAuthDomainConfig := &StorableAuthDomainConfig{
|
||||
Enabled: updatableAuthDomain.Enabled,
|
||||
Config: updatableAuthDomain.Config,
|
||||
RoleMapping: updatableAuthDomain.RoleMapping,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(storableAuthDomainConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
typ.storableAuthDomainConfig = storableAuthDomainConfig
|
||||
typ.storableAuthDomain.Data = string(data)
|
||||
typ.storableAuthDomain.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (typ *PostableAuthDomain) UnmarshalJSON(data []byte) error {
|
||||
type Alias PostableAuthDomain
|
||||
|
||||
var temp Alias
|
||||
if err := json.Unmarshal(data, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !authDomainNameRegex.MatchString(temp.Name) {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidName, "invalid domain name %s", temp.Name)
|
||||
}
|
||||
|
||||
// A present config always carries a kind (its UnmarshalJSON rejects
|
||||
// anything else), so a zero kind means the key was absent.
|
||||
if temp.Config.Kind.IsZero() {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "config is required")
|
||||
}
|
||||
|
||||
*typ = PostableAuthDomain(temp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (typ *UpdatableAuthDomain) UnmarshalJSON(data []byte) error {
|
||||
type Alias UpdatableAuthDomain
|
||||
|
||||
var temp Alias
|
||||
if err := json.Unmarshal(data, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// A present config always carries a kind (its UnmarshalJSON rejects
|
||||
// anything else), so a zero kind means the key was absent.
|
||||
if temp.Config.Kind.IsZero() {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "config is required")
|
||||
}
|
||||
|
||||
*typ = UpdatableAuthDomain(temp)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,10 +12,13 @@ const wildCardDomain = "*"
|
||||
|
||||
type GoogleConfig struct {
|
||||
// ClientID is the application's ID. For example, 292085223830.apps.googleusercontent.com.
|
||||
ClientID string `json:"clientId" required:"true"`
|
||||
ClientID string `json:"clientId"`
|
||||
|
||||
// It is the application's secret.
|
||||
ClientSecret string `json:"clientSecret" required:"true" format:"password"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
|
||||
// What is the meaning of this? Should we remove this?
|
||||
RedirectURI string `json:"redirectURI"`
|
||||
|
||||
// Whether to fetch the Google workspace groups (required additional API scopes)
|
||||
FetchGroups bool `json:"fetchGroups"`
|
||||
@@ -23,7 +26,7 @@ type GoogleConfig struct {
|
||||
// Service Account creds JSON stored for Google Admin SDK access
|
||||
// This is content of the JSON file stored directly into db as string
|
||||
// Required if FetchGroups is true (unless running on GCE with default credentials)
|
||||
ServiceAccountJSON string `json:"serviceAccountJson,omitempty" format:"password"`
|
||||
ServiceAccountJSON string `json:"serviceAccountJson,omitempty"`
|
||||
|
||||
// Map of workspace domain to admin email for service account impersonation
|
||||
// The service account will impersonate this admin to call the directory API
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
type OIDCConfig struct {
|
||||
// It is the URL identifier for the service. For example: "https://accounts.google.com" or "https://login.salesforce.com".
|
||||
Issuer string `json:"issuer" required:"true"`
|
||||
Issuer string `json:"issuer"`
|
||||
|
||||
// Some offspec providers like Azure, Oracle IDCS have oidc discovery url different from issuer url which causes issuerValidation to fail
|
||||
// This provides a way to override the Issuer url from the .well-known/openid-configuration issuer
|
||||
@@ -16,10 +16,10 @@ type OIDCConfig struct {
|
||||
IssuerAlias string `json:"issuerAlias"`
|
||||
|
||||
// It is the application's ID.
|
||||
ClientID string `json:"clientId" required:"true"`
|
||||
ClientID string `json:"clientId"`
|
||||
|
||||
// It is the application's secret.
|
||||
ClientSecret string `json:"clientSecret" required:"true" format:"password"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
|
||||
// Mapping of claims to the corresponding fields in the token.
|
||||
ClaimMapping AttributeMapping `json:"claimMapping"`
|
||||
|
||||
@@ -7,14 +7,14 @@ import (
|
||||
)
|
||||
|
||||
type SamlConfig struct {
|
||||
// The entityID of the SAML identity provider. It can typically be found in the EntityID attribute of the EntityDescriptor element in the SAML metadata of the identity provider. Example: <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="{entityId}">
|
||||
EntityID string `json:"entityId" required:"true"`
|
||||
// The entityID of the SAML identity provider. It can typically be found in the EntityID attribute of the EntityDescriptor element in the SAML metadata of the identity provider. Example: <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="{samlEntity}">
|
||||
SamlEntity string `json:"samlEntity"`
|
||||
|
||||
// The SSO endpoint of the SAML identity provider. It can typically be found in the Location attribute of the SingleSignOnService element in the SAML metadata of the identity provider. Example: <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="{location}"/>
|
||||
Location string `json:"location" required:"true"`
|
||||
// The SSO endpoint of the SAML identity provider. It can typically be found in the SingleSignOnService element in the SAML metadata of the identity provider. Example: <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="{samlIdp}"/>
|
||||
SamlIdp string `json:"samlIdp"`
|
||||
|
||||
// The certificate of the SAML identity provider. It can typically be found in the X509Certificate element in the SAML metadata of the identity provider. Example: <ds:X509Certificate><ds:X509Certificate>{certificate}</ds:X509Certificate></ds:X509Certificate>
|
||||
Certificate string `json:"certificate" required:"true"`
|
||||
// The certificate of the SAML identity provider. It can typically be found in the X509Certificate element in the SAML metadata of the identity provider. Example: <ds:X509Certificate><ds:X509Certificate>{samlCert}</ds:X509Certificate></ds:X509Certificate>
|
||||
SamlCert string `json:"samlCert"`
|
||||
|
||||
// Whether to skip signing the SAML requests. It can typically be found in the WantAuthnRequestsSigned attribute of the IDPSSODescriptor element in the SAML metadata of the identity provider. Example: <md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
// For providers like jumpcloud, this should be set to true.
|
||||
@@ -33,34 +33,24 @@ func (config *SamlConfig) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
samlConfig := SamlConfig(temp)
|
||||
if err := samlConfig.validate(); err != nil {
|
||||
return err
|
||||
if temp.SamlEntity == "" {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "samlEntity is required")
|
||||
}
|
||||
|
||||
*config = samlConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
// validate also assigns the default attribute mapping when none is present.
|
||||
func (config *SamlConfig) validate() error {
|
||||
if config.EntityID == "" {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "entityId is required")
|
||||
if temp.SamlIdp == "" {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "samlIdp is required")
|
||||
}
|
||||
|
||||
if config.Location == "" {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "location is required")
|
||||
if temp.SamlCert == "" {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "samlCert is required")
|
||||
}
|
||||
|
||||
if config.Certificate == "" {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "certificate is required")
|
||||
}
|
||||
|
||||
if config.AttributeMapping == (AttributeMapping{}) {
|
||||
if err := json.Unmarshal([]byte("{}"), &config.AttributeMapping); err != nil {
|
||||
if temp.AttributeMapping == (AttributeMapping{}) {
|
||||
if err := json.Unmarshal([]byte("{}"), &temp.AttributeMapping); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
*config = SamlConfig(temp)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
import { authToken } from './common';
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────
|
||||
|
||||
export const ORG_SETTINGS_PATH = '/settings/org-settings';
|
||||
|
||||
// ─── Types ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface GoogleAuthDomainSeed {
|
||||
/** Domain name (e.g. `sso-edit.example.com`). Keep unique per test. */
|
||||
name: string;
|
||||
/** Enforce-SSO flag. Defaults to false. */
|
||||
enabled?: boolean;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
/**
|
||||
* Enables Google Workspace group fetching. The backend then requires
|
||||
* `serviceAccountJson` and `domainToAdminEmail`, and only with it may
|
||||
* `allowedGroups` be set.
|
||||
*/
|
||||
fetchGroups?: boolean;
|
||||
serviceAccountJson?: string;
|
||||
domainToAdminEmail?: Record<string, string>;
|
||||
allowedGroups?: string[];
|
||||
roleMapping?: {
|
||||
defaultRole?: string;
|
||||
groupMappings?: Record<string, string>;
|
||||
useRoleAttribute?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// ─── API helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Seed a Google auth domain via POST /api/v2/auth_domains. Returns the new
|
||||
* domain ID. Pair with {@link deleteAuthDomainByNameViaApi} for cleanup.
|
||||
*/
|
||||
export async function createGoogleAuthDomainViaApi(
|
||||
page: Page,
|
||||
seed: GoogleAuthDomainSeed,
|
||||
): Promise<string> {
|
||||
const token = await authToken(page);
|
||||
|
||||
const spec: Record<string, unknown> = {
|
||||
clientId: seed.clientId ?? 'e2e-client-id.apps.googleusercontent.com',
|
||||
clientSecret: seed.clientSecret ?? 'e2e-client-secret',
|
||||
fetchGroups: seed.fetchGroups ?? false,
|
||||
insecureSkipEmailVerified: false,
|
||||
};
|
||||
if (seed.serviceAccountJson) {
|
||||
spec.serviceAccountJson = seed.serviceAccountJson;
|
||||
}
|
||||
if (seed.domainToAdminEmail) {
|
||||
spec.domainToAdminEmail = seed.domainToAdminEmail;
|
||||
}
|
||||
if (seed.allowedGroups) {
|
||||
spec.allowedGroups = seed.allowedGroups;
|
||||
}
|
||||
|
||||
const res = await page.request.post('/api/v2/auth_domains', {
|
||||
data: {
|
||||
name: seed.name,
|
||||
enabled: seed.enabled ?? false,
|
||||
config: { kind: 'google', spec },
|
||||
roleMapping: seed.roleMapping,
|
||||
},
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok()) {
|
||||
throw new Error(
|
||||
`POST /api/v2/auth_domains ${res.status()}: ${await res.text()}`,
|
||||
);
|
||||
}
|
||||
const json = (await res.json()) as { data: { id: string } };
|
||||
return json.data.id;
|
||||
}
|
||||
|
||||
/** Delete an auth domain by ID (best-effort cleanup). */
|
||||
export async function deleteAuthDomainViaApi(
|
||||
page: Page,
|
||||
id: string,
|
||||
): Promise<void> {
|
||||
const token = await authToken(page);
|
||||
await page.request.delete(`/api/v2/auth_domains/${id}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete any auth domain named `name`; a no-op when absent. Doubles as the
|
||||
* leftover guard before seeding (domain names are unique per org, so a
|
||||
* crashed earlier run would otherwise make the seed conflict).
|
||||
*/
|
||||
export async function deleteAuthDomainByNameViaApi(
|
||||
page: Page,
|
||||
name: string,
|
||||
): Promise<void> {
|
||||
const token = await authToken(page);
|
||||
const res = await page.request.get('/api/v2/auth_domains', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok()) {
|
||||
throw new Error(
|
||||
`GET /api/v2/auth_domains ${res.status()}: ${await res.text()}`,
|
||||
);
|
||||
}
|
||||
const json = (await res.json()) as {
|
||||
data: Array<{ id: string; name: string }> | null;
|
||||
};
|
||||
const match = (json.data ?? []).find((domain) => domain.name === name);
|
||||
if (match) {
|
||||
await deleteAuthDomainViaApi(page, match.id);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Navigation ────────────────────────────────────────────────────────────
|
||||
|
||||
/** Open org settings and wait for the Authenticated Domains section. */
|
||||
export async function gotoAuthDomains(page: Page): Promise<void> {
|
||||
await page.goto(ORG_SETTINGS_PATH);
|
||||
await expect(page.getByTestId('auth-domain-title')).toBeVisible();
|
||||
}
|
||||
|
||||
/** Open the Configure (edit) modal for the domain row named `name`. */
|
||||
export async function openConfigureAuthDomain(
|
||||
page: Page,
|
||||
name: string,
|
||||
): Promise<void> {
|
||||
await page
|
||||
.getByTestId(`auth-domain-row-${name}`)
|
||||
.getByTestId('auth-domain-configure')
|
||||
.click();
|
||||
await expect(page.getByTestId('auth-domain-form')).toBeVisible();
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
import { expect, test } from '../../fixtures/auth';
|
||||
import {
|
||||
createGoogleAuthDomainViaApi,
|
||||
deleteAuthDomainByNameViaApi,
|
||||
gotoAuthDomains,
|
||||
openConfigureAuthDomain,
|
||||
ORG_SETTINGS_PATH,
|
||||
} from '../../helpers/sso';
|
||||
|
||||
// Every test seeds its own uniquely-named domain so the file can run fully
|
||||
// parallel. Names are registered here and removed by the afterEach guard;
|
||||
// the delete-by-name call before each seed clears leftovers of crashed runs.
|
||||
const cleanupNames: string[] = [];
|
||||
|
||||
test.afterEach(async ({ authedPage: page }) => {
|
||||
for (const name of cleanupNames.splice(0)) {
|
||||
await deleteAuthDomainByNameViaApi(page, name);
|
||||
}
|
||||
});
|
||||
|
||||
test('TC-01 org settings shows the authenticated domains section', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
await page.goto(ORG_SETTINGS_PATH);
|
||||
await expect(page.getByTestId('auth-domain-title')).toBeVisible();
|
||||
await expect(page.getByTestId('auth-domain-add')).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-02 create a google auth domain via the UI', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const domain = 'sso-create.example.com';
|
||||
cleanupNames.push(domain);
|
||||
await deleteAuthDomainByNameViaApi(page, domain);
|
||||
|
||||
await gotoAuthDomains(page);
|
||||
await page.getByTestId('auth-domain-add').click();
|
||||
await page.getByTestId('authn-provider-configure-google').click();
|
||||
|
||||
await page.getByTestId('google-auth-domain').fill(domain);
|
||||
await page
|
||||
.getByTestId('google-auth-client-id')
|
||||
.fill('e2e-client-id.apps.googleusercontent.com');
|
||||
await page.getByTestId('google-auth-client-secret').fill('e2e-client-secret');
|
||||
await page.getByTestId('auth-domain-save').click();
|
||||
|
||||
await expect(page.getByText('Domain created successfully')).toBeVisible();
|
||||
const row = page.getByTestId(`auth-domain-row-${domain}`);
|
||||
await expect(row).toBeVisible();
|
||||
await expect(row.getByTestId('auth-domain-configure')).toHaveText(
|
||||
'Configure Google Auth',
|
||||
);
|
||||
});
|
||||
|
||||
test('TC-03 editing the client id persists across reopen', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const domain = 'sso-edit.example.com';
|
||||
cleanupNames.push(domain);
|
||||
await deleteAuthDomainByNameViaApi(page, domain);
|
||||
await createGoogleAuthDomainViaApi(page, { name: domain });
|
||||
|
||||
await gotoAuthDomains(page);
|
||||
await openConfigureAuthDomain(page, domain);
|
||||
await expect(page.getByTestId('google-auth-client-id')).toHaveValue(
|
||||
'e2e-client-id.apps.googleusercontent.com',
|
||||
);
|
||||
await page
|
||||
.getByTestId('google-auth-client-id')
|
||||
.fill('rotated-client-id.apps.googleusercontent.com');
|
||||
await page.getByTestId('auth-domain-save').click();
|
||||
await expect(page.getByText('Domain updated successfully')).toBeVisible();
|
||||
await expect(page.getByTestId('auth-domain-form')).toBeHidden();
|
||||
|
||||
await openConfigureAuthDomain(page, domain);
|
||||
await expect(page.getByTestId('google-auth-client-id')).toHaveValue(
|
||||
'rotated-client-id.apps.googleusercontent.com',
|
||||
);
|
||||
});
|
||||
|
||||
test('TC-04 removing a group mapping persists after save', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const domain = 'sso-rolemap.example.com';
|
||||
cleanupNames.push(domain);
|
||||
await deleteAuthDomainByNameViaApi(page, domain);
|
||||
await createGoogleAuthDomainViaApi(page, {
|
||||
name: domain,
|
||||
roleMapping: {
|
||||
defaultRole: 'signoz-viewer',
|
||||
groupMappings: {
|
||||
engineers: 'signoz-editor',
|
||||
support: 'signoz-viewer',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await gotoAuthDomains(page);
|
||||
await openConfigureAuthDomain(page, domain);
|
||||
await page.getByTestId('role-mapping-header').click();
|
||||
|
||||
const rows = page.getByTestId('role-mapping-row');
|
||||
await expect(rows).toHaveCount(2);
|
||||
// Go marshals map keys sorted, so "engineers" is always the first row.
|
||||
await expect(rows.nth(0).getByTestId('role-mapping-group-name')).toHaveValue(
|
||||
'engineers',
|
||||
);
|
||||
await rows.nth(0).getByTestId('role-mapping-remove').click();
|
||||
await expect(rows).toHaveCount(1);
|
||||
|
||||
// The PUT body is the #2402 contract: the removed mapping must be gone
|
||||
// from the payload, not just from the form state.
|
||||
const putRequest = page.waitForRequest(
|
||||
(req) =>
|
||||
req.method() === 'PUT' && req.url().includes('/api/v2/auth_domains/'),
|
||||
);
|
||||
await page.getByTestId('auth-domain-save').click();
|
||||
const body = JSON.parse((await putRequest).postData() ?? '{}');
|
||||
expect(body.roleMapping?.groupMappings).toEqual({
|
||||
support: 'signoz-viewer',
|
||||
});
|
||||
await expect(page.getByText('Domain updated successfully')).toBeVisible();
|
||||
await expect(page.getByTestId('auth-domain-form')).toBeHidden();
|
||||
|
||||
await openConfigureAuthDomain(page, domain);
|
||||
await page.getByTestId('role-mapping-header').click();
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(rows.getByTestId('role-mapping-group-name')).toHaveValue(
|
||||
'support',
|
||||
);
|
||||
});
|
||||
|
||||
test('TC-05 disabling fetch groups clears the allowed groups', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const domain = 'sso-groups.example.com';
|
||||
cleanupNames.push(domain);
|
||||
await deleteAuthDomainByNameViaApi(page, domain);
|
||||
await createGoogleAuthDomainViaApi(page, {
|
||||
name: domain,
|
||||
fetchGroups: true,
|
||||
serviceAccountJson: '{"type":"service_account","project_id":"e2e"}',
|
||||
domainToAdminEmail: { '*': 'admin@sso-groups.example.com' },
|
||||
allowedGroups: ['engineering@sso-groups.example.com'],
|
||||
});
|
||||
|
||||
await gotoAuthDomains(page);
|
||||
await openConfigureAuthDomain(page, domain);
|
||||
await page.getByTestId('google-auth-workspace-groups-header').click();
|
||||
|
||||
const fetchGroups = page
|
||||
.getByTestId('google-auth-fetch-groups')
|
||||
.getByRole('checkbox');
|
||||
await expect(fetchGroups).toBeChecked();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('google-auth-allowed-groups')
|
||||
.locator('.ant-select-selection-item'),
|
||||
).toHaveCount(1);
|
||||
await fetchGroups.click();
|
||||
await expect(fetchGroups).not.toBeChecked();
|
||||
|
||||
// The PUT body is the #2402 contract: with fetchGroups off, the payload
|
||||
// must drop allowedGroups instead of resending the stale list.
|
||||
const putRequest = page.waitForRequest(
|
||||
(req) =>
|
||||
req.method() === 'PUT' && req.url().includes('/api/v2/auth_domains/'),
|
||||
);
|
||||
await page.getByTestId('auth-domain-save').click();
|
||||
const spec = JSON.parse((await putRequest).postData() ?? '{}').config?.spec;
|
||||
expect(spec?.fetchGroups).toBeFalsy();
|
||||
expect(spec?.allowedGroups).toBeUndefined();
|
||||
expect(spec?.domainToAdminEmail).toEqual({});
|
||||
await expect(page.getByText('Domain updated successfully')).toBeVisible();
|
||||
await expect(page.getByTestId('auth-domain-form')).toBeHidden();
|
||||
|
||||
await openConfigureAuthDomain(page, domain);
|
||||
await page.getByTestId('google-auth-workspace-groups-header').click();
|
||||
await expect(fetchGroups).not.toBeChecked();
|
||||
// Re-enable to reveal the group fields: the allowed-groups list must be
|
||||
// empty, not repopulated from the pre-disable state.
|
||||
await fetchGroups.click();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('google-auth-allowed-groups')
|
||||
.locator('.ant-select-selection-item'),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('TC-06 enforce sso toggle persists across reload', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const domain = 'sso-toggle.example.com';
|
||||
cleanupNames.push(domain);
|
||||
await deleteAuthDomainByNameViaApi(page, domain);
|
||||
await createGoogleAuthDomainViaApi(page, { name: domain, enabled: false });
|
||||
|
||||
await gotoAuthDomains(page);
|
||||
const toggle = page
|
||||
.getByTestId(`auth-domain-row-${domain}`)
|
||||
.getByTestId('auth-domain-enforce-sso');
|
||||
await expect(toggle).not.toBeChecked();
|
||||
|
||||
const putResponse = page.waitForResponse(
|
||||
(res) =>
|
||||
res.request().method() === 'PUT' &&
|
||||
res.url().includes('/api/v2/auth_domains/'),
|
||||
);
|
||||
await toggle.click();
|
||||
expect((await putResponse).status()).toBe(204);
|
||||
|
||||
await page.reload();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId(`auth-domain-row-${domain}`)
|
||||
.getByTestId('auth-domain-enforce-sso'),
|
||||
).toBeChecked();
|
||||
});
|
||||
|
||||
test('TC-07 delete a domain via the UI', async ({ authedPage: page }) => {
|
||||
const domain = 'sso-delete.example.com';
|
||||
cleanupNames.push(domain);
|
||||
await deleteAuthDomainByNameViaApi(page, domain);
|
||||
await createGoogleAuthDomainViaApi(page, { name: domain });
|
||||
|
||||
await gotoAuthDomains(page);
|
||||
await page
|
||||
.getByTestId(`auth-domain-row-${domain}`)
|
||||
.getByTestId('auth-domain-delete')
|
||||
.click();
|
||||
await page.getByTestId('auth-domain-delete-confirm').click();
|
||||
|
||||
await expect(page.getByText('Domain deleted successfully')).toBeVisible();
|
||||
await expect(page.getByTestId(`auth-domain-row-${domain}`)).toHaveCount(0);
|
||||
});
|
||||
2
tests/fixtures/googleidp.py
vendored
2
tests/fixtures/googleidp.py
vendored
@@ -74,7 +74,7 @@ def perform_google_login(
|
||||
|
||||
def get_google_domain(signoz: types.SigNoz, admin_token: str) -> dict:
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
4
tests/fixtures/idp.py
vendored
4
tests/fixtures/idp.py
vendored
@@ -624,7 +624,7 @@ def _ensure_groups_client_scope(client: KeycloakAdmin) -> None:
|
||||
|
||||
def get_oidc_domain(signoz: types.SigNoz, admin_token: str) -> dict:
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
@@ -665,7 +665,7 @@ def perform_oidc_login(
|
||||
|
||||
def get_saml_domain(signoz: types.SigNoz, admin_token: str) -> dict:
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
@@ -50,13 +50,13 @@ def test_create_auth_domain(
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/signoz/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/signoz/api/v1/domains"),
|
||||
json={
|
||||
"name": "oidc.basepath.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "oidc",
|
||||
"spec": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "oidc",
|
||||
"oidcConfig": {
|
||||
"clientId": settings["client_id"],
|
||||
"clientSecret": settings["client_secret"],
|
||||
# Change the hostname of the issuer to the internal resolvable hostname of the idp
|
||||
|
||||
@@ -48,16 +48,16 @@ def test_create_auth_domain(
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/signoz/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/signoz/api/v1/domains"),
|
||||
json={
|
||||
"name": "saml.basepath.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": settings["entityID"],
|
||||
"location": settings["singleSignOnServiceLocation"],
|
||||
"certificate": settings["certificate"],
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": settings["entityID"],
|
||||
"samlIdp": settings["singleSignOnServiceLocation"],
|
||||
"samlCert": settings["certificate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from collections.abc import Callable
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
@@ -15,35 +14,30 @@ def test_create_and_get_domain(
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Reruns against a reused stack find domains from previous runs; drop them
|
||||
# all so the suite starts from a clean slate.
|
||||
# Get domains which should be an empty list
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
for domain in response.json()["data"]:
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
data = response.json()["data"]
|
||||
assert len(data) == 0
|
||||
|
||||
# Create a domain with google auth config
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
json={
|
||||
"name": "domain-google.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "google",
|
||||
"spec": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "google_auth",
|
||||
"googleAuthConfig": {
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
"redirectURI": "redirect-uri",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -55,16 +49,16 @@ def test_create_and_get_domain(
|
||||
|
||||
# Create a domain with saml config
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
json={
|
||||
"name": "domain-saml.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": "saml-entity",
|
||||
"samlIdp": "saml-idp",
|
||||
"samlCert": "saml-cert",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -76,7 +70,7 @@ def test_create_and_get_domain(
|
||||
|
||||
# List the domains
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
@@ -92,7 +86,7 @@ def test_create_and_get_domain(
|
||||
"domain-google.integration.test",
|
||||
"domain-saml.integration.test",
|
||||
]
|
||||
assert domain["config"]["kind"] in ["google", "saml"]
|
||||
assert domain["config"]["ssoType"] in ["google_auth", "saml"]
|
||||
|
||||
|
||||
def test_create_invalid(
|
||||
@@ -102,15 +96,15 @@ def test_create_invalid(
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create a domain with kind saml and a spec for oidc, this should fail because the spec does not match the kind
|
||||
# Create a domain with type saml and body for oidc, this should fail because oidcConfig is not allowed for saml
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
json={
|
||||
"name": "domain.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"oidcConfig": {
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
"issuer": "issuer",
|
||||
@@ -123,34 +117,18 @@ def test_create_invalid(
|
||||
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
|
||||
# Create a domain with a kind but no spec
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "domain.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
|
||||
# Create a domain with invalid name
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
json={
|
||||
"name": "$%^invalid",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": "saml-entity",
|
||||
"samlIdp": "saml-idp",
|
||||
"samlCert": "saml-cert",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -162,17 +140,17 @@ def test_create_invalid(
|
||||
|
||||
# Create a domain with no name
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
json={
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": "saml-entity",
|
||||
"samlIdp": "saml-idp",
|
||||
"samlCert": "saml-cert",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
@@ -182,7 +160,7 @@ def test_create_invalid(
|
||||
|
||||
# Create a domain with no config
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
json={
|
||||
"name": "domain.integration.test",
|
||||
},
|
||||
@@ -202,20 +180,20 @@ def test_create_invalid_role_mapping(
|
||||
|
||||
# Create domain with invalid defaultRole
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
json={
|
||||
"name": "invalid-role-test.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": "saml-entity",
|
||||
"samlIdp": "saml-idp",
|
||||
"samlCert": "saml-cert",
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "SUPERADMIN", # Invalid role
|
||||
},
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "SUPERADMIN", # Invalid role
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
@@ -226,22 +204,22 @@ def test_create_invalid_role_mapping(
|
||||
|
||||
# Create domain with invalid role in groupMappings
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
json={
|
||||
"name": "invalid-group-role.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": "saml-entity",
|
||||
"samlIdp": "saml-idp",
|
||||
"samlCert": "saml-cert",
|
||||
},
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"admins": "SUPERUSER", # Invalid role
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"admins": "SUPERUSER", # Invalid role
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -253,401 +231,28 @@ def test_create_invalid_role_mapping(
|
||||
|
||||
# Valid role mapping should succeed
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
json={
|
||||
"name": "valid-role-mapping.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": "saml-entity",
|
||||
"samlIdp": "saml-idp",
|
||||
"samlCert": "saml-cert",
|
||||
},
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.CREATED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "role_mapping", "expected_config", "expected_role_mapping"),
|
||||
[
|
||||
pytest.param(
|
||||
{
|
||||
"kind": "google",
|
||||
"spec": {"clientId": "client-id", "clientSecret": "client-secret"},
|
||||
},
|
||||
None,
|
||||
{
|
||||
"kind": "google",
|
||||
"spec": {
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
"fetchGroups": False,
|
||||
"insecureSkipEmailVerified": False,
|
||||
},
|
||||
},
|
||||
None,
|
||||
id="google_minimal",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"kind": "google",
|
||||
"spec": {
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
"fetchGroups": True,
|
||||
"serviceAccountJson": '{"type": "service_account"}',
|
||||
"domainToAdminEmail": {
|
||||
"roundtrip.integration.test": "admin@roundtrip.integration.test",
|
||||
"*": "fallback@roundtrip.integration.test",
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
},
|
||||
"fetchTransitiveGroupMembership": True,
|
||||
"allowedGroups": ["group-one", "group-two"],
|
||||
"insecureSkipEmailVerified": True,
|
||||
},
|
||||
},
|
||||
None,
|
||||
{
|
||||
"kind": "google",
|
||||
"spec": {
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
"fetchGroups": True,
|
||||
"serviceAccountJson": '{"type": "service_account"}',
|
||||
"domainToAdminEmail": {
|
||||
"roundtrip.integration.test": "admin@roundtrip.integration.test",
|
||||
"*": "fallback@roundtrip.integration.test",
|
||||
},
|
||||
"fetchTransitiveGroupMembership": True,
|
||||
"allowedGroups": ["group-one", "group-two"],
|
||||
"insecureSkipEmailVerified": True,
|
||||
},
|
||||
},
|
||||
None,
|
||||
id="google_full",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "https://idp.integration.test/sso",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
},
|
||||
None,
|
||||
{
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "https://idp.integration.test/sso",
|
||||
"certificate": "saml-cert",
|
||||
"insecureSkipAuthNRequestsSigned": False,
|
||||
"attributeMapping": {"email": "email", "name": "name", "groups": "groups", "role": "role"},
|
||||
},
|
||||
},
|
||||
None,
|
||||
id="saml_minimal",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "https://idp.integration.test/sso",
|
||||
"certificate": "saml-cert",
|
||||
"insecureSkipAuthNRequestsSigned": True,
|
||||
"attributeMapping": {"email": "mail"},
|
||||
},
|
||||
},
|
||||
None,
|
||||
{
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "https://idp.integration.test/sso",
|
||||
"certificate": "saml-cert",
|
||||
"insecureSkipAuthNRequestsSigned": True,
|
||||
"attributeMapping": {"email": "mail", "name": "name", "groups": "groups", "role": "role"},
|
||||
},
|
||||
},
|
||||
None,
|
||||
id="saml_partial_attribute_mapping",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"kind": "oidc",
|
||||
"spec": {
|
||||
"issuer": "https://issuer.integration.test",
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
},
|
||||
},
|
||||
None,
|
||||
{
|
||||
"kind": "oidc",
|
||||
"spec": {
|
||||
"issuer": "https://issuer.integration.test",
|
||||
"issuerAlias": "",
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
"claimMapping": {"email": "email", "name": "name", "groups": "groups", "role": "role"},
|
||||
"insecureSkipEmailVerified": False,
|
||||
"getUserInfo": False,
|
||||
},
|
||||
},
|
||||
None,
|
||||
id="oidc_minimal",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"kind": "oidc",
|
||||
"spec": {
|
||||
"issuer": "https://issuer.integration.test",
|
||||
"issuerAlias": "https://alias.integration.test",
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
"claimMapping": {"email": "eml", "name": "nm", "groups": "grps", "role": "rl"},
|
||||
"insecureSkipEmailVerified": True,
|
||||
"getUserInfo": True,
|
||||
},
|
||||
},
|
||||
None,
|
||||
{
|
||||
"kind": "oidc",
|
||||
"spec": {
|
||||
"issuer": "https://issuer.integration.test",
|
||||
"issuerAlias": "https://alias.integration.test",
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
"claimMapping": {"email": "eml", "name": "nm", "groups": "grps", "role": "rl"},
|
||||
"insecureSkipEmailVerified": True,
|
||||
"getUserInfo": True,
|
||||
},
|
||||
},
|
||||
None,
|
||||
id="oidc_full",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "https://idp.integration.test/sso",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
},
|
||||
{
|
||||
"defaultRole": "EDITOR",
|
||||
"groupMappings": {"platform-team": "ADMIN"},
|
||||
"useRoleAttribute": False,
|
||||
},
|
||||
{
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "https://idp.integration.test/sso",
|
||||
"certificate": "saml-cert",
|
||||
"insecureSkipAuthNRequestsSigned": False,
|
||||
"attributeMapping": {"email": "email", "name": "name", "groups": "groups", "role": "role"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"defaultRole": "signoz-editor",
|
||||
"groupMappings": {"platform-team": "signoz-admin"},
|
||||
"useRoleAttribute": False,
|
||||
},
|
||||
id="role_mapping_names_normalized",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "https://idp.integration.test/sso",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
},
|
||||
{"defaultRole": "VIEWER", "useRoleAttribute": True},
|
||||
{
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "https://idp.integration.test/sso",
|
||||
"certificate": "saml-cert",
|
||||
"insecureSkipAuthNRequestsSigned": False,
|
||||
"attributeMapping": {"email": "email", "name": "name", "groups": "groups", "role": "role"},
|
||||
},
|
||||
},
|
||||
{"defaultRole": "signoz-viewer", "groupMappings": None, "useRoleAttribute": True},
|
||||
id="role_mapping_null_group_mappings",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_domain_roundtrip( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
config: dict,
|
||||
role_mapping: dict | None,
|
||||
expected_config: dict,
|
||||
expected_role_mapping: dict | None,
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Drop a same-named leftover so reruns against a reused stack stay green.
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
for domain in response.json()["data"]:
|
||||
if domain["name"] == "roundtrip.integration.test":
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "roundtrip.integration.test",
|
||||
"enabled": True,
|
||||
"config": config,
|
||||
"roleMapping": role_mapping,
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.CREATED
|
||||
domain_id = response.json()["data"]["id"]
|
||||
|
||||
# Clients (e.g. the terraform provider) read state back with a follow-up
|
||||
# GET after every write, so posted values must round-trip exactly; the
|
||||
# server-side defaulting and role-name normalization pinned here are part
|
||||
# of that contract.
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
data = response.json()["data"]
|
||||
|
||||
assert data["name"] == "roundtrip.integration.test"
|
||||
assert data["enabled"] is True
|
||||
assert data["config"] == expected_config
|
||||
assert data["roleMapping"] == expected_role_mapping
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
def test_update_enabled(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Drop a same-named leftover so reruns against a reused stack stay green.
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
for domain in response.json()["data"]:
|
||||
if domain["name"] == "update-enabled.integration.test":
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "update-enabled.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED
|
||||
domain_id = response.json()["data"]["id"]
|
||||
|
||||
# Flipping enforcement goes through the same full update as any other
|
||||
# change; the echoed provider config must survive the round trip.
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
json={
|
||||
"enabled": False,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
data = response.json()["data"]
|
||||
|
||||
assert data["enabled"] is False
|
||||
assert data["config"] == {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
"insecureSkipAuthNRequestsSigned": False,
|
||||
"attributeMapping": {"email": "email", "name": "name", "groups": "groups", "role": "role"},
|
||||
},
|
||||
}
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
@@ -50,16 +50,16 @@ def test_create_auth_domain(
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
json={
|
||||
"name": "saml.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": settings["entityID"],
|
||||
"location": settings["singleSignOnServiceLocation"],
|
||||
"certificate": settings["certificate"],
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": settings["entityID"],
|
||||
"samlIdp": settings["singleSignOnServiceLocation"],
|
||||
"samlCert": settings["certificate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -71,7 +71,7 @@ def test_create_auth_domain(
|
||||
|
||||
# Get the domains from signoz
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
@@ -174,30 +174,30 @@ def test_saml_update_domain_with_group_mappings(
|
||||
|
||||
# update the existing saml domain to have role mappings also
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/domains/{domain['id']}"),
|
||||
json={
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": settings["entityID"],
|
||||
"location": settings["singleSignOnServiceLocation"],
|
||||
"certificate": settings["certificate"],
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": settings["entityID"],
|
||||
"samlIdp": settings["singleSignOnServiceLocation"],
|
||||
"samlCert": settings["certificate"],
|
||||
"attributeMapping": {
|
||||
"name": "givenName",
|
||||
"groups": "groups",
|
||||
"role": "signoz_role",
|
||||
},
|
||||
},
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
"signoz-viewers": "VIEWER",
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
"signoz-viewers": "VIEWER",
|
||||
},
|
||||
"useRoleAttribute": False,
|
||||
},
|
||||
"useRoleAttribute": False,
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
@@ -329,29 +329,29 @@ def test_saml_update_domain_with_use_role_claim(
|
||||
settings = get_saml_settings()
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/domains/{domain['id']}"),
|
||||
json={
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": settings["entityID"],
|
||||
"location": settings["singleSignOnServiceLocation"],
|
||||
"certificate": settings["certificate"],
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": settings["entityID"],
|
||||
"samlIdp": settings["singleSignOnServiceLocation"],
|
||||
"samlCert": settings["certificate"],
|
||||
"attributeMapping": {
|
||||
"name": "displayName",
|
||||
"groups": "groups",
|
||||
"role": "signoz_role",
|
||||
},
|
||||
},
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
},
|
||||
"useRoleAttribute": True,
|
||||
},
|
||||
"useRoleAttribute": True,
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
|
||||
@@ -48,13 +48,13 @@ def test_create_auth_domain(
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
json={
|
||||
"name": "oidc.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "oidc",
|
||||
"spec": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "oidc",
|
||||
"oidcConfig": {
|
||||
"clientId": settings["client_id"],
|
||||
"clientSecret": settings["client_secret"],
|
||||
# Change the hostname of the issuer to the internal resolvable hostname of the idp
|
||||
@@ -121,12 +121,12 @@ def test_oidc_update_domain_with_group_mappings(
|
||||
settings = get_oidc_settings(client_id)
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/domains/{domain['id']}"),
|
||||
json={
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "oidc",
|
||||
"spec": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "oidc",
|
||||
"oidcConfig": {
|
||||
"clientId": settings["client_id"],
|
||||
"clientSecret": settings["client_secret"],
|
||||
"issuer": f"{idp.container.container_configs['6060'].get(urlparse(settings['issuer']).path)}",
|
||||
@@ -139,15 +139,15 @@ def test_oidc_update_domain_with_group_mappings(
|
||||
"role": "signoz_role",
|
||||
},
|
||||
},
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
"signoz-viewers": "VIEWER",
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
"signoz-viewers": "VIEWER",
|
||||
},
|
||||
"useRoleAttribute": False,
|
||||
},
|
||||
"useRoleAttribute": False,
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
@@ -278,12 +278,12 @@ def test_oidc_update_domain_with_use_role_claim(
|
||||
settings = get_oidc_settings(client_id)
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/domains/{domain['id']}"),
|
||||
json={
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "oidc",
|
||||
"spec": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "oidc",
|
||||
"oidcConfig": {
|
||||
"clientId": settings["client_id"],
|
||||
"clientSecret": settings["client_secret"],
|
||||
"issuer": f"{idp.container.container_configs['6060'].get(urlparse(settings['issuer']).path)}",
|
||||
@@ -296,14 +296,14 @@ def test_oidc_update_domain_with_use_role_claim(
|
||||
"role": "signoz_role",
|
||||
},
|
||||
},
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
},
|
||||
"useRoleAttribute": True,
|
||||
},
|
||||
"useRoleAttribute": True,
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
|
||||
@@ -30,20 +30,20 @@ def test_create_auth_domain(
|
||||
domain = get_google_domain(signoz, admin_token)
|
||||
if domain:
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/domains/{domain['id']}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
json={
|
||||
"name": GOOGLE_DOMAIN,
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "google",
|
||||
"spec": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "google_auth",
|
||||
"googleAuthConfig": {
|
||||
"clientId": GOOGLE_CLIENT_ID,
|
||||
"clientSecret": GOOGLE_CLIENT_SECRET,
|
||||
},
|
||||
@@ -121,12 +121,12 @@ def test_google_authn_unverified_email(
|
||||
domain = get_google_domain(signoz, admin_token)
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/domains/{domain['id']}"),
|
||||
json={
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "google",
|
||||
"spec": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "google_auth",
|
||||
"googleAuthConfig": {
|
||||
"clientId": GOOGLE_CLIENT_ID,
|
||||
"clientSecret": GOOGLE_CLIENT_SECRET,
|
||||
"insecureSkipEmailVerified": True,
|
||||
@@ -156,18 +156,18 @@ def test_google_role_mapping_default_role(
|
||||
domain = get_google_domain(signoz, admin_token)
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/domains/{domain['id']}"),
|
||||
json={
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "google",
|
||||
"spec": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "google_auth",
|
||||
"googleAuthConfig": {
|
||||
"clientId": GOOGLE_CLIENT_ID,
|
||||
"clientSecret": GOOGLE_CLIENT_SECRET,
|
||||
},
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "EDITOR",
|
||||
"roleMapping": {
|
||||
"defaultRole": "EDITOR",
|
||||
},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
|
||||
Reference in New Issue
Block a user