mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-13 08:20:36 +01:00
Compare commits
1 Commits
fix/authdo
...
fix/uplot-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0940db0875 |
@@ -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:
|
||||
@@ -10661,6 +10609,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
|
||||
@@ -14463,275 +14680,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
|
||||
|
||||
@@ -349,7 +349,7 @@ func (Step) JSONSchema() (jsonschema.Schema, error) {
|
||||
|
||||
### `oneOf` with a discriminator
|
||||
|
||||
For a sum type whose variants are keyed by a property (e.g. `kind`), expose the variants via `JSONSchemaOneOf()` and add a discriminator. Without it, code generators intersect the variants (`A & B & C`) instead of producing a clean discriminated union (`A | B | C`).
|
||||
For a sum type whose variants are keyed by a property (e.g. `kind`), expose the variants via `JSONSchemaOneOf()` and add a discriminator. Without it, code generators intersect the variants (`A & B & C`) instead of producing a clean discriminated union (`A | B | C`). How to model the sum type itself is covered in [types.md](types.md#sum-types-the-kindspec-envelope) — this section is only about its schema.
|
||||
|
||||
The parent keeps its `JSONSchemaOneOf()` (the `oneOf` itself) and *additionally* tags it via `PrepareJSONSchema` with the `x-signoz-discriminator` extension; `signoz.attachDiscriminators` then promotes that marker to a real OpenAPI 3 `discriminator` (and strips the duplicate parent properties) after reflection.
|
||||
|
||||
|
||||
@@ -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;
|
||||
/**
|
||||
|
||||
@@ -10,10 +10,6 @@ jest.mock('providers/Timezone', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('providers/App/App', () => ({
|
||||
useAppContext: (): { featureFlags: [] } => ({ featureFlags: [] }),
|
||||
}));
|
||||
|
||||
const field = (name: string, type = ''): IField => ({
|
||||
name,
|
||||
type,
|
||||
|
||||
@@ -2,15 +2,13 @@ import type { ReactElement } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import {
|
||||
getBodyDisplayString,
|
||||
getSanitizedLogBody,
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { getLogFieldValue } from 'lib/logs/flatLogData';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
@@ -28,10 +26,6 @@ export function useLogsTableColumns({
|
||||
fontSize,
|
||||
}: UseLogsTableColumnsProps): TableColumnDef<ILog>[] {
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
const { featureFlags } = useAppContext();
|
||||
const isBodyJsonEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
|
||||
?.active || false;
|
||||
|
||||
return useMemo<TableColumnDef<ILog>[]>(() => {
|
||||
const stateIndicatorCol: TableColumnDef<ILog> = {
|
||||
@@ -94,8 +88,7 @@ export function useLogsTableColumns({
|
||||
const makeUserFieldCol = (f: IField): TableColumnDef<ILog> => ({
|
||||
id: buildCompositeKey(f.name, f.type),
|
||||
header: f.name,
|
||||
accessorFn: (log): unknown =>
|
||||
getLogFieldValue(log, f.name, isBodyJsonEnabled),
|
||||
accessorFn: (log): unknown => FlatLogData(log)[f.name],
|
||||
enableRemove: true,
|
||||
width: { min: 192 },
|
||||
cell: ({ value }): ReactElement => (
|
||||
@@ -122,5 +115,5 @@ export function useLogsTableColumns({
|
||||
.filter((c): c is TableColumnDef<ILog> => c !== null);
|
||||
|
||||
return [stateIndicatorCol, ...fieldCols];
|
||||
}, [fields, fontSize, formatTimezoneAdjustedTimestamp, isBodyJsonEnabled]);
|
||||
}, [fields, fontSize, formatTimezoneAdjustedTimestamp]);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface BaseConfigBuilderProps {
|
||||
panelType: PANEL_TYPES;
|
||||
minTimeScale?: number;
|
||||
maxTimeScale?: number;
|
||||
useExactTimeRange?: boolean;
|
||||
stepInterval?: number;
|
||||
isLogScale?: boolean;
|
||||
yAxisUnit?: string;
|
||||
@@ -46,6 +47,7 @@ export function buildBaseConfig({
|
||||
thresholds,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
useExactTimeRange,
|
||||
stepInterval,
|
||||
isLogScale,
|
||||
yAxisUnit,
|
||||
@@ -88,6 +90,7 @@ export function buildBaseConfig({
|
||||
time: true,
|
||||
min: minTimeScale,
|
||||
max: maxTimeScale,
|
||||
useExactTimeRange,
|
||||
logBase: isLogScale ? 10 : undefined,
|
||||
distribution: isLogScale
|
||||
? DistributionType.Logarithmic
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -25,7 +25,6 @@ jest.mock('@signozhq/ui/switch', () => ({
|
||||
import SSOEnforcementToggle from '../SSOEnforcementToggle';
|
||||
import {
|
||||
AUTH_DOMAINS_UPDATE_ENDPOINT,
|
||||
mockDomainWithRoleMapping,
|
||||
mockErrorResponse,
|
||||
mockGoogleAuthDomain,
|
||||
mockUpdateSuccessResponse,
|
||||
@@ -58,7 +57,7 @@ describe('SSOEnforcementToggle', () => {
|
||||
isDefaultChecked={false}
|
||||
record={{
|
||||
...mockGoogleAuthDomain,
|
||||
enabled: false,
|
||||
config: { ...mockGoogleAuthDomain.config, ssoEnabled: false },
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
@@ -96,42 +95,13 @@ describe('SSOEnforcementToggle', () => {
|
||||
expect(mockUpdateAPI).toHaveBeenCalledTimes(1);
|
||||
expect(mockUpdateAPI).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
enabled: false,
|
||||
config: mockGoogleAuthDomain.config,
|
||||
config: expect.objectContaining({
|
||||
ssoEnabled: false,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// The toggle sends a full replacement, so anything it fails to echo back is
|
||||
// dropped from the domain — role mappings included.
|
||||
it('echoes the existing role mapping when toggling enforcement', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockUpdateAPI = jest.fn();
|
||||
|
||||
server.use(
|
||||
rest.put(AUTH_DOMAINS_UPDATE_ENDPOINT, async (req, res, ctx) => {
|
||||
mockUpdateAPI(await req.json());
|
||||
return res(ctx.status(200), ctx.json(mockUpdateSuccessResponse));
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<SSOEnforcementToggle
|
||||
isDefaultChecked={true}
|
||||
record={mockDomainWithRoleMapping}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('switch'));
|
||||
|
||||
await waitFor(() => expect(mockUpdateAPI).toHaveBeenCalledTimes(1));
|
||||
expect(mockUpdateAPI).toHaveBeenCalledWith({
|
||||
enabled: false,
|
||||
config: mockDomainWithRoleMapping.config,
|
||||
roleMapping: mockDomainWithRoleMapping.roleMapping,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows error modal when update fails', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { getLogFieldValue } from './flatLogData';
|
||||
|
||||
const asLog = (partial: Partial<ILog>): ILog => partial as unknown as ILog;
|
||||
|
||||
describe('getLogFieldValue', () => {
|
||||
it('resolves a nested body field by dotted key when use_json_body is on', () => {
|
||||
const log = asLog({ body: { a: { b: { c: 'deep' } } } });
|
||||
expect(getLogFieldValue(log, 'a.b.c', true)).toBe('deep');
|
||||
});
|
||||
|
||||
it('ignores body when use_json_body is off', () => {
|
||||
const log = asLog({ body: { a: { b: { c: 'deep' } } } });
|
||||
expect(getLogFieldValue(log, 'a.b.c', false)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('ignores a stringified body even when use_json_body is on', () => {
|
||||
const log = asLog({ body: '{"a":{"b":1}}' });
|
||||
expect(getLogFieldValue(log, 'a.b', true)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('prefers the body value over attributes when the key exists in both (body first)', () => {
|
||||
const log = asLog({
|
||||
attributes_string: { 'a.b': 'attr' } as never,
|
||||
body: { a: { b: 'bodyval' } },
|
||||
});
|
||||
expect(getLogFieldValue(log, 'a.b', true)).toBe('bodyval');
|
||||
});
|
||||
|
||||
it('falls back to attributes when the key is not in the body', () => {
|
||||
const log = asLog({
|
||||
attributes_string: { 'x.y': 'attr' } as never,
|
||||
body: { other: 1 },
|
||||
});
|
||||
expect(getLogFieldValue(log, 'x.y', true)).toBe('attr');
|
||||
});
|
||||
|
||||
it('preserves falsy body values (0, false, empty string)', () => {
|
||||
const log = asLog({ body: { n: 0, flag: false, s: '' } });
|
||||
expect(getLogFieldValue(log, 'n', true)).toBe(0);
|
||||
expect(getLogFieldValue(log, 'flag', true)).toBe(false);
|
||||
expect(getLogFieldValue(log, 's', true)).toBe('');
|
||||
});
|
||||
|
||||
it('returns undefined when the body path is missing', () => {
|
||||
const log = asLog({ body: { x: 1 } });
|
||||
expect(getLogFieldValue(log, 'nope', true)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined when a mid path segment is not an object', () => {
|
||||
const log = asLog({ body: { a: { b: 'leaf' } } });
|
||||
expect(getLogFieldValue(log, 'a.b.c', true)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { ILog, ILogBody } from 'types/api/logs/log';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
export function FlatLogData(log: ILog): Record<string, string> {
|
||||
const flattenLogObject: Record<string, string> = {};
|
||||
@@ -15,29 +15,3 @@ export function FlatLogData(log: ILog): Record<string, string> {
|
||||
});
|
||||
return flattenLogObject;
|
||||
}
|
||||
|
||||
function getBodyFieldValue(body: ILogBody, key: string): unknown {
|
||||
return key.split('.').reduce<unknown>((acc, segment) => {
|
||||
if (acc && typeof acc === 'object' && !Array.isArray(acc)) {
|
||||
return (acc as Record<string, unknown>)[segment];
|
||||
}
|
||||
return undefined;
|
||||
}, body);
|
||||
}
|
||||
|
||||
// Resolve one field for the logs table. A JSON body is checked first (use_json_body
|
||||
// only), splitting the key on `.`; otherwise fall back to FlatLogData
|
||||
// (attributes/resources/scope/top-level).
|
||||
export function getLogFieldValue(
|
||||
log: ILog,
|
||||
fieldName: string,
|
||||
isBodyJsonEnabled: boolean,
|
||||
): unknown {
|
||||
if (isBodyJsonEnabled && log.body && typeof log.body === 'object') {
|
||||
const bodyValue = getBodyFieldValue(log.body, fieldName);
|
||||
if (bodyValue !== undefined) {
|
||||
return bodyValue;
|
||||
}
|
||||
}
|
||||
return FlatLogData(log)[fieldName];
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ export class UPlotScaleBuilder extends ConfigBuilder<
|
||||
logBase = 10,
|
||||
padMinBy = 0,
|
||||
padMaxBy = 0.05,
|
||||
useExactTimeRange = false,
|
||||
} = this.props;
|
||||
|
||||
// Special handling for time scales (X axis)
|
||||
@@ -58,14 +59,20 @@ export class UPlotScaleBuilder extends ConfigBuilder<
|
||||
|
||||
// Align max time to "endTime - 1 minute", rounded down to minute precision
|
||||
// This matches legacy getXAxisScale behavior and avoids empty space at the right edge
|
||||
const oneMinuteAgoTimestamp = (maxTime - 60) * 1000;
|
||||
const currentDate = new Date(oneMinuteAgoTimestamp);
|
||||
if (!useExactTimeRange) {
|
||||
const oneMinuteAgoTimestamp = (maxTime - 60) * 1000;
|
||||
const currentDate = new Date(oneMinuteAgoTimestamp);
|
||||
|
||||
currentDate.setSeconds(0);
|
||||
currentDate.setMilliseconds(0);
|
||||
currentDate.setSeconds(0);
|
||||
currentDate.setMilliseconds(0);
|
||||
|
||||
const unixTimestampSeconds = Math.floor(currentDate.getTime() / 1000);
|
||||
maxTime = unixTimestampSeconds;
|
||||
const unixTimestampSeconds = Math.floor(currentDate.getTime() / 1000);
|
||||
|
||||
// Trimming past min inverts the range, which uPlot draws as an empty plot.
|
||||
if (unixTimestampSeconds > minTime) {
|
||||
maxTime = unixTimestampSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
[scaleKey]: {
|
||||
|
||||
@@ -79,6 +79,44 @@ describe('UPlotScaleBuilder', () => {
|
||||
expect(resolvedMax).toBe(expectedMax);
|
||||
});
|
||||
|
||||
it('plots min/max as given when useExactTimeRange is set', () => {
|
||||
const min = 1_700_000_000;
|
||||
const max = 1_700_000_630;
|
||||
|
||||
const builder = new UPlotScaleBuilder(
|
||||
createScaleProps({
|
||||
scaleKey: 'x',
|
||||
time: true,
|
||||
min,
|
||||
max,
|
||||
useExactTimeRange: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.x.range).toStrictEqual([min, max]);
|
||||
});
|
||||
|
||||
it('keeps the requested end when the window is shorter than the trim', () => {
|
||||
// 23 second window: trimming a minute off the end would put max before min.
|
||||
const min = 1_786_527_160;
|
||||
const max = 1_786_527_183;
|
||||
|
||||
const builder = new UPlotScaleBuilder(
|
||||
createScaleProps({
|
||||
scaleKey: 'x',
|
||||
time: true,
|
||||
min,
|
||||
max,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.x.range).toStrictEqual([min, max]);
|
||||
});
|
||||
|
||||
it('falls back to getFallbackMinMaxTimeStamp when time scale has no min/max', () => {
|
||||
getFallbackMinMaxSpy.mockReturnValue({
|
||||
fallbackMin: 100,
|
||||
|
||||
@@ -97,6 +97,8 @@ export interface ScaleProps {
|
||||
auto?: boolean;
|
||||
logBase?: uPlot.Scale.LogBase;
|
||||
distribution?: DistributionType;
|
||||
/** Plots a time scale's `min`/`max` as given, skipping the trim below. */
|
||||
useExactTimeRange?: boolean;
|
||||
}
|
||||
|
||||
export enum DisconnectedValuesMode {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,13 +24,12 @@ HASTOKEN=23
|
||||
HAS=24
|
||||
HASANY=25
|
||||
HASALL=26
|
||||
SEARCH=27
|
||||
BOOL=28
|
||||
NUMBER=29
|
||||
QUOTED_TEXT=30
|
||||
KEY=31
|
||||
WS=32
|
||||
FREETEXT=33
|
||||
BOOL=27
|
||||
NUMBER=28
|
||||
QUOTED_TEXT=29
|
||||
KEY=30
|
||||
WS=31
|
||||
FREETEXT=32
|
||||
'('=1
|
||||
')'=2
|
||||
'['=3
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,13 +24,12 @@ HASTOKEN=23
|
||||
HAS=24
|
||||
HASANY=25
|
||||
HASALL=26
|
||||
SEARCH=27
|
||||
BOOL=28
|
||||
NUMBER=29
|
||||
QUOTED_TEXT=30
|
||||
KEY=31
|
||||
WS=32
|
||||
FREETEXT=33
|
||||
BOOL=27
|
||||
NUMBER=28
|
||||
QUOTED_TEXT=29
|
||||
KEY=30
|
||||
WS=31
|
||||
FREETEXT=32
|
||||
'('=1
|
||||
')'=2
|
||||
'['=3
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Generated from FilterQuery.g4 by ANTLR 4.13.2
|
||||
// Generated from FilterQuery.g4 by ANTLR 4.13.1
|
||||
// noinspection ES6UnusedImports,JSUnusedGlobalSymbols,JSUnusedLocalSymbols
|
||||
import {
|
||||
ATN,
|
||||
@@ -38,13 +38,12 @@ export default class FilterQueryLexer extends Lexer {
|
||||
public static readonly HAS = 24;
|
||||
public static readonly HASANY = 25;
|
||||
public static readonly HASALL = 26;
|
||||
public static readonly SEARCH = 27;
|
||||
public static readonly BOOL = 28;
|
||||
public static readonly NUMBER = 29;
|
||||
public static readonly QUOTED_TEXT = 30;
|
||||
public static readonly KEY = 31;
|
||||
public static readonly WS = 32;
|
||||
public static readonly FREETEXT = 33;
|
||||
public static readonly BOOL = 27;
|
||||
public static readonly NUMBER = 28;
|
||||
public static readonly QUOTED_TEXT = 29;
|
||||
public static readonly KEY = 30;
|
||||
public static readonly WS = 31;
|
||||
public static readonly FREETEXT = 32;
|
||||
public static readonly EOF = Token.EOF;
|
||||
|
||||
public static readonly channelNames: string[] = [ "DEFAULT_TOKEN_CHANNEL", "HIDDEN" ];
|
||||
@@ -69,9 +68,8 @@ export default class FilterQueryLexer extends Lexer {
|
||||
"AND", "OR",
|
||||
"HASTOKEN",
|
||||
"HAS", "HASANY",
|
||||
"HASALL", "SEARCH",
|
||||
"BOOL", "NUMBER",
|
||||
"QUOTED_TEXT",
|
||||
"HASALL", "BOOL",
|
||||
"NUMBER", "QUOTED_TEXT",
|
||||
"KEY", "WS",
|
||||
"FREETEXT" ];
|
||||
public static readonly modeNames: string[] = [ "DEFAULT_MODE", ];
|
||||
@@ -80,8 +78,8 @@ export default class FilterQueryLexer extends Lexer {
|
||||
"LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
|
||||
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS", "REGEXP",
|
||||
"CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY", "HASALL",
|
||||
"SEARCH", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS",
|
||||
"OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
|
||||
"BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS", "OLD_JSON_BRACKS",
|
||||
"KEY", "WS", "DIGIT", "FREETEXT",
|
||||
];
|
||||
|
||||
|
||||
@@ -102,122 +100,119 @@ export default class FilterQueryLexer extends Lexer {
|
||||
|
||||
public get modeNames(): string[] { return FilterQueryLexer.modeNames; }
|
||||
|
||||
public static readonly _serializedATN: number[] = [4,0,33,329,6,-1,2,0,
|
||||
public static readonly _serializedATN: number[] = [4,0,32,320,6,-1,2,0,
|
||||
7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,
|
||||
7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14,2,15,7,15,2,16,7,
|
||||
16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,
|
||||
2,24,7,24,2,25,7,25,2,26,7,26,2,27,7,27,2,28,7,28,2,29,7,29,2,30,7,30,2,
|
||||
31,7,31,2,32,7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,2,37,7,37,1,0,
|
||||
1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,5,3,5,91,8,5,1,6,1,6,1,6,
|
||||
1,7,1,7,1,7,1,8,1,8,1,9,1,9,1,9,1,10,1,10,1,11,1,11,1,11,1,12,1,12,1,12,
|
||||
1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,14,1,
|
||||
14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,3,15,134,8,15,1,16,1,16,1,16,1,16,
|
||||
1,16,1,16,1,16,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,151,8,17,1,
|
||||
18,1,18,1,18,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,21,1,21,1,21,1,22,
|
||||
1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,23,1,24,1,24,1,
|
||||
24,1,24,1,24,1,24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,26,1,26,1,26,
|
||||
1,26,1,26,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3,27,210,
|
||||
8,27,1,28,1,28,1,29,3,29,215,8,29,1,29,4,29,218,8,29,11,29,12,29,219,1,
|
||||
29,1,29,5,29,224,8,29,10,29,12,29,227,9,29,3,29,229,8,29,1,29,1,29,3,29,
|
||||
233,8,29,1,29,4,29,236,8,29,11,29,12,29,237,3,29,240,8,29,1,29,3,29,243,
|
||||
8,29,1,29,1,29,4,29,247,8,29,11,29,12,29,248,1,29,1,29,3,29,253,8,29,1,
|
||||
29,4,29,256,8,29,11,29,12,29,257,3,29,260,8,29,3,29,262,8,29,1,30,1,30,
|
||||
1,30,1,30,5,30,268,8,30,10,30,12,30,271,9,30,1,30,1,30,1,30,1,30,1,30,5,
|
||||
30,278,8,30,10,30,12,30,281,9,30,1,30,3,30,284,8,30,1,31,1,31,5,31,288,
|
||||
8,31,10,31,12,31,291,9,31,1,32,1,32,1,32,1,33,1,33,1,33,1,33,1,34,1,34,
|
||||
1,34,1,34,1,34,1,34,1,34,4,34,307,8,34,11,34,12,34,308,5,34,311,8,34,10,
|
||||
34,12,34,314,9,34,1,35,4,35,317,8,35,11,35,12,35,318,1,35,1,35,1,36,1,36,
|
||||
1,37,4,37,326,8,37,11,37,12,37,327,0,0,38,1,1,3,2,5,3,7,4,9,5,11,6,13,7,
|
||||
15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15,31,16,33,17,35,18,37,19,39,
|
||||
20,41,21,43,22,45,23,47,24,49,25,51,26,53,27,55,28,57,0,59,29,61,30,63,
|
||||
0,65,0,67,0,69,31,71,32,73,0,75,33,1,0,29,2,0,76,76,108,108,2,0,73,73,105,
|
||||
105,2,0,75,75,107,107,2,0,69,69,101,101,2,0,66,66,98,98,2,0,84,84,116,116,
|
||||
2,0,87,87,119,119,2,0,78,78,110,110,2,0,88,88,120,120,2,0,83,83,115,115,
|
||||
2,0,82,82,114,114,2,0,71,71,103,103,2,0,80,80,112,112,2,0,67,67,99,99,2,
|
||||
0,79,79,111,111,2,0,65,65,97,97,2,0,68,68,100,100,2,0,72,72,104,104,2,0,
|
||||
89,89,121,121,2,0,85,85,117,117,2,0,70,70,102,102,2,0,43,43,45,45,2,0,34,
|
||||
34,92,92,2,0,39,39,92,92,4,0,35,36,64,90,95,95,97,123,7,0,35,36,45,45,47,
|
||||
58,64,90,95,95,97,123,125,125,3,0,9,10,13,13,32,32,1,0,48,57,8,0,9,10,13,
|
||||
13,32,34,39,41,44,44,60,62,91,91,93,93,353,0,1,1,0,0,0,0,3,1,0,0,0,0,5,
|
||||
1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,
|
||||
0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,
|
||||
0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,
|
||||
0,39,1,0,0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,1,0,0,0,0,49,1,
|
||||
0,0,0,0,51,1,0,0,0,0,53,1,0,0,0,0,55,1,0,0,0,0,59,1,0,0,0,0,61,1,0,0,0,
|
||||
0,69,1,0,0,0,0,71,1,0,0,0,0,75,1,0,0,0,1,77,1,0,0,0,3,79,1,0,0,0,5,81,1,
|
||||
0,0,0,7,83,1,0,0,0,9,85,1,0,0,0,11,90,1,0,0,0,13,92,1,0,0,0,15,95,1,0,0,
|
||||
0,17,98,1,0,0,0,19,100,1,0,0,0,21,103,1,0,0,0,23,105,1,0,0,0,25,108,1,0,
|
||||
0,0,27,113,1,0,0,0,29,119,1,0,0,0,31,127,1,0,0,0,33,135,1,0,0,0,35,142,
|
||||
1,0,0,0,37,152,1,0,0,0,39,155,1,0,0,0,41,159,1,0,0,0,43,163,1,0,0,0,45,
|
||||
166,1,0,0,0,47,175,1,0,0,0,49,179,1,0,0,0,51,186,1,0,0,0,53,193,1,0,0,0,
|
||||
55,209,1,0,0,0,57,211,1,0,0,0,59,261,1,0,0,0,61,283,1,0,0,0,63,285,1,0,
|
||||
0,0,65,292,1,0,0,0,67,295,1,0,0,0,69,299,1,0,0,0,71,316,1,0,0,0,73,322,
|
||||
1,0,0,0,75,325,1,0,0,0,77,78,5,40,0,0,78,2,1,0,0,0,79,80,5,41,0,0,80,4,
|
||||
1,0,0,0,81,82,5,91,0,0,82,6,1,0,0,0,83,84,5,93,0,0,84,8,1,0,0,0,85,86,5,
|
||||
44,0,0,86,10,1,0,0,0,87,91,5,61,0,0,88,89,5,61,0,0,89,91,5,61,0,0,90,87,
|
||||
1,0,0,0,90,88,1,0,0,0,91,12,1,0,0,0,92,93,5,33,0,0,93,94,5,61,0,0,94,14,
|
||||
1,0,0,0,95,96,5,60,0,0,96,97,5,62,0,0,97,16,1,0,0,0,98,99,5,60,0,0,99,18,
|
||||
1,0,0,0,100,101,5,60,0,0,101,102,5,61,0,0,102,20,1,0,0,0,103,104,5,62,0,
|
||||
0,104,22,1,0,0,0,105,106,5,62,0,0,106,107,5,61,0,0,107,24,1,0,0,0,108,109,
|
||||
7,0,0,0,109,110,7,1,0,0,110,111,7,2,0,0,111,112,7,3,0,0,112,26,1,0,0,0,
|
||||
113,114,7,1,0,0,114,115,7,0,0,0,115,116,7,1,0,0,116,117,7,2,0,0,117,118,
|
||||
7,3,0,0,118,28,1,0,0,0,119,120,7,4,0,0,120,121,7,3,0,0,121,122,7,5,0,0,
|
||||
122,123,7,6,0,0,123,124,7,3,0,0,124,125,7,3,0,0,125,126,7,7,0,0,126,30,
|
||||
1,0,0,0,127,128,7,3,0,0,128,129,7,8,0,0,129,130,7,1,0,0,130,131,7,9,0,0,
|
||||
131,133,7,5,0,0,132,134,7,9,0,0,133,132,1,0,0,0,133,134,1,0,0,0,134,32,
|
||||
1,0,0,0,135,136,7,10,0,0,136,137,7,3,0,0,137,138,7,11,0,0,138,139,7,3,0,
|
||||
0,139,140,7,8,0,0,140,141,7,12,0,0,141,34,1,0,0,0,142,143,7,13,0,0,143,
|
||||
144,7,14,0,0,144,145,7,7,0,0,145,146,7,5,0,0,146,147,7,15,0,0,147,148,7,
|
||||
1,0,0,148,150,7,7,0,0,149,151,7,9,0,0,150,149,1,0,0,0,150,151,1,0,0,0,151,
|
||||
36,1,0,0,0,152,153,7,1,0,0,153,154,7,7,0,0,154,38,1,0,0,0,155,156,7,7,0,
|
||||
0,156,157,7,14,0,0,157,158,7,5,0,0,158,40,1,0,0,0,159,160,7,15,0,0,160,
|
||||
161,7,7,0,0,161,162,7,16,0,0,162,42,1,0,0,0,163,164,7,14,0,0,164,165,7,
|
||||
10,0,0,165,44,1,0,0,0,166,167,7,17,0,0,167,168,7,15,0,0,168,169,7,9,0,0,
|
||||
169,170,7,5,0,0,170,171,7,14,0,0,171,172,7,2,0,0,172,173,7,3,0,0,173,174,
|
||||
7,7,0,0,174,46,1,0,0,0,175,176,7,17,0,0,176,177,7,15,0,0,177,178,7,9,0,
|
||||
0,178,48,1,0,0,0,179,180,7,17,0,0,180,181,7,15,0,0,181,182,7,9,0,0,182,
|
||||
183,7,15,0,0,183,184,7,7,0,0,184,185,7,18,0,0,185,50,1,0,0,0,186,187,7,
|
||||
17,0,0,187,188,7,15,0,0,188,189,7,9,0,0,189,190,7,15,0,0,190,191,7,0,0,
|
||||
0,191,192,7,0,0,0,192,52,1,0,0,0,193,194,7,9,0,0,194,195,7,3,0,0,195,196,
|
||||
7,15,0,0,196,197,7,10,0,0,197,198,7,13,0,0,198,199,7,17,0,0,199,54,1,0,
|
||||
0,0,200,201,7,5,0,0,201,202,7,10,0,0,202,203,7,19,0,0,203,210,7,3,0,0,204,
|
||||
205,7,20,0,0,205,206,7,15,0,0,206,207,7,0,0,0,207,208,7,9,0,0,208,210,7,
|
||||
3,0,0,209,200,1,0,0,0,209,204,1,0,0,0,210,56,1,0,0,0,211,212,7,21,0,0,212,
|
||||
58,1,0,0,0,213,215,3,57,28,0,214,213,1,0,0,0,214,215,1,0,0,0,215,217,1,
|
||||
0,0,0,216,218,3,73,36,0,217,216,1,0,0,0,218,219,1,0,0,0,219,217,1,0,0,0,
|
||||
219,220,1,0,0,0,220,228,1,0,0,0,221,225,5,46,0,0,222,224,3,73,36,0,223,
|
||||
222,1,0,0,0,224,227,1,0,0,0,225,223,1,0,0,0,225,226,1,0,0,0,226,229,1,0,
|
||||
0,0,227,225,1,0,0,0,228,221,1,0,0,0,228,229,1,0,0,0,229,239,1,0,0,0,230,
|
||||
232,7,3,0,0,231,233,3,57,28,0,232,231,1,0,0,0,232,233,1,0,0,0,233,235,1,
|
||||
0,0,0,234,236,3,73,36,0,235,234,1,0,0,0,236,237,1,0,0,0,237,235,1,0,0,0,
|
||||
237,238,1,0,0,0,238,240,1,0,0,0,239,230,1,0,0,0,239,240,1,0,0,0,240,262,
|
||||
1,0,0,0,241,243,3,57,28,0,242,241,1,0,0,0,242,243,1,0,0,0,243,244,1,0,0,
|
||||
0,244,246,5,46,0,0,245,247,3,73,36,0,246,245,1,0,0,0,247,248,1,0,0,0,248,
|
||||
246,1,0,0,0,248,249,1,0,0,0,249,259,1,0,0,0,250,252,7,3,0,0,251,253,3,57,
|
||||
28,0,252,251,1,0,0,0,252,253,1,0,0,0,253,255,1,0,0,0,254,256,3,73,36,0,
|
||||
255,254,1,0,0,0,256,257,1,0,0,0,257,255,1,0,0,0,257,258,1,0,0,0,258,260,
|
||||
1,0,0,0,259,250,1,0,0,0,259,260,1,0,0,0,260,262,1,0,0,0,261,214,1,0,0,0,
|
||||
261,242,1,0,0,0,262,60,1,0,0,0,263,269,5,34,0,0,264,268,8,22,0,0,265,266,
|
||||
5,92,0,0,266,268,9,0,0,0,267,264,1,0,0,0,267,265,1,0,0,0,268,271,1,0,0,
|
||||
0,269,267,1,0,0,0,269,270,1,0,0,0,270,272,1,0,0,0,271,269,1,0,0,0,272,284,
|
||||
5,34,0,0,273,279,5,39,0,0,274,278,8,23,0,0,275,276,5,92,0,0,276,278,9,0,
|
||||
0,0,277,274,1,0,0,0,277,275,1,0,0,0,278,281,1,0,0,0,279,277,1,0,0,0,279,
|
||||
280,1,0,0,0,280,282,1,0,0,0,281,279,1,0,0,0,282,284,5,39,0,0,283,263,1,
|
||||
0,0,0,283,273,1,0,0,0,284,62,1,0,0,0,285,289,7,24,0,0,286,288,7,25,0,0,
|
||||
287,286,1,0,0,0,288,291,1,0,0,0,289,287,1,0,0,0,289,290,1,0,0,0,290,64,
|
||||
1,0,0,0,291,289,1,0,0,0,292,293,5,91,0,0,293,294,5,93,0,0,294,66,1,0,0,
|
||||
0,295,296,5,91,0,0,296,297,5,42,0,0,297,298,5,93,0,0,298,68,1,0,0,0,299,
|
||||
312,3,63,31,0,300,301,5,46,0,0,301,311,3,63,31,0,302,311,3,65,32,0,303,
|
||||
311,3,67,33,0,304,306,5,46,0,0,305,307,3,73,36,0,306,305,1,0,0,0,307,308,
|
||||
1,0,0,0,308,306,1,0,0,0,308,309,1,0,0,0,309,311,1,0,0,0,310,300,1,0,0,0,
|
||||
310,302,1,0,0,0,310,303,1,0,0,0,310,304,1,0,0,0,311,314,1,0,0,0,312,310,
|
||||
1,0,0,0,312,313,1,0,0,0,313,70,1,0,0,0,314,312,1,0,0,0,315,317,7,26,0,0,
|
||||
316,315,1,0,0,0,317,318,1,0,0,0,318,316,1,0,0,0,318,319,1,0,0,0,319,320,
|
||||
1,0,0,0,320,321,6,35,0,0,321,72,1,0,0,0,322,323,7,27,0,0,323,74,1,0,0,0,
|
||||
324,326,8,28,0,0,325,324,1,0,0,0,326,327,1,0,0,0,327,325,1,0,0,0,327,328,
|
||||
1,0,0,0,328,76,1,0,0,0,29,0,90,133,150,209,214,219,225,228,232,237,239,
|
||||
242,248,252,257,259,261,267,269,277,279,283,289,308,310,312,318,327,1,6,
|
||||
0,0];
|
||||
31,7,31,2,32,7,32,2,33,7,33,2,34,7,34,2,35,7,35,2,36,7,36,1,0,1,0,1,1,1,
|
||||
1,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,5,3,5,89,8,5,1,6,1,6,1,6,1,7,1,7,1,
|
||||
7,1,8,1,8,1,9,1,9,1,9,1,10,1,10,1,11,1,11,1,11,1,12,1,12,1,12,1,12,1,12,
|
||||
1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,14,1,14,1,14,1,
|
||||
15,1,15,1,15,1,15,1,15,1,15,3,15,132,8,15,1,16,1,16,1,16,1,16,1,16,1,16,
|
||||
1,16,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,149,8,17,1,18,1,18,1,
|
||||
18,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,21,1,21,1,21,1,22,1,22,1,22,
|
||||
1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,23,1,24,1,24,1,24,1,24,1,
|
||||
24,1,24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,26,1,26,1,26,1,26,1,26,
|
||||
1,26,1,26,1,26,1,26,3,26,201,8,26,1,27,1,27,1,28,3,28,206,8,28,1,28,4,28,
|
||||
209,8,28,11,28,12,28,210,1,28,1,28,5,28,215,8,28,10,28,12,28,218,9,28,3,
|
||||
28,220,8,28,1,28,1,28,3,28,224,8,28,1,28,4,28,227,8,28,11,28,12,28,228,
|
||||
3,28,231,8,28,1,28,3,28,234,8,28,1,28,1,28,4,28,238,8,28,11,28,12,28,239,
|
||||
1,28,1,28,3,28,244,8,28,1,28,4,28,247,8,28,11,28,12,28,248,3,28,251,8,28,
|
||||
3,28,253,8,28,1,29,1,29,1,29,1,29,5,29,259,8,29,10,29,12,29,262,9,29,1,
|
||||
29,1,29,1,29,1,29,1,29,5,29,269,8,29,10,29,12,29,272,9,29,1,29,3,29,275,
|
||||
8,29,1,30,1,30,5,30,279,8,30,10,30,12,30,282,9,30,1,31,1,31,1,31,1,32,1,
|
||||
32,1,32,1,32,1,33,1,33,1,33,1,33,1,33,1,33,1,33,4,33,298,8,33,11,33,12,
|
||||
33,299,5,33,302,8,33,10,33,12,33,305,9,33,1,34,4,34,308,8,34,11,34,12,34,
|
||||
309,1,34,1,34,1,35,1,35,1,36,4,36,317,8,36,11,36,12,36,318,0,0,37,1,1,3,
|
||||
2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15,31,
|
||||
16,33,17,35,18,37,19,39,20,41,21,43,22,45,23,47,24,49,25,51,26,53,27,55,
|
||||
0,57,28,59,29,61,0,63,0,65,0,67,30,69,31,71,0,73,32,1,0,29,2,0,76,76,108,
|
||||
108,2,0,73,73,105,105,2,0,75,75,107,107,2,0,69,69,101,101,2,0,66,66,98,
|
||||
98,2,0,84,84,116,116,2,0,87,87,119,119,2,0,78,78,110,110,2,0,88,88,120,
|
||||
120,2,0,83,83,115,115,2,0,82,82,114,114,2,0,71,71,103,103,2,0,80,80,112,
|
||||
112,2,0,67,67,99,99,2,0,79,79,111,111,2,0,65,65,97,97,2,0,68,68,100,100,
|
||||
2,0,72,72,104,104,2,0,89,89,121,121,2,0,85,85,117,117,2,0,70,70,102,102,
|
||||
2,0,43,43,45,45,2,0,34,34,92,92,2,0,39,39,92,92,4,0,35,36,64,90,95,95,97,
|
||||
123,7,0,35,36,45,45,47,58,64,90,95,95,97,123,125,125,3,0,9,10,13,13,32,
|
||||
32,1,0,48,57,8,0,9,10,13,13,32,34,39,41,44,44,60,62,91,91,93,93,344,0,1,
|
||||
1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,
|
||||
13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,
|
||||
0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,
|
||||
35,1,0,0,0,0,37,1,0,0,0,0,39,1,0,0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,
|
||||
0,0,0,47,1,0,0,0,0,49,1,0,0,0,0,51,1,0,0,0,0,53,1,0,0,0,0,57,1,0,0,0,0,
|
||||
59,1,0,0,0,0,67,1,0,0,0,0,69,1,0,0,0,0,73,1,0,0,0,1,75,1,0,0,0,3,77,1,0,
|
||||
0,0,5,79,1,0,0,0,7,81,1,0,0,0,9,83,1,0,0,0,11,88,1,0,0,0,13,90,1,0,0,0,
|
||||
15,93,1,0,0,0,17,96,1,0,0,0,19,98,1,0,0,0,21,101,1,0,0,0,23,103,1,0,0,0,
|
||||
25,106,1,0,0,0,27,111,1,0,0,0,29,117,1,0,0,0,31,125,1,0,0,0,33,133,1,0,
|
||||
0,0,35,140,1,0,0,0,37,150,1,0,0,0,39,153,1,0,0,0,41,157,1,0,0,0,43,161,
|
||||
1,0,0,0,45,164,1,0,0,0,47,173,1,0,0,0,49,177,1,0,0,0,51,184,1,0,0,0,53,
|
||||
200,1,0,0,0,55,202,1,0,0,0,57,252,1,0,0,0,59,274,1,0,0,0,61,276,1,0,0,0,
|
||||
63,283,1,0,0,0,65,286,1,0,0,0,67,290,1,0,0,0,69,307,1,0,0,0,71,313,1,0,
|
||||
0,0,73,316,1,0,0,0,75,76,5,40,0,0,76,2,1,0,0,0,77,78,5,41,0,0,78,4,1,0,
|
||||
0,0,79,80,5,91,0,0,80,6,1,0,0,0,81,82,5,93,0,0,82,8,1,0,0,0,83,84,5,44,
|
||||
0,0,84,10,1,0,0,0,85,89,5,61,0,0,86,87,5,61,0,0,87,89,5,61,0,0,88,85,1,
|
||||
0,0,0,88,86,1,0,0,0,89,12,1,0,0,0,90,91,5,33,0,0,91,92,5,61,0,0,92,14,1,
|
||||
0,0,0,93,94,5,60,0,0,94,95,5,62,0,0,95,16,1,0,0,0,96,97,5,60,0,0,97,18,
|
||||
1,0,0,0,98,99,5,60,0,0,99,100,5,61,0,0,100,20,1,0,0,0,101,102,5,62,0,0,
|
||||
102,22,1,0,0,0,103,104,5,62,0,0,104,105,5,61,0,0,105,24,1,0,0,0,106,107,
|
||||
7,0,0,0,107,108,7,1,0,0,108,109,7,2,0,0,109,110,7,3,0,0,110,26,1,0,0,0,
|
||||
111,112,7,1,0,0,112,113,7,0,0,0,113,114,7,1,0,0,114,115,7,2,0,0,115,116,
|
||||
7,3,0,0,116,28,1,0,0,0,117,118,7,4,0,0,118,119,7,3,0,0,119,120,7,5,0,0,
|
||||
120,121,7,6,0,0,121,122,7,3,0,0,122,123,7,3,0,0,123,124,7,7,0,0,124,30,
|
||||
1,0,0,0,125,126,7,3,0,0,126,127,7,8,0,0,127,128,7,1,0,0,128,129,7,9,0,0,
|
||||
129,131,7,5,0,0,130,132,7,9,0,0,131,130,1,0,0,0,131,132,1,0,0,0,132,32,
|
||||
1,0,0,0,133,134,7,10,0,0,134,135,7,3,0,0,135,136,7,11,0,0,136,137,7,3,0,
|
||||
0,137,138,7,8,0,0,138,139,7,12,0,0,139,34,1,0,0,0,140,141,7,13,0,0,141,
|
||||
142,7,14,0,0,142,143,7,7,0,0,143,144,7,5,0,0,144,145,7,15,0,0,145,146,7,
|
||||
1,0,0,146,148,7,7,0,0,147,149,7,9,0,0,148,147,1,0,0,0,148,149,1,0,0,0,149,
|
||||
36,1,0,0,0,150,151,7,1,0,0,151,152,7,7,0,0,152,38,1,0,0,0,153,154,7,7,0,
|
||||
0,154,155,7,14,0,0,155,156,7,5,0,0,156,40,1,0,0,0,157,158,7,15,0,0,158,
|
||||
159,7,7,0,0,159,160,7,16,0,0,160,42,1,0,0,0,161,162,7,14,0,0,162,163,7,
|
||||
10,0,0,163,44,1,0,0,0,164,165,7,17,0,0,165,166,7,15,0,0,166,167,7,9,0,0,
|
||||
167,168,7,5,0,0,168,169,7,14,0,0,169,170,7,2,0,0,170,171,7,3,0,0,171,172,
|
||||
7,7,0,0,172,46,1,0,0,0,173,174,7,17,0,0,174,175,7,15,0,0,175,176,7,9,0,
|
||||
0,176,48,1,0,0,0,177,178,7,17,0,0,178,179,7,15,0,0,179,180,7,9,0,0,180,
|
||||
181,7,15,0,0,181,182,7,7,0,0,182,183,7,18,0,0,183,50,1,0,0,0,184,185,7,
|
||||
17,0,0,185,186,7,15,0,0,186,187,7,9,0,0,187,188,7,15,0,0,188,189,7,0,0,
|
||||
0,189,190,7,0,0,0,190,52,1,0,0,0,191,192,7,5,0,0,192,193,7,10,0,0,193,194,
|
||||
7,19,0,0,194,201,7,3,0,0,195,196,7,20,0,0,196,197,7,15,0,0,197,198,7,0,
|
||||
0,0,198,199,7,9,0,0,199,201,7,3,0,0,200,191,1,0,0,0,200,195,1,0,0,0,201,
|
||||
54,1,0,0,0,202,203,7,21,0,0,203,56,1,0,0,0,204,206,3,55,27,0,205,204,1,
|
||||
0,0,0,205,206,1,0,0,0,206,208,1,0,0,0,207,209,3,71,35,0,208,207,1,0,0,0,
|
||||
209,210,1,0,0,0,210,208,1,0,0,0,210,211,1,0,0,0,211,219,1,0,0,0,212,216,
|
||||
5,46,0,0,213,215,3,71,35,0,214,213,1,0,0,0,215,218,1,0,0,0,216,214,1,0,
|
||||
0,0,216,217,1,0,0,0,217,220,1,0,0,0,218,216,1,0,0,0,219,212,1,0,0,0,219,
|
||||
220,1,0,0,0,220,230,1,0,0,0,221,223,7,3,0,0,222,224,3,55,27,0,223,222,1,
|
||||
0,0,0,223,224,1,0,0,0,224,226,1,0,0,0,225,227,3,71,35,0,226,225,1,0,0,0,
|
||||
227,228,1,0,0,0,228,226,1,0,0,0,228,229,1,0,0,0,229,231,1,0,0,0,230,221,
|
||||
1,0,0,0,230,231,1,0,0,0,231,253,1,0,0,0,232,234,3,55,27,0,233,232,1,0,0,
|
||||
0,233,234,1,0,0,0,234,235,1,0,0,0,235,237,5,46,0,0,236,238,3,71,35,0,237,
|
||||
236,1,0,0,0,238,239,1,0,0,0,239,237,1,0,0,0,239,240,1,0,0,0,240,250,1,0,
|
||||
0,0,241,243,7,3,0,0,242,244,3,55,27,0,243,242,1,0,0,0,243,244,1,0,0,0,244,
|
||||
246,1,0,0,0,245,247,3,71,35,0,246,245,1,0,0,0,247,248,1,0,0,0,248,246,1,
|
||||
0,0,0,248,249,1,0,0,0,249,251,1,0,0,0,250,241,1,0,0,0,250,251,1,0,0,0,251,
|
||||
253,1,0,0,0,252,205,1,0,0,0,252,233,1,0,0,0,253,58,1,0,0,0,254,260,5,34,
|
||||
0,0,255,259,8,22,0,0,256,257,5,92,0,0,257,259,9,0,0,0,258,255,1,0,0,0,258,
|
||||
256,1,0,0,0,259,262,1,0,0,0,260,258,1,0,0,0,260,261,1,0,0,0,261,263,1,0,
|
||||
0,0,262,260,1,0,0,0,263,275,5,34,0,0,264,270,5,39,0,0,265,269,8,23,0,0,
|
||||
266,267,5,92,0,0,267,269,9,0,0,0,268,265,1,0,0,0,268,266,1,0,0,0,269,272,
|
||||
1,0,0,0,270,268,1,0,0,0,270,271,1,0,0,0,271,273,1,0,0,0,272,270,1,0,0,0,
|
||||
273,275,5,39,0,0,274,254,1,0,0,0,274,264,1,0,0,0,275,60,1,0,0,0,276,280,
|
||||
7,24,0,0,277,279,7,25,0,0,278,277,1,0,0,0,279,282,1,0,0,0,280,278,1,0,0,
|
||||
0,280,281,1,0,0,0,281,62,1,0,0,0,282,280,1,0,0,0,283,284,5,91,0,0,284,285,
|
||||
5,93,0,0,285,64,1,0,0,0,286,287,5,91,0,0,287,288,5,42,0,0,288,289,5,93,
|
||||
0,0,289,66,1,0,0,0,290,303,3,61,30,0,291,292,5,46,0,0,292,302,3,61,30,0,
|
||||
293,302,3,63,31,0,294,302,3,65,32,0,295,297,5,46,0,0,296,298,3,71,35,0,
|
||||
297,296,1,0,0,0,298,299,1,0,0,0,299,297,1,0,0,0,299,300,1,0,0,0,300,302,
|
||||
1,0,0,0,301,291,1,0,0,0,301,293,1,0,0,0,301,294,1,0,0,0,301,295,1,0,0,0,
|
||||
302,305,1,0,0,0,303,301,1,0,0,0,303,304,1,0,0,0,304,68,1,0,0,0,305,303,
|
||||
1,0,0,0,306,308,7,26,0,0,307,306,1,0,0,0,308,309,1,0,0,0,309,307,1,0,0,
|
||||
0,309,310,1,0,0,0,310,311,1,0,0,0,311,312,6,34,0,0,312,70,1,0,0,0,313,314,
|
||||
7,27,0,0,314,72,1,0,0,0,315,317,8,28,0,0,316,315,1,0,0,0,317,318,1,0,0,
|
||||
0,318,316,1,0,0,0,318,319,1,0,0,0,319,74,1,0,0,0,29,0,88,131,148,200,205,
|
||||
210,216,219,223,228,230,233,239,243,248,250,252,258,260,268,270,274,280,
|
||||
299,301,303,309,318,1,6,0,0];
|
||||
|
||||
private static __ATN: ATN;
|
||||
public static get _ATN(): ATN {
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
// Generated from FilterQuery.g4 by ANTLR 4.13.2
|
||||
// Generated from FilterQuery.g4 by ANTLR 4.13.1
|
||||
|
||||
import {ParseTreeListener} from "antlr4";
|
||||
|
||||
|
||||
import { QueryContext } from "./FilterQueryParser.js";
|
||||
import { ExpressionContext } from "./FilterQueryParser.js";
|
||||
import { OrExpressionContext } from "./FilterQueryParser.js";
|
||||
import { AndExpressionContext } from "./FilterQueryParser.js";
|
||||
import { UnaryExpressionContext } from "./FilterQueryParser.js";
|
||||
import { PrimaryContext } from "./FilterQueryParser.js";
|
||||
import { ComparisonContext } from "./FilterQueryParser.js";
|
||||
import { InClauseContext } from "./FilterQueryParser.js";
|
||||
import { NotInClauseContext } from "./FilterQueryParser.js";
|
||||
import { ValueListContext } from "./FilterQueryParser.js";
|
||||
import { FullTextContext } from "./FilterQueryParser.js";
|
||||
import { FunctionCallContext } from "./FilterQueryParser.js";
|
||||
import { SearchCallContext } from "./FilterQueryParser.js";
|
||||
import { FunctionParamListContext } from "./FilterQueryParser.js";
|
||||
import { FunctionParamContext } from "./FilterQueryParser.js";
|
||||
import { ArrayContext } from "./FilterQueryParser.js";
|
||||
import { ValueContext } from "./FilterQueryParser.js";
|
||||
import { KeyContext } from "./FilterQueryParser.js";
|
||||
import { QueryContext } from "./FilterQueryParser";
|
||||
import { ExpressionContext } from "./FilterQueryParser";
|
||||
import { OrExpressionContext } from "./FilterQueryParser";
|
||||
import { AndExpressionContext } from "./FilterQueryParser";
|
||||
import { UnaryExpressionContext } from "./FilterQueryParser";
|
||||
import { PrimaryContext } from "./FilterQueryParser";
|
||||
import { ComparisonContext } from "./FilterQueryParser";
|
||||
import { InClauseContext } from "./FilterQueryParser";
|
||||
import { NotInClauseContext } from "./FilterQueryParser";
|
||||
import { ValueListContext } from "./FilterQueryParser";
|
||||
import { FullTextContext } from "./FilterQueryParser";
|
||||
import { FunctionCallContext } from "./FilterQueryParser";
|
||||
import { FunctionParamListContext } from "./FilterQueryParser";
|
||||
import { FunctionParamContext } from "./FilterQueryParser";
|
||||
import { ArrayContext } from "./FilterQueryParser";
|
||||
import { ValueContext } from "./FilterQueryParser";
|
||||
import { KeyContext } from "./FilterQueryParser";
|
||||
|
||||
|
||||
/**
|
||||
@@ -148,16 +147,6 @@ export default class FilterQueryListener extends ParseTreeListener {
|
||||
* @param ctx the parse tree
|
||||
*/
|
||||
exitFunctionCall?: (ctx: FunctionCallContext) => void;
|
||||
/**
|
||||
* Enter a parse tree produced by `FilterQueryParser.searchCall`.
|
||||
* @param ctx the parse tree
|
||||
*/
|
||||
enterSearchCall?: (ctx: SearchCallContext) => void;
|
||||
/**
|
||||
* Exit a parse tree produced by `FilterQueryParser.searchCall`.
|
||||
* @param ctx the parse tree
|
||||
*/
|
||||
exitSearchCall?: (ctx: SearchCallContext) => void;
|
||||
/**
|
||||
* Enter a parse tree produced by `FilterQueryParser.functionParamList`.
|
||||
* @param ctx the parse tree
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,25 @@
|
||||
// Generated from FilterQuery.g4 by ANTLR 4.13.2
|
||||
// Generated from FilterQuery.g4 by ANTLR 4.13.1
|
||||
|
||||
import {ParseTreeVisitor} from 'antlr4';
|
||||
|
||||
|
||||
import { QueryContext } from "./FilterQueryParser.js";
|
||||
import { ExpressionContext } from "./FilterQueryParser.js";
|
||||
import { OrExpressionContext } from "./FilterQueryParser.js";
|
||||
import { AndExpressionContext } from "./FilterQueryParser.js";
|
||||
import { UnaryExpressionContext } from "./FilterQueryParser.js";
|
||||
import { PrimaryContext } from "./FilterQueryParser.js";
|
||||
import { ComparisonContext } from "./FilterQueryParser.js";
|
||||
import { InClauseContext } from "./FilterQueryParser.js";
|
||||
import { NotInClauseContext } from "./FilterQueryParser.js";
|
||||
import { ValueListContext } from "./FilterQueryParser.js";
|
||||
import { FullTextContext } from "./FilterQueryParser.js";
|
||||
import { FunctionCallContext } from "./FilterQueryParser.js";
|
||||
import { SearchCallContext } from "./FilterQueryParser.js";
|
||||
import { FunctionParamListContext } from "./FilterQueryParser.js";
|
||||
import { FunctionParamContext } from "./FilterQueryParser.js";
|
||||
import { ArrayContext } from "./FilterQueryParser.js";
|
||||
import { ValueContext } from "./FilterQueryParser.js";
|
||||
import { KeyContext } from "./FilterQueryParser.js";
|
||||
import { QueryContext } from "./FilterQueryParser";
|
||||
import { ExpressionContext } from "./FilterQueryParser";
|
||||
import { OrExpressionContext } from "./FilterQueryParser";
|
||||
import { AndExpressionContext } from "./FilterQueryParser";
|
||||
import { UnaryExpressionContext } from "./FilterQueryParser";
|
||||
import { PrimaryContext } from "./FilterQueryParser";
|
||||
import { ComparisonContext } from "./FilterQueryParser";
|
||||
import { InClauseContext } from "./FilterQueryParser";
|
||||
import { NotInClauseContext } from "./FilterQueryParser";
|
||||
import { ValueListContext } from "./FilterQueryParser";
|
||||
import { FullTextContext } from "./FilterQueryParser";
|
||||
import { FunctionCallContext } from "./FilterQueryParser";
|
||||
import { FunctionParamListContext } from "./FilterQueryParser";
|
||||
import { FunctionParamContext } from "./FilterQueryParser";
|
||||
import { ArrayContext } from "./FilterQueryParser";
|
||||
import { ValueContext } from "./FilterQueryParser";
|
||||
import { KeyContext } from "./FilterQueryParser";
|
||||
|
||||
|
||||
/**
|
||||
@@ -103,12 +102,6 @@ export default class FilterQueryVisitor<Result> extends ParseTreeVisitor<Result>
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitFunctionCall?: (ctx: FunctionCallContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `FilterQueryParser.searchCall`.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
visitSearchCall?: (ctx: SearchCallContext) => Result;
|
||||
/**
|
||||
* Visit a parse tree produced by `FilterQueryParser.functionParamList`.
|
||||
* @param ctx the parse tree
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -4135,10 +4135,6 @@ func (r *ClickHouseReader) GetTimeSeriesResultV3(ctx context.Context, query stri
|
||||
return readRowsForTimeSeriesResult(rows, vars, columnNames, countOfNumberCols)
|
||||
}
|
||||
|
||||
func isJSONColumn(columnType driver.ColumnType) bool {
|
||||
return strings.HasPrefix(strings.ToUpper(columnType.DatabaseTypeName()), "JSON")
|
||||
}
|
||||
|
||||
// GetListResultV3 runs the query and returns list of rows
|
||||
func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([]*v3.Row, error) {
|
||||
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
||||
@@ -4163,12 +4159,6 @@ func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([
|
||||
for rows.Next() {
|
||||
var vars = make([]interface{}, len(columnTypes))
|
||||
for i := range columnTypes {
|
||||
if isJSONColumn(columnTypes[i]) {
|
||||
// the driver fails to decode JSON into native Go values, so it is read as raw bytes
|
||||
var raw []byte
|
||||
vars[i] = &raw
|
||||
continue
|
||||
}
|
||||
vars[i] = reflect.New(columnTypes[i].ScanType()).Interface()
|
||||
}
|
||||
if err := rows.Scan(vars...); err != nil {
|
||||
@@ -4177,17 +4167,7 @@ func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([
|
||||
row := map[string]interface{}{}
|
||||
var t time.Time
|
||||
for idx, v := range vars {
|
||||
if isJSONColumn(columnTypes[idx]) {
|
||||
raw, ok := v.(*[]byte)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var value map[string]interface{}
|
||||
if err := json.Unmarshal(*raw, &value); err != nil {
|
||||
return nil, errors.New(err.Error())
|
||||
}
|
||||
row[columnNames[idx]] = value
|
||||
} else if columnNames[idx] == "timestamp" {
|
||||
if columnNames[idx] == "timestamp" {
|
||||
switch v := v.(type) {
|
||||
case *uint64:
|
||||
t = time.Unix(0, int64(*v))
|
||||
|
||||
@@ -3803,10 +3803,6 @@ func (aH *APIHandler) QueryRangeV3(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
queryRangeParams.UseJSONBody = aH.Signoz.Flagger.BooleanOrEmpty(
|
||||
r.Context(), flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgID),
|
||||
)
|
||||
|
||||
// add temporality for each metric
|
||||
temporalityErr := aH.PopulateTemporality(r.Context(), orgID, queryRangeParams)
|
||||
if temporalityErr != nil {
|
||||
|
||||
@@ -361,7 +361,7 @@ func generateAggregateClause(panelType v3.PanelType, start, end int64, aggOp v3.
|
||||
}
|
||||
}
|
||||
|
||||
func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.BuilderQuery, graphLimitQtype string, useJSONBody bool) (string, error) {
|
||||
func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.BuilderQuery, graphLimitQtype string) (string, error) {
|
||||
// timerange will be sent in epoch millisecond
|
||||
logsStart := utils.GetEpochNanoSecs(start)
|
||||
logsEnd := utils.GetEpochNanoSecs(end)
|
||||
@@ -405,9 +405,6 @@ func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.Build
|
||||
if mq.AggregateOperator == v3.AggregateOperatorNoOp {
|
||||
// with noop any filter or different order by other than ts will use new table
|
||||
sqlSelect := constants.LogsSQLSelectV2
|
||||
if useJSONBody {
|
||||
sqlSelect = constants.LogsSQLSelectV2WithBodyJSON
|
||||
}
|
||||
queryTmpl := sqlSelect + "from signoz_logs.%s where %s%s order by %s"
|
||||
query := fmt.Sprintf(queryTmpl, DISTRIBUTED_LOGS_V2, timeFilter, filterSubQuery, orderBy)
|
||||
return query, nil
|
||||
@@ -520,7 +517,7 @@ func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.Pan
|
||||
return query, nil
|
||||
} else if options.GraphLimitQtype == constants.FirstQueryGraphLimit {
|
||||
// give me just the group_by names (no values)
|
||||
query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, options.GraphLimitQtype, options.UseJSONBody)
|
||||
query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, options.GraphLimitQtype)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -528,14 +525,14 @@ func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.Pan
|
||||
|
||||
return query, nil
|
||||
} else if options.GraphLimitQtype == constants.SecondQueryGraphLimit {
|
||||
query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, options.GraphLimitQtype, options.UseJSONBody)
|
||||
query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, options.GraphLimitQtype)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return query, nil
|
||||
}
|
||||
|
||||
query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, options.GraphLimitQtype, options.UseJSONBody)
|
||||
query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, options.GraphLimitQtype)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -899,7 +899,7 @@ func Test_buildLogsQuery(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := buildLogsQuery(tt.args.panelType, tt.args.start, tt.args.end, tt.args.step, tt.args.mq, tt.args.graphLimitQtype, false)
|
||||
got, err := buildLogsQuery(tt.args.panelType, tt.args.start, tt.args.end, tt.args.step, tt.args.mq, tt.args.graphLimitQtype)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("buildLogsQuery() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
||||
@@ -215,18 +215,18 @@ func (qb *QueryBuilder) PrepareQueries(params *v3.QueryRangeParamsV3) (map[strin
|
||||
case v3.DataSourceLogs:
|
||||
// for ts query with limit replace it as it is already formed
|
||||
if compositeQuery.PanelType == v3.PanelTypeGraph && query.Limit > 0 && len(query.GroupBy) > 0 {
|
||||
limitQuery, err := qb.options.BuildLogQuery(start, end, compositeQuery.QueryType, compositeQuery.PanelType, query, v3.QBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit, UseJSONBody: params.UseJSONBody})
|
||||
limitQuery, err := qb.options.BuildLogQuery(start, end, compositeQuery.QueryType, compositeQuery.PanelType, query, v3.QBOptions{GraphLimitQtype: constants.FirstQueryGraphLimit})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
placeholderQuery, err := qb.options.BuildLogQuery(start, end, compositeQuery.QueryType, compositeQuery.PanelType, query, v3.QBOptions{GraphLimitQtype: constants.SecondQueryGraphLimit, UseJSONBody: params.UseJSONBody})
|
||||
placeholderQuery, err := qb.options.BuildLogQuery(start, end, compositeQuery.QueryType, compositeQuery.PanelType, query, v3.QBOptions{GraphLimitQtype: constants.SecondQueryGraphLimit})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := fmt.Sprintf(placeholderQuery, limitQuery)
|
||||
queries[queryName] = query
|
||||
} else {
|
||||
queryString, err := qb.options.BuildLogQuery(start, end, compositeQuery.QueryType, compositeQuery.PanelType, query, v3.QBOptions{GraphLimitQtype: "", UseJSONBody: params.UseJSONBody})
|
||||
queryString, err := qb.options.BuildLogQuery(start, end, compositeQuery.QueryType, compositeQuery.PanelType, query, v3.QBOptions{GraphLimitQtype: ""})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -196,17 +196,13 @@ const (
|
||||
"CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool," +
|
||||
"CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string," +
|
||||
"CAST((scope_string_key, scope_string_value), 'Map(String, String)') as scope "
|
||||
logsSQLSelectV2Head = "SELECT " +
|
||||
"timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, "
|
||||
logsSQLSelectV2Tail = "attributes_string, " +
|
||||
LogsSQLSelectV2 = "SELECT " +
|
||||
"timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, " +
|
||||
"attributes_string, " +
|
||||
"attributes_number, " +
|
||||
"attributes_bool, " +
|
||||
"resources_string, " +
|
||||
"scope_string "
|
||||
LogsSQLSelectV2 = logsSQLSelectV2Head + "body, " + logsSQLSelectV2Tail
|
||||
// Orgs on JSON bodies keep the body in body_v2 and have the body column written empty.
|
||||
// Selected as JSON so the response carries the same body object v5 returns.
|
||||
LogsSQLSelectV2WithBodyJSON = logsSQLSelectV2Head + "body_v2 as body, " + logsSQLSelectV2Tail
|
||||
TracesExplorerViewSQLSelectWithSubQuery = "(SELECT traceID, durationNano, " +
|
||||
"serviceName, name FROM %s.%s WHERE parentSpanID = '' AND %s ORDER BY durationNano DESC LIMIT 1 BY traceID"
|
||||
TracesExplorerViewSQLSelectBeforeSubQuery = "SELECT subQuery.serviceName as `subQuery.serviceName`, subQuery.name as `subQuery.name`, count() AS " +
|
||||
|
||||
@@ -435,8 +435,6 @@ type QueryRangeParamsV3 struct {
|
||||
NoCache bool `json:"noCache"`
|
||||
Version string `json:"-"`
|
||||
FormatForWeb bool `json:"formatForWeb,omitempty"`
|
||||
// Resolved from the use_json_body feature flag by the handler, never sent by clients.
|
||||
UseJSONBody bool `json:"-"`
|
||||
}
|
||||
|
||||
func (q *QueryRangeParamsV3) Clone() *QueryRangeParamsV3 {
|
||||
@@ -452,7 +450,6 @@ func (q *QueryRangeParamsV3) Clone() *QueryRangeParamsV3 {
|
||||
NoCache: q.NoCache,
|
||||
Version: q.Version,
|
||||
FormatForWeb: q.FormatForWeb,
|
||||
UseJSONBody: q.UseJSONBody,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1472,5 +1469,4 @@ type MetricMetadataResponse struct {
|
||||
type QBOptions struct {
|
||||
GraphLimitQtype string
|
||||
IsLivetailQuery bool
|
||||
UseJSONBody bool
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -239,7 +239,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddSavedViewTuplesFactory(sqlstore),
|
||||
sqlmigration.NewFixSavedViewSelectedFieldsFactory(sqlstore),
|
||||
sqlmigration.NewBackfillSavedViewRequestTypeFactory(sqlstore),
|
||||
sqlmigration.NewRestructureAuthDomainConfigFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"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)
|
||||
}
|
||||
|
||||
// remove deletes a legacy row the migration cannot convert. Such rows never
|
||||
// carry the {kind, spec} envelope, so the reader would reject them; dropping
|
||||
// them here keeps the read path from failing on a document it cannot decode.
|
||||
func (migration *restructureAuthDomainConfig) remove(ctx context.Context, tx bun.Tx, id string) error {
|
||||
_, err := tx.NewDelete().Model((*restructureAuthDomainRow)(nil)).Where("id = ?", id).Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
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, "removing auth domain with unreadable data", slog.String("auth_domain_id", row.ID), errors.Attr(err))
|
||||
if err := migration.remove(ctx, tx, row.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
ssoTypeRaw, ok := legacy["ssoType"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var ssoType string
|
||||
if err := json.Unmarshal(ssoTypeRaw, &ssoType); err != nil {
|
||||
migration.logger.WarnContext(ctx, "removing auth domain with unreadable ssoType", slog.String("auth_domain_id", row.ID), errors.Attr(err))
|
||||
if err := migration.remove(ctx, tx, row.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Documents written before the provider enum became a valuer.String spell
|
||||
// the discriminator uppercase ("SAML", "GOOGLE_AUTH"); reads lowercase it
|
||||
// through valuer.NewString, so those rows still carry the original casing
|
||||
// on disk. Every lookup and spec rewrite below keys off this value.
|
||||
ssoType = strings.ToLower(strings.TrimSpace(ssoType))
|
||||
|
||||
kind, ok := legacySSOTypeToKind[ssoType]
|
||||
if !ok {
|
||||
migration.logger.WarnContext(ctx, "removing auth domain with unknown ssoType", slog.String("auth_domain_id", row.ID), slog.String("sso_type", ssoType))
|
||||
if err := migration.remove(ctx, tx, row.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
spec, ok := legacy[legacySSOTypeToConfigKey[ssoType]]
|
||||
if !ok || string(spec) == "null" {
|
||||
migration.logger.WarnContext(ctx, "removing auth domain with missing provider config", slog.String("auth_domain_id", row.ID), slog.String("sso_type", ssoType))
|
||||
if err := migration.remove(ctx, tx, row.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ssoType == "saml" {
|
||||
samlSpec := make(map[string]json.RawMessage)
|
||||
if err := json.Unmarshal(spec, &samlSpec); err != nil {
|
||||
migration.logger.WarnContext(ctx, "removing auth domain with unreadable saml config", slog.String("auth_domain_id", row.ID), errors.Attr(err))
|
||||
if err := migration.remove(ctx, tx, row.ID); err != nil {
|
||||
return 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, "removing auth domain with unreadable google config", slog.String("auth_domain_id", row.ID), errors.Attr(err))
|
||||
if err := migration.remove(ctx, tx, row.ID); err != nil {
|
||||
return 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
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func TestRestructureAuthDomainConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
data string
|
||||
expectedData string
|
||||
expectedRemoved bool
|
||||
}{
|
||||
{
|
||||
// Rows written before the provider enum became a valuer.String spell
|
||||
// the discriminator uppercase and were never rewritten on disk.
|
||||
name: "UppercaseSAML",
|
||||
data: `{"ssoEnabled":true,"ssoType":"SAML","samlConfig":{"samlEntity":"entity","samlIdp":"https://idp.example.com/sso","samlCert":"cert"},"roleMapping":{"defaultRole":"signoz-admin"}}`,
|
||||
expectedData: `{"enabled":true,"config":{"kind":"saml","spec":{"entityId":"entity","location":"https://idp.example.com/sso","certificate":"cert"}},"roleMapping":{"defaultRole":"signoz-admin"}}`,
|
||||
},
|
||||
{
|
||||
name: "UppercaseGoogleAuth",
|
||||
data: `{"ssoEnabled":true,"ssoType":"GOOGLE_AUTH","googleAuthConfig":{"clientId":"cid","clientSecret":"secret","redirectURI":"https://example.com/callback"}}`,
|
||||
expectedData: `{"enabled":true,"config":{"kind":"google","spec":{"clientId":"cid","clientSecret":"secret"}}}`,
|
||||
},
|
||||
{
|
||||
name: "MixedCaseWithSurroundingWhitespace",
|
||||
data: `{"ssoEnabled":false,"ssoType":" Saml ","samlConfig":{"samlEntity":"entity","samlIdp":"location","samlCert":"cert"}}`,
|
||||
expectedData: `{"enabled":false,"config":{"kind":"saml","spec":{"entityId":"entity","location":"location","certificate":"cert"}}}`,
|
||||
},
|
||||
{
|
||||
name: "LowercaseSAML",
|
||||
data: `{"ssoEnabled":true,"ssoType":"saml","samlConfig":{"samlEntity":"entity","samlIdp":"location","samlCert":"cert","fieldFromANewerVersion":true}}`,
|
||||
expectedData: `{"enabled":true,"config":{"kind":"saml","spec":{"entityId":"entity","location":"location","certificate":"cert","fieldFromANewerVersion":true}}}`,
|
||||
},
|
||||
{
|
||||
name: "LowercaseGoogleAuthWithWorkspaceGroups",
|
||||
data: `{"ssoEnabled":true,"ssoType":"google_auth","googleAuthConfig":{"clientId":"cid","clientSecret":"secret","redirectURI":"https://example.com/callback","fetchGroups":true,"serviceAccountJson":"{}","domainToAdminEmail":{"*":"admin@example.com"},"allowedGroups":["eng@example.com"]},"roleMapping":{"defaultRole":"signoz-viewer","groupMappings":{"eng":"signoz-editor"}}}`,
|
||||
expectedData: `{"enabled":true,"config":{"kind":"google","spec":{"clientId":"cid","clientSecret":"secret","fetchGroups":true,"serviceAccountJson":"{}","domainToAdminEmail":{"*":"admin@example.com"},"allowedGroups":["eng@example.com"]}},"roleMapping":{"defaultRole":"signoz-viewer","groupMappings":{"eng":"signoz-editor"}}}`,
|
||||
},
|
||||
{
|
||||
name: "LowercaseOIDC",
|
||||
data: `{"ssoEnabled":true,"ssoType":"oidc","oidcConfig":{"clientId":"cid","clientSecret":"secret","issuer":"https://issuer.example.com"}}`,
|
||||
expectedData: `{"enabled":true,"config":{"kind":"oidc","spec":{"clientId":"cid","clientSecret":"secret","issuer":"https://issuer.example.com"}}}`,
|
||||
},
|
||||
{
|
||||
name: "MissingSSOEnabledDefaultsToDisabled",
|
||||
data: `{"ssoType":"saml","samlConfig":{"samlEntity":"entity","samlIdp":"location","samlCert":"cert"}}`,
|
||||
expectedData: `{"enabled":false,"config":{"kind":"saml","spec":{"entityId":"entity","location":"location","certificate":"cert"}}}`,
|
||||
},
|
||||
{
|
||||
name: "NullRoleMappingIsDropped",
|
||||
data: `{"ssoEnabled":true,"ssoType":"saml","samlConfig":{"samlEntity":"entity","samlIdp":"location","samlCert":"cert"},"roleMapping":null}`,
|
||||
expectedData: `{"enabled":true,"config":{"kind":"saml","spec":{"entityId":"entity","location":"location","certificate":"cert"}}}`,
|
||||
},
|
||||
{
|
||||
name: "AlreadyRestructured",
|
||||
data: `{"enabled":true,"config":{"kind":"saml","spec":{"entityId":"entity","location":"location","certificate":"cert"}}}`,
|
||||
expectedData: `{"enabled":true,"config":{"kind":"saml","spec":{"entityId":"entity","location":"location","certificate":"cert"}}}`,
|
||||
},
|
||||
{
|
||||
name: "UnknownSSOType",
|
||||
data: `{"ssoEnabled":true,"ssoType":"ldap","samlConfig":{"samlEntity":"entity"}}`,
|
||||
expectedRemoved: true,
|
||||
},
|
||||
{
|
||||
name: "NullProviderConfig",
|
||||
data: `{"ssoEnabled":true,"ssoType":"saml","samlConfig":null}`,
|
||||
expectedRemoved: true,
|
||||
},
|
||||
{
|
||||
name: "UnreadableData",
|
||||
data: `not json`,
|
||||
expectedRemoved: true,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
sqldb, err := sql.Open("sqlite", "file:"+filepath.Join(t.TempDir(), "test.db"))
|
||||
require.NoError(t, err)
|
||||
defer sqldb.Close()
|
||||
|
||||
db := bun.NewDB(sqldb, sqlitedialect.New())
|
||||
|
||||
// Only the two columns the migration reads and writes.
|
||||
_, err = db.ExecContext(ctx, `CREATE TABLE auth_domain (id TEXT PRIMARY KEY, data TEXT NOT NULL)`)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, testCase := range testCases {
|
||||
_, err := db.NewInsert().
|
||||
Model(&restructureAuthDomainRow{ID: testCase.name, Data: testCase.data}).
|
||||
Exec(ctx)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
migration := &restructureAuthDomainConfig{logger: slog.New(slog.DiscardHandler)}
|
||||
|
||||
// Running twice pins idempotency: a restructured document carries no
|
||||
// ssoType, so the second pass must leave every row untouched.
|
||||
for range 2 {
|
||||
require.NoError(t, migration.Up(ctx, db))
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
row := new(restructureAuthDomainRow)
|
||||
err := db.NewSelect().Model(row).Where("id = ?", testCase.name).Scan(ctx)
|
||||
|
||||
if testCase.expectedRemoved {
|
||||
assert.ErrorIs(t, err, sql.ErrNoRows)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, testCase.expectedData, row.Data)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package authtypes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
@@ -10,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"
|
||||
)
|
||||
|
||||
@@ -30,81 +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.
|
||||
//
|
||||
// rejectUnknownSpecFields decodes into a method-less alias of the spec: a type
|
||||
// carrying its own UnmarshalJSON consumes the bytes itself, which would bypass
|
||||
// DisallowUnknownFields. It only sees the spec's own fields, not those of
|
||||
// nested objects that unmarshal themselves.
|
||||
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
|
||||
},
|
||||
rejectUnknownSpecFields: func(data []byte) error {
|
||||
type alias SamlConfig
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
return decoder.Decode(new(alias))
|
||||
},
|
||||
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
|
||||
},
|
||||
rejectUnknownSpecFields: func(data []byte) error {
|
||||
type alias GoogleConfig
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
return decoder.Decode(new(alias))
|
||||
},
|
||||
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
|
||||
},
|
||||
rejectUnknownSpecFields: func(data []byte) error {
|
||||
type alias OIDCConfig
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
return decoder.Decode(new(alias))
|
||||
},
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -113,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 {
|
||||
@@ -135,51 +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"`
|
||||
|
||||
// rawSpec retains the undecoded spec bytes from UnmarshalJSON so the
|
||||
// request path can run its foreign-field check without re-parsing the body.
|
||||
rawSpec json.RawMessage
|
||||
}
|
||||
|
||||
// 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)
|
||||
rejectUnknownSpecFields func(data []byte) 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 {
|
||||
@@ -207,262 +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
|
||||
typ.rawSpec = specData
|
||||
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
|
||||
}
|
||||
|
||||
// rejectForeignSpecFields rejects a request whose config.spec carries fields the
|
||||
// declared kind does not define. Without it a spec belonging to another provider
|
||||
// decodes as a partial config of the declared kind whenever their required
|
||||
// fields overlap — an oidc spec sent as kind google loses its issuer and points
|
||||
// the domain at Google instead of the intended provider.
|
||||
//
|
||||
// This is a request-shape check by design: stored documents keep decoding
|
||||
// leniently so a rollback can still read a document a newer binary wrote, and so
|
||||
// removing a field later does not need a migration.
|
||||
func rejectForeignSpecFields(config AuthDomainConfig) error {
|
||||
for _, variant := range authDomainConfigVariants {
|
||||
if variant.kind != config.Kind {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := variant.rejectUnknownSpecFields(config.rawSpec); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "invalid %q spec", config.Kind.StringValue())
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
if err := rejectForeignSpecFields(temp.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*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")
|
||||
}
|
||||
|
||||
if err := rejectForeignSpecFields(temp.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*typ = UpdatableAuthDomain(temp)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
package authtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPostableAuthDomainSpecFields(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
body string
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "SAMLSpec",
|
||||
body: `{"name":"a.test","enabled":true,"config":{"kind":"saml","spec":{"entityId":"e","location":"l","certificate":"c"}}}`,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "SAMLSpecWithAttributeMapping",
|
||||
body: `{"name":"a.test","enabled":true,"config":{"kind":"saml","spec":{"entityId":"e","location":"l","certificate":"c","insecureSkipAuthNRequestsSigned":true,"attributeMapping":{"email":"mail"}}}}`,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "GoogleSpec",
|
||||
body: `{"name":"a.test","enabled":true,"config":{"kind":"google","spec":{"clientId":"c","clientSecret":"s","insecureSkipEmailVerified":false}}}`,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "GoogleSpecWithWorkspaceGroups",
|
||||
body: `{"name":"a.test","enabled":true,"config":{"kind":"google","spec":{"clientId":"c","clientSecret":"s","fetchGroups":true,"serviceAccountJson":"{}","domainToAdminEmail":{"*":"admin@a.test"},"fetchTransitiveGroupMembership":true,"allowedGroups":["g@a.test"]}}}`,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "OIDCSpec",
|
||||
body: `{"name":"a.test","enabled":true,"config":{"kind":"oidc","spec":{"issuer":"https://issuer.a.test","clientId":"c","clientSecret":"s"}}}`,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
// clientId and clientSecret are google's only required fields, so an
|
||||
// oidc spec satisfies them and would otherwise be accepted with its
|
||||
// issuer silently dropped.
|
||||
name: "OIDCSpecUnderGoogleKind",
|
||||
body: `{"name":"a.test","enabled":true,"config":{"kind":"google","spec":{"issuer":"https://issuer.a.test","clientId":"c","clientSecret":"s","claimMapping":{"email":"mail"}}}}`,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "GoogleSpecUnderOIDCKind",
|
||||
body: `{"name":"a.test","enabled":true,"config":{"kind":"oidc","spec":{"issuer":"https://issuer.a.test","clientId":"c","clientSecret":"s","fetchGroups":true}}}`,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "SAMLSpecUnderGoogleKind",
|
||||
body: `{"name":"a.test","enabled":true,"config":{"kind":"google","spec":{"clientId":"c","clientSecret":"s","entityId":"e"}}}`,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "UnrecognizedFieldInGoogleSpec",
|
||||
body: `{"name":"a.test","enabled":true,"config":{"kind":"google","spec":{"clientId":"c","clientSecret":"s","redirectURI":"https://a.test/cb"}}}`,
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
postable := new(PostableAuthDomain)
|
||||
err := json.Unmarshal([]byte(testCase.body), postable)
|
||||
|
||||
if testCase.expectedError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The enforcement toggle and the edit modal both PUT a full replacement, so the
|
||||
// update body needs the same check as the create body.
|
||||
func TestUpdatableAuthDomainSpecFields(t *testing.T) {
|
||||
updatable := new(UpdatableAuthDomain)
|
||||
err := json.Unmarshal([]byte(`{"enabled":true,"config":{"kind":"google","spec":{"issuer":"https://issuer.a.test","clientId":"c","clientSecret":"s"}}}`), updatable)
|
||||
assert.Error(t, err)
|
||||
|
||||
updatable = new(UpdatableAuthDomain)
|
||||
err = json.Unmarshal([]byte(`{"enabled":true,"config":{"kind":"google","spec":{"clientId":"c","clientSecret":"s"}}}`), updatable)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Reads stay lenient on purpose: a document written by a newer binary has to keep
|
||||
// loading after a rollback, and dropping a field must not need a migration.
|
||||
func TestNewAuthDomainFromStorableAuthDomainKeepsUnknownSpecFields(t *testing.T) {
|
||||
storable := &StorableAuthDomain{
|
||||
Data: `{"enabled":true,"config":{"kind":"google","spec":{"clientId":"c","clientSecret":"s","fieldFromANewerVersion":"x"}}}`,
|
||||
}
|
||||
|
||||
authDomain, err := NewAuthDomainFromStorableAuthDomain(storable)
|
||||
require.NoError(t, err)
|
||||
|
||||
google, err := authDomain.Config().GoogleConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, AuthNProviderGoogle, authDomain.Kind())
|
||||
assert.Equal(t, "c", google.ClientID)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@ echo "Generating TypeScript parser..."
|
||||
mkdir -p frontend/src/parser
|
||||
|
||||
# Generate TypeScript parser
|
||||
(cd grammar && antlr4 -Dlanguage=TypeScript -o ../frontend/src/parser FilterQuery.g4 -visitor)
|
||||
antlr4 -Dlanguage=TypeScript -o frontend/src/parser grammar/FilterQuery.g4 -visitor
|
||||
|
||||
echo "TypeScript parser generation complete"
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
import { expect, type Locator, 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;
|
||||
}
|
||||
|
||||
/** Names of every auth domain in the org, across all list pages. */
|
||||
export async function listAuthDomainNamesViaApi(page: Page): Promise<string[]> {
|
||||
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<{ name: string }> | null;
|
||||
};
|
||||
return (json.data ?? []).map((domain) => domain.name);
|
||||
}
|
||||
|
||||
/** 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate the list row for `name`, advancing through the pager when needed. The
|
||||
* table paginates, and a shared stack holds domains this suite did not seed, so
|
||||
* a freshly created row is not necessarily on the page currently shown.
|
||||
*/
|
||||
export async function findAuthDomainRow(
|
||||
page: Page,
|
||||
name: string,
|
||||
): Promise<Locator> {
|
||||
const row = page.getByTestId(`auth-domain-row-${name}`);
|
||||
const nextPage = page.locator('.auth-domain-list .ant-pagination-next');
|
||||
|
||||
// Bounded: a pager that never reports itself disabled must not spin forever.
|
||||
for (let visited = 0; visited < 25; visited += 1) {
|
||||
try {
|
||||
await row.waitFor({ state: 'attached', timeout: 2_000 });
|
||||
return row;
|
||||
} catch {
|
||||
// Not on the page currently shown; fall through to the pager.
|
||||
}
|
||||
|
||||
if (
|
||||
(await nextPage.count()) === 0 ||
|
||||
(await nextPage.getAttribute('aria-disabled')) === 'true' ||
|
||||
(await nextPage.evaluate((node) =>
|
||||
node.classList.contains('ant-pagination-disabled'),
|
||||
))
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
await nextPage.click();
|
||||
}
|
||||
|
||||
throw new Error(`auth domain row not found in the list: ${name}`);
|
||||
}
|
||||
|
||||
/** Open the Configure (edit) modal for the domain row named `name`. */
|
||||
export async function openConfigureAuthDomain(
|
||||
page: Page,
|
||||
name: string,
|
||||
): Promise<void> {
|
||||
const row = await findAuthDomainRow(page, name);
|
||||
await row.getByTestId('auth-domain-configure').click();
|
||||
await expect(page.getByTestId('auth-domain-form')).toBeVisible();
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
import { expect, test } from '../../fixtures/auth';
|
||||
import {
|
||||
createGoogleAuthDomainViaApi,
|
||||
deleteAuthDomainByNameViaApi,
|
||||
findAuthDomainRow,
|
||||
gotoAuthDomains,
|
||||
listAuthDomainNamesViaApi,
|
||||
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 = await findAuthDomainRow(page, domain);
|
||||
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 = (await findAuthDomainRow(page, 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();
|
||||
const reloadedRow = await findAuthDomainRow(page, domain);
|
||||
await expect(reloadedRow.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);
|
||||
const row = await findAuthDomainRow(page, domain);
|
||||
await row.getByTestId('auth-domain-delete').click();
|
||||
await page.getByTestId('auth-domain-delete-confirm').click();
|
||||
|
||||
await expect(page.getByText('Domain deleted successfully')).toBeVisible();
|
||||
// Asserted against the list rather than the rendered page: an absent row
|
||||
// proves nothing while the table paginates.
|
||||
expect(await listAuthDomainNamesViaApi(page)).not.toContain(domain);
|
||||
});
|
||||
|
||||
test('TC-08 enforce sso toggle preserves the role mapping', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const domain = 'sso-toggle-rolemap.example.com';
|
||||
cleanupNames.push(domain);
|
||||
await deleteAuthDomainByNameViaApi(page, domain);
|
||||
await createGoogleAuthDomainViaApi(page, {
|
||||
name: domain,
|
||||
enabled: false,
|
||||
roleMapping: {
|
||||
defaultRole: 'signoz-editor',
|
||||
groupMappings: { engineers: 'signoz-editor', support: 'signoz-viewer' },
|
||||
},
|
||||
});
|
||||
|
||||
await gotoAuthDomains(page);
|
||||
|
||||
// The toggle PUTs a full replacement, so a role mapping it fails to echo
|
||||
// back is silently dropped.
|
||||
const putResponse = page.waitForResponse(
|
||||
(res) =>
|
||||
res.request().method() === 'PUT' &&
|
||||
res.url().includes('/api/v2/auth_domains/'),
|
||||
);
|
||||
const row = await findAuthDomainRow(page, domain);
|
||||
await row.getByTestId('auth-domain-enforce-sso').click();
|
||||
expect((await putResponse).status()).toBe(204);
|
||||
|
||||
await page.reload();
|
||||
await openConfigureAuthDomain(page, domain);
|
||||
await page.getByTestId('role-mapping-header').click();
|
||||
|
||||
const rows = page.getByTestId('role-mapping-row');
|
||||
await expect(rows).toHaveCount(2);
|
||||
await expect(rows.nth(0).getByTestId('role-mapping-group-name')).toHaveValue(
|
||||
'engineers',
|
||||
);
|
||||
await expect(rows.nth(1).getByTestId('role-mapping-group-name')).toHaveValue(
|
||||
'support',
|
||||
);
|
||||
});
|
||||
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,58 +117,18 @@ def test_create_invalid(
|
||||
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
|
||||
# The reverse direction: an oidc spec under kind google satisfies google's
|
||||
# required clientId and clientSecret, so only the foreign issuer and
|
||||
# claimMapping fields distinguish it.
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "domain.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "google",
|
||||
"spec": {
|
||||
"issuer": "https://issuer.integration.test",
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
"claimMapping": {"email": "mail"},
|
||||
},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -186,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,
|
||||
@@ -206,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",
|
||||
},
|
||||
@@ -226,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}"},
|
||||
@@ -250,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
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -277,411 +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
|
||||
|
||||
role_mapping = {
|
||||
"defaultRole": "signoz-editor",
|
||||
"groupMappings": {"admin-group": "signoz-admin", "dev-team": "signoz-editor"},
|
||||
"useRoleAttribute": False,
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
},
|
||||
"roleMapping": role_mapping,
|
||||
},
|
||||
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 and role mapping must both survive the
|
||||
# round trip, since the UI resends them verbatim on every toggle.
|
||||
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",
|
||||
},
|
||||
},
|
||||
"roleMapping": role_mapping,
|
||||
},
|
||||
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"},
|
||||
},
|
||||
}
|
||||
assert data["roleMapping"] == 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
|
||||
|
||||
@@ -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