mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-10 15:00:47 +01:00
Compare commits
27 Commits
issue_2899
...
fix/authdo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21069bc652 | ||
|
|
5b8785480a | ||
|
|
fee8eb9a29 | ||
|
|
90d884c113 | ||
|
|
f44d6c7c84 | ||
|
|
07eb864447 | ||
|
|
df2658de38 | ||
|
|
125b5b3ab8 | ||
|
|
e25bb31f28 | ||
|
|
3a6c54a47a | ||
|
|
015689f587 | ||
|
|
3aacca0d52 | ||
|
|
f4d428cbee | ||
|
|
60cb50af25 | ||
|
|
609a9c9bf9 | ||
|
|
b955f1eee1 | ||
|
|
4f400da770 | ||
|
|
de3ba9d20c | ||
|
|
fe447e7f35 | ||
|
|
94ce3e51f1 | ||
|
|
a5ddafc5db | ||
|
|
54e2caaac2 | ||
|
|
9313e7bea5 | ||
|
|
6b58b6fcae | ||
|
|
cc5d3574a4 | ||
|
|
6175ff3fc2 | ||
|
|
db7ec08969 |
@@ -464,27 +464,50 @@ 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/AuthtypesSamlConfig'
|
||||
- $ref: '#/components/schemas/AuthtypesGoogleConfig'
|
||||
- $ref: '#/components/schemas/AuthtypesOIDCConfig'
|
||||
- $ref: '#/components/schemas/AuthtypesAuthDomainConfigSAML'
|
||||
- $ref: '#/components/schemas/AuthtypesAuthDomainConfigGoogle'
|
||||
- $ref: '#/components/schemas/AuthtypesAuthDomainConfigOIDC'
|
||||
type: object
|
||||
AuthtypesAuthDomainConfigGoogle:
|
||||
properties:
|
||||
googleAuthConfig:
|
||||
$ref: '#/components/schemas/AuthtypesGoogleConfig'
|
||||
oidcConfig:
|
||||
$ref: '#/components/schemas/AuthtypesOIDCConfig'
|
||||
roleMapping:
|
||||
$ref: '#/components/schemas/AuthtypesRoleMapping'
|
||||
samlConfig:
|
||||
$ref: '#/components/schemas/AuthtypesSamlConfig'
|
||||
ssoEnabled:
|
||||
type: boolean
|
||||
ssoType:
|
||||
kind:
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
spec:
|
||||
$ref: '#/components/schemas/AuthtypesGoogleConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AuthtypesAuthDomainConfigOIDC:
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
spec:
|
||||
$ref: '#/components/schemas/AuthtypesOIDCConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AuthtypesAuthDomainConfigSAML:
|
||||
properties:
|
||||
kind:
|
||||
$ref: '#/components/schemas/AuthtypesAuthNProvider'
|
||||
spec:
|
||||
$ref: '#/components/schemas/AuthtypesSamlConfig'
|
||||
required:
|
||||
- kind
|
||||
- spec
|
||||
type: object
|
||||
AuthtypesAuthNProvider:
|
||||
enum:
|
||||
- google_auth
|
||||
- google
|
||||
- saml
|
||||
- email_password
|
||||
- oidc
|
||||
@@ -531,12 +554,16 @@ 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
|
||||
@@ -601,6 +628,7 @@ components:
|
||||
clientId:
|
||||
type: string
|
||||
clientSecret:
|
||||
format: password
|
||||
type: string
|
||||
domainToAdminEmail:
|
||||
additionalProperties:
|
||||
@@ -612,10 +640,12 @@ components:
|
||||
type: boolean
|
||||
insecureSkipEmailVerified:
|
||||
type: boolean
|
||||
redirectURI:
|
||||
type: string
|
||||
serviceAccountJson:
|
||||
format: password
|
||||
type: string
|
||||
required:
|
||||
- clientId
|
||||
- clientSecret
|
||||
type: object
|
||||
AuthtypesOIDCConfig:
|
||||
properties:
|
||||
@@ -624,6 +654,7 @@ components:
|
||||
clientId:
|
||||
type: string
|
||||
clientSecret:
|
||||
format: password
|
||||
type: string
|
||||
getUserInfo:
|
||||
type: boolean
|
||||
@@ -633,6 +664,10 @@ components:
|
||||
type: string
|
||||
issuerAlias:
|
||||
type: string
|
||||
required:
|
||||
- issuer
|
||||
- clientId
|
||||
- clientSecret
|
||||
type: object
|
||||
AuthtypesOrgSessionContext:
|
||||
properties:
|
||||
@@ -654,8 +689,15 @@ 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:
|
||||
@@ -762,14 +804,18 @@ components:
|
||||
properties:
|
||||
attributeMapping:
|
||||
$ref: '#/components/schemas/AuthtypesAttributeMapping'
|
||||
certificate:
|
||||
type: string
|
||||
entityId:
|
||||
type: string
|
||||
insecureSkipAuthNRequestsSigned:
|
||||
type: boolean
|
||||
samlCert:
|
||||
type: string
|
||||
samlEntity:
|
||||
type: string
|
||||
samlIdp:
|
||||
location:
|
||||
type: string
|
||||
required:
|
||||
- entityId
|
||||
- location
|
||||
- certificate
|
||||
type: object
|
||||
AuthtypesSessionContext:
|
||||
properties:
|
||||
@@ -809,6 +855,12 @@ components:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/components/schemas/AuthtypesAuthDomainConfig'
|
||||
enabled:
|
||||
type: boolean
|
||||
roleMapping:
|
||||
$ref: '#/components/schemas/AuthtypesRoleMapping'
|
||||
required:
|
||||
- config
|
||||
type: object
|
||||
AuthtypesUpdatableRole:
|
||||
properties:
|
||||
@@ -10602,275 +10654,6 @@ 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
|
||||
@@ -14673,6 +14456,275 @@ 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,31 +61,37 @@ type Channel struct {
|
||||
|
||||
```go
|
||||
type AuthDomain struct {
|
||||
storableAuthDomain *StorableAuthDomain
|
||||
authDomainConfig *AuthDomainConfig
|
||||
storableAuthDomain *StorableAuthDomain
|
||||
storableAuthDomainConfig *StorableAuthDomainConfig
|
||||
}
|
||||
|
||||
type StorableAuthDomain struct {
|
||||
bun.BaseModel `bun:"table:auth_domain"`
|
||||
types.Identifiable
|
||||
Name string `bun:"name"`
|
||||
Data string `bun:"data"` // AuthDomainConfig serialized as JSON
|
||||
Data string `bun:"data"` // StorableAuthDomainConfig serialized as JSON
|
||||
OrgID valuer.UUID `bun:"org_id"`
|
||||
types.TimeAuditable
|
||||
}
|
||||
|
||||
type PostableAuthDomain struct {
|
||||
Config AuthDomainConfig `json:"config"`
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name" required:"true"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Config AuthDomainConfig `json:"config" required:"true"`
|
||||
RoleMapping *RoleMapping `json:"roleMapping"`
|
||||
}
|
||||
|
||||
type UpdateableAuthDomain struct {
|
||||
Config AuthDomainConfig `json:"config"` // Name intentionally absent
|
||||
type UpdatableAuthDomain struct {
|
||||
Enabled bool `json:"enabled"` // Name intentionally absent
|
||||
Config AuthDomainConfig `json:"config" required:"true"`
|
||||
RoleMapping *RoleMapping `json:"roleMapping"`
|
||||
}
|
||||
|
||||
type GettableAuthDomain struct {
|
||||
*StorableAuthDomain
|
||||
*AuthDomainConfig
|
||||
StorableAuthDomain
|
||||
Enabled bool `json:"enabled"`
|
||||
Config AuthDomainConfig `json:"config"`
|
||||
RoleMapping *RoleMapping `json:"roleMapping"`
|
||||
AuthNProviderInfo *AuthNProviderInfo `json:"authNProviderInfo"`
|
||||
}
|
||||
```
|
||||
@@ -93,11 +99,74 @@ 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.
|
||||
- `UpdateableAuthDomain` 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; `AuthDomainConfig` is a kind/spec envelope.
|
||||
- `UpdatableAuthDomain` 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 `authDomainConfig` — and owns business methods such as `Update(config)`. Conversions use the `New<Output>From<Input>` form: `NewAuthDomainFromConfig`, `NewAuthDomainFromStorableAuthDomain`, `NewGettableAuthDomainFromAuthDomain`.
|
||||
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`.
|
||||
|
||||
## Sum types: the kind/spec envelope
|
||||
|
||||
When a domain type is a *sum type* — exactly one of several variants, selected by a discriminator — model it as an envelope with a `kind` and a `spec`:
|
||||
|
||||
```go
|
||||
type FooConfig struct {
|
||||
Kind FooKind `json:"kind" required:"true"`
|
||||
Spec any `json:"spec" required:"true"`
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{ "kind": "bar", "spec": { "url": "...", "timeout": "30s" } }
|
||||
```
|
||||
|
||||
`Kind` is a `valuer.String` enum implementing `Enum()`; `Spec` holds exactly one concrete variant type (`BarSpec`, `BazSpec`, …). `RuleThresholdData` and `EvaluationEnvelope` in `pkg/types/ruletypes/` are the canonical in-tree examples; the dashboard panel/query/variable plugins in `pkg/types/dashboardtypes/` are the same pattern behind generics. (`QueryEnvelope` in querybuildertypes uses `type` as the discriminator key for historical reasons; new envelopes use `kind`.)
|
||||
|
||||
### The envelope goes at the point of variance, not the resource root
|
||||
|
||||
Put the envelope on the field that actually varies. The resource root is almost never a sum type — a `Foo` has a `name` and an `enabled` flag regardless of which kind it is configured with; only its configuration varies, so the envelope is the `config` field:
|
||||
|
||||
```json
|
||||
{ "name": "my-foo", "enabled": true, "config": { "kind": "bar", "spec": { "...": "..." } } }
|
||||
```
|
||||
|
||||
Hoisting `kind`/`spec` to the root would turn the whole resource into a `oneOf`: every flavor (`PostableFoo`, `UpdatableFoo`, `GettableFoo`) then needs one variant schema per kind, each repeating the common fields; every new common field has to be added to all of them; and generated clients get unions of large objects instead of one small union that narrows on `config.kind`. A root-level `kind` also collides with the resource-model meaning of the word — root `kind` conventionally answers "what resource is this" (`Dashboard`), never "which flavor of config does it hold".
|
||||
|
||||
The existing domains already follow this placement:
|
||||
|
||||
- **Rules** — plain root; envelopes on the varying fields: `thresholds: {kind, spec}` and `evaluation: {kind, spec}`.
|
||||
- **Dashboards** — metadata at the root plus one typed `spec`; the unions sit deep inside, at each panel/query/variable plugin (`{kind, spec}` in `perses_plugin_wrappers.go`).
|
||||
- **Saved views** — root `{schemaVersion, spec}`, where `spec` is a *versioning* envelope holding one fixed type, not a union; the unions are inside it (`spec.queries: [{type, spec}]`). Same word, different job — a versioned body is not a discriminated union.
|
||||
|
||||
### Why this tagging style
|
||||
|
||||
Of the union encodings in common use, the envelope is the *adjacently tagged* one — tag and payload side by side. Variant payloads stay collision-free, and each kind maps to a named wrapper schema that carries the discriminator, which is exactly what OpenAPI generators need. The alternatives lose on those points: *internally tagged* (`{"kind": "bar", ...fields flattened}`) mixes common and variant fields, admits cross-variant key collisions, and forces every variant schema to redeclare the discriminator; *sibling optional fields* (`{"kind": "bar", "barConfig": {}, "bazConfig": {}}`) is the anti-pattern the first rule below exists to prevent.
|
||||
|
||||
The rules that make the envelope work:
|
||||
|
||||
- **Never model variants as sibling fields.** A struct with `Bar *BarSpec`, `Baz *BazSpec` next to a discriminator cannot be expressed as an OpenAPI discriminated union, forces nilability checks on every consumer, and silently admits contradictory payloads (kind=bar with a baz spec). The chosen variant *is* the payload.
|
||||
- **The envelope owns `UnmarshalJSON`.** Decode `kind` first, then switch on it to decode and validate the matching concrete type into `Spec`. Unknown kinds and missing specs are rejected at the boundary:
|
||||
|
||||
```go
|
||||
func (typ *FooConfig) UnmarshalJSON(data []byte) error {
|
||||
var raw map[string]json.RawMessage
|
||||
// ... unmarshal raw, decode raw["kind"] ...
|
||||
switch kind {
|
||||
case FooKindBar:
|
||||
spec := BarSpec{}
|
||||
if err := json.Unmarshal(raw["spec"], &spec); err != nil {
|
||||
return err
|
||||
}
|
||||
typ.Spec = spec
|
||||
// ... one case per kind, default rejects ...
|
||||
}
|
||||
typ.Kind = kind
|
||||
return nil
|
||||
}
|
||||
```
|
||||
- **Consumers type-assert on `Spec`** (`config.Spec.(BarSpec)`) after switching on `Kind`. If assertion sites multiply, add typed accessors on the envelope (see `EvaluationEnvelope.GetEvaluation()`).
|
||||
- **OpenAPI needs one unexported variant struct per kind** (`fooConfigBar{Kind; Spec BarSpec}`), exposed via `JSONSchemaOneOf()` and mapped via `PrepareJSONSchema` with the `x-signoz-discriminator` extension. The schema mechanics are covered in [handler.md](handler.md#oneof-with-a-discriminator).
|
||||
- **A legacy persisted shape gets a data migration or a `StorableX`.** When rows were written before the envelope existed, prefer an idempotent `sqlmigration` that rewrites them into the new shape, so the storable type simply nests the envelope. Only when the old shape must keep being written (external writers, rollback windows) keep it in a storable twin and convert at the type boundary.
|
||||
|
||||
## Conventions that tie the flavors together
|
||||
|
||||
@@ -139,6 +208,8 @@ Both are optional. Do not introduce them if `PostableX` already covers the case.
|
||||
|
||||
- Every domain package defines the core type `X`. Only `X` is mandatory.
|
||||
- Add `PostableX` / `GettableX` / `UpdatableX` / `StorableX` one at a time, only when the shape actually diverges from `X`.
|
||||
- Model sum types as a `{kind, spec}` envelope with a validating `UnmarshalJSON` — never as sibling variant fields next to a discriminator.
|
||||
- The envelope goes on the field that varies, never at the resource root — common fields stay on the resource, outside the union.
|
||||
- Domain logic lives on `X`, not on the flavor types.
|
||||
- Conversions can be a `New<Output>From<Input>` constructor or a receiver-style `ToY()` method — pick whichever reads best at the call site.
|
||||
- Use a type alias when two shapes are truly identical.
|
||||
|
||||
@@ -53,10 +53,6 @@ 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
|
||||
@@ -85,6 +81,11 @@ 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
|
||||
@@ -106,14 +107,14 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims == nil && authDomain.AuthDomainConfig().OIDC.GetUserInfo {
|
||||
if claims == nil && oidcConfig.GetUserInfo {
|
||||
claims, err = a.claimsFromUserInfo(ctx, oidcProvider, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
emailClaim, ok := claims[authDomain.AuthDomainConfig().OIDC.ClaimMapping.Email].(string)
|
||||
emailClaim, ok := claims[oidcConfig.ClaimMapping.Email].(string)
|
||||
if !ok {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: missing email in claims")
|
||||
}
|
||||
@@ -123,7 +124,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 !authDomain.AuthDomainConfig().OIDC.InsecureSkipEmailVerified {
|
||||
if !oidcConfig.InsecureSkipEmailVerified {
|
||||
emailVerifiedClaim, ok := claims["email_verified"].(bool)
|
||||
if !ok {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: missing email_verified in claims")
|
||||
@@ -135,14 +136,14 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
|
||||
}
|
||||
|
||||
name := ""
|
||||
if nameClaim := authDomain.AuthDomainConfig().OIDC.ClaimMapping.Name; nameClaim != "" {
|
||||
if nameClaim := oidcConfig.ClaimMapping.Name; nameClaim != "" {
|
||||
if n, ok := claims[nameClaim].(string); ok {
|
||||
name = n
|
||||
}
|
||||
}
|
||||
|
||||
var groups []string
|
||||
if groupsClaim := authDomain.AuthDomainConfig().OIDC.ClaimMapping.Groups; groupsClaim != "" {
|
||||
if groupsClaim := oidcConfig.ClaimMapping.Groups; groupsClaim != "" {
|
||||
if claimValue, exists := claims[groupsClaim]; exists {
|
||||
switch g := claimValue.(type) {
|
||||
case []any:
|
||||
@@ -161,7 +162,7 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
|
||||
}
|
||||
|
||||
role := ""
|
||||
if roleClaim := authDomain.AuthDomainConfig().OIDC.ClaimMapping.Role; roleClaim != "" {
|
||||
if roleClaim := oidcConfig.ClaimMapping.Role; roleClaim != "" {
|
||||
if r, ok := claims[roleClaim].(string); ok {
|
||||
role = r
|
||||
}
|
||||
@@ -177,11 +178,16 @@ 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) {
|
||||
if authDomain.AuthDomainConfig().OIDC.IssuerAlias != "" {
|
||||
ctx = oidc.InsecureIssuerURLContext(ctx, authDomain.AuthDomainConfig().OIDC.IssuerAlias)
|
||||
oidcConfig, err := authDomain.Config().OIDCConfig()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
oidcProvider, err := oidc.NewProvider(ctx, authDomain.AuthDomainConfig().OIDC.Issuer)
|
||||
if oidcConfig.IssuerAlias != "" {
|
||||
ctx = oidc.InsecureIssuerURLContext(ctx, oidcConfig.IssuerAlias)
|
||||
}
|
||||
|
||||
oidcProvider, err := oidc.NewProvider(ctx, oidcConfig.Issuer)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -189,13 +195,13 @@ func (a *AuthN) oidcProviderAndoauth2Config(ctx context.Context, siteURL *url.UR
|
||||
scopes := make([]string, len(defaultScopes))
|
||||
copy(scopes, defaultScopes)
|
||||
|
||||
if authDomain.AuthDomainConfig().RoleMapping != nil && len(authDomain.AuthDomainConfig().RoleMapping.GroupMappings) > 0 {
|
||||
if authDomain.RoleMapping() != nil && len(authDomain.RoleMapping().GroupMappings) > 0 {
|
||||
scopes = append(scopes, "groups")
|
||||
}
|
||||
|
||||
return oidcProvider, &oauth2.Config{
|
||||
ClientID: authDomain.AuthDomainConfig().OIDC.ClientID,
|
||||
ClientSecret: authDomain.AuthDomainConfig().OIDC.ClientSecret,
|
||||
ClientID: oidcConfig.ClientID,
|
||||
ClientSecret: oidcConfig.ClientSecret,
|
||||
Endpoint: oidcProvider.Endpoint(),
|
||||
Scopes: scopes,
|
||||
RedirectURL: (&url.URL{
|
||||
@@ -212,7 +218,12 @@ 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")
|
||||
}
|
||||
|
||||
verifier := provider.Verifier(&oidc.Config{ClientID: authDomain.AuthDomainConfig().OIDC.ClientID})
|
||||
oidcConfig, err := authDomain.Config().OIDCConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
verifier := provider.Verifier(&oidc.Config{ClientID: oidcConfig.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,10 +40,6 @@ 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
|
||||
@@ -73,6 +69,11 @@ 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
|
||||
@@ -101,19 +102,19 @@ func (a *AuthN) HandleCallback(ctx context.Context, formValues url.Values) (*aut
|
||||
}
|
||||
|
||||
name := ""
|
||||
if nameAttribute := authDomain.AuthDomainConfig().SAML.AttributeMapping.Name; nameAttribute != "" {
|
||||
if nameAttribute := samlConfig.AttributeMapping.Name; nameAttribute != "" {
|
||||
if val := assertionInfo.Values.Get(nameAttribute); val != "" {
|
||||
name = val
|
||||
}
|
||||
}
|
||||
|
||||
var groups []string
|
||||
if groupAttribute := authDomain.AuthDomainConfig().SAML.AttributeMapping.Groups; groupAttribute != "" {
|
||||
if groupAttribute := samlConfig.AttributeMapping.Groups; groupAttribute != "" {
|
||||
groups = assertionInfo.Values.GetAll(groupAttribute)
|
||||
}
|
||||
|
||||
role := ""
|
||||
if roleAttribute := authDomain.AuthDomainConfig().SAML.AttributeMapping.Role; roleAttribute != "" {
|
||||
if roleAttribute := samlConfig.AttributeMapping.Role; roleAttribute != "" {
|
||||
if val := assertionInfo.Values.Get(roleAttribute); val != "" {
|
||||
role = val
|
||||
}
|
||||
@@ -131,7 +132,12 @@ func (a *AuthN) ProviderInfo(ctx context.Context, authDomain *authtypes.AuthDoma
|
||||
}
|
||||
|
||||
func (a *AuthN) serviceProvider(siteURL *url.URL, authDomain *authtypes.AuthDomain) (*saml2.SAMLServiceProvider, error) {
|
||||
certStore, err := a.getCertificateStore(authDomain)
|
||||
samlConfig, err := authDomain.Config().SamlConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certStore, err := a.getCertificateStore(samlConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -142,32 +148,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: authDomain.AuthDomainConfig().SAML.SamlIdp,
|
||||
IdentityProviderIssuer: authDomain.AuthDomainConfig().SAML.SamlEntity,
|
||||
IdentityProviderSSOURL: samlConfig.Location,
|
||||
IdentityProviderIssuer: samlConfig.EntityID,
|
||||
ServiceProviderIssuer: siteURL.Host,
|
||||
AssertionConsumerServiceURL: acsURL.String(),
|
||||
SignAuthnRequests: !authDomain.AuthDomainConfig().SAML.InsecureSkipAuthNRequestsSigned,
|
||||
SignAuthnRequests: !samlConfig.InsecureSkipAuthNRequestsSigned,
|
||||
AllowMissingAttributes: true,
|
||||
IDPCertificateStore: certStore,
|
||||
SPKeyStore: dsig.RandomKeyStoreForTest(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *AuthN) getCertificateStore(authDomain *authtypes.AuthDomain) (dsig.X509CertificateStore, error) {
|
||||
func (a *AuthN) getCertificateStore(samlConfig authtypes.SamlConfig) (dsig.X509CertificateStore, error) {
|
||||
certStore := &dsig.MemoryX509CertificateStore{
|
||||
Roots: []*x509.Certificate{},
|
||||
}
|
||||
|
||||
var certBytes []byte
|
||||
if strings.Contains(authDomain.AuthDomainConfig().SAML.SamlCert, "-----BEGIN CERTIFICATE-----") {
|
||||
block, _ := pem.Decode([]byte(authDomain.AuthDomainConfig().SAML.SamlCert))
|
||||
if strings.Contains(samlConfig.Certificate, "-----BEGIN CERTIFICATE-----") {
|
||||
block, _ := pem.Decode([]byte(samlConfig.Certificate))
|
||||
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(authDomain.AuthDomainConfig().SAML.SamlCert)
|
||||
certData, err := base64.StdEncoding.DecodeString(samlConfig.Certificate)
|
||||
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/v1/domains`,
|
||||
url: `/api/v2/auth_domains`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListAuthDomainsQueryKey = () => {
|
||||
return [`/api/v1/domains`] as const;
|
||||
return [`/api/v2/auth_domains`] as const;
|
||||
};
|
||||
|
||||
export const getListAuthDomainsQueryOptions = <
|
||||
@@ -125,7 +125,7 @@ export const createAuthDomain = (
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateAuthDomain201>({
|
||||
url: `/api/v1/domains`,
|
||||
url: `/api/v2/auth_domains`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: authtypesPostableAuthDomainDTO,
|
||||
@@ -208,7 +208,7 @@ export const deleteAuthDomain = (
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/domains/${id}`,
|
||||
url: `/api/v2/auth_domains/${id}`,
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
});
|
||||
@@ -287,7 +287,7 @@ export const getAuthDomain = (
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetAuthDomain200>({
|
||||
url: `/api/v1/domains/${id}`,
|
||||
url: `/api/v2/auth_domains/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
@@ -296,7 +296,7 @@ export const getAuthDomain = (
|
||||
export const getGetAuthDomainQueryKey = ({
|
||||
id,
|
||||
}: GetAuthDomainPathParameters) => {
|
||||
return [`/api/v1/domains/${id}`] as const;
|
||||
return [`/api/v2/auth_domains/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetAuthDomainQueryOptions = <
|
||||
@@ -389,7 +389,7 @@ export const updateAuthDomain = (
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/domains/${id}`,
|
||||
url: `/api/v2/auth_domains/${id}`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: authtypesUpdatableAuthDomainDTO,
|
||||
|
||||
@@ -1861,8 +1861,19 @@ 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
|
||||
*/
|
||||
@@ -1870,17 +1881,21 @@ export interface AuthtypesSamlConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
samlCert?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
samlEntity?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
samlIdp?: string;
|
||||
location: string;
|
||||
}
|
||||
|
||||
export interface AuthtypesAuthDomainConfigSAMLDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum saml
|
||||
*/
|
||||
kind: AuthtypesAuthDomainConfigSAMLDTOKind;
|
||||
spec: AuthtypesSamlConfigDTO;
|
||||
}
|
||||
|
||||
export enum AuthtypesAuthDomainConfigGoogleDTOKind {
|
||||
google = 'google',
|
||||
}
|
||||
export type AuthtypesGoogleConfigDTODomainToAdminEmail = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -1893,11 +1908,12 @@ export interface AuthtypesGoogleConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
clientId?: string;
|
||||
clientId: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format password
|
||||
*/
|
||||
clientSecret?: string;
|
||||
clientSecret: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
@@ -1916,24 +1932,34 @@ export interface AuthtypesGoogleConfigDTO {
|
||||
insecureSkipEmailVerified?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
redirectURI?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format password
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@@ -1945,79 +1971,33 @@ export interface AuthtypesOIDCConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
issuer?: string;
|
||||
issuer: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
issuerAlias?: string;
|
||||
}
|
||||
|
||||
export type AuthtypesRoleMappingDTOGroupMappingsAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type AuthtypesRoleMappingDTOGroupMappings =
|
||||
AuthtypesRoleMappingDTOGroupMappingsAnyOf | null;
|
||||
|
||||
export interface AuthtypesRoleMappingDTO {
|
||||
export interface AuthtypesAuthDomainConfigOIDCDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @enum oidc
|
||||
*/
|
||||
defaultRole?: string;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
groupMappings?: AuthtypesRoleMappingDTOGroupMappings;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
useRoleAttribute?: boolean;
|
||||
kind: AuthtypesAuthDomainConfigOIDCDTOKind;
|
||||
spec: AuthtypesOIDCConfigDTO;
|
||||
}
|
||||
|
||||
export type AuthtypesAuthDomainConfigDTO =
|
||||
| AuthtypesAuthDomainConfigSAMLDTO
|
||||
| AuthtypesAuthDomainConfigGoogleDTO
|
||||
| AuthtypesAuthDomainConfigOIDCDTO;
|
||||
|
||||
export enum AuthtypesAuthNProviderDTO {
|
||||
google_auth = 'google_auth',
|
||||
google = 'google',
|
||||
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
|
||||
@@ -2055,6 +2035,31 @@ 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;
|
||||
@@ -2063,6 +2068,10 @@ export interface AuthtypesGettableAuthDomainDTO {
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -2075,6 +2084,7 @@ export interface AuthtypesGettableAuthDomainDTO {
|
||||
* @type string
|
||||
*/
|
||||
orgId?: string;
|
||||
roleMapping?: AuthtypesRoleMappingDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -2271,11 +2281,16 @@ export interface AuthtypesOrgSessionContextDTO {
|
||||
}
|
||||
|
||||
export interface AuthtypesPostableAuthDomainDTO {
|
||||
config?: AuthtypesAuthDomainConfigDTO;
|
||||
config: AuthtypesAuthDomainConfigDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
name: string;
|
||||
roleMapping?: AuthtypesRoleMappingDTO;
|
||||
}
|
||||
|
||||
export interface AuthtypesPostableEmailPasswordSessionDTO {
|
||||
@@ -2408,7 +2423,12 @@ export interface AuthtypesTransactionDTO {
|
||||
}
|
||||
|
||||
export interface AuthtypesUpdatableAuthDomainDTO {
|
||||
config?: AuthtypesAuthDomainConfigDTO;
|
||||
config: AuthtypesAuthDomainConfigDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
roleMapping?: AuthtypesRoleMappingDTO;
|
||||
}
|
||||
|
||||
export interface AuthtypesUpdatableRoleDTO {
|
||||
@@ -10525,42 +10545,6 @@ 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
|
||||
@@ -11234,6 +11218,42 @@ 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;
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,7 @@ interface AuthNProvider {
|
||||
function getAuthNProviders(samlEnabled: boolean): AuthNProvider[] {
|
||||
return [
|
||||
{
|
||||
key: AuthtypesAuthNProviderDTO.google_auth,
|
||||
key: AuthtypesAuthNProviderDTO.google,
|
||||
title: 'Google Apps Authentication',
|
||||
description: 'Let members sign-in with a Google workspace account',
|
||||
icon: <SolidGoogle size={37} />,
|
||||
@@ -78,6 +78,7 @@ function AuthnProviderSelector({
|
||||
<Button
|
||||
onClick={(): void => setAuthnProvider(provider.key)}
|
||||
type="primary"
|
||||
data-testid={`authn-provider-configure-${provider.key}`}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
import {
|
||||
AuthtypesAuthNProviderDTO,
|
||||
AuthtypesGettableAuthDomainDTO,
|
||||
AuthtypesGoogleConfigDTO,
|
||||
AuthtypesRoleMappingDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
@@ -24,10 +22,11 @@ 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';
|
||||
@@ -41,7 +40,7 @@ function configureAuthnProvider(
|
||||
switch (authnProvider) {
|
||||
case 'saml':
|
||||
return <ConfigureSAMLAuthnProvider isCreate={isCreate} />;
|
||||
case 'google_auth':
|
||||
case 'google':
|
||||
return <ConfigureGoogleAuthAuthnProvider isCreate={isCreate} />;
|
||||
case 'oidc':
|
||||
return <ConfigureOIDCAuthnProvider isCreate={isCreate} />;
|
||||
@@ -61,7 +60,7 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const [authnProvider, setAuthnProvider] = useState<
|
||||
AuthtypesAuthNProviderDTO | ''
|
||||
>(record?.config?.ssoType || '');
|
||||
>(kindToProvider(record?.config?.kind));
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const { featureFlags } = useAppContext();
|
||||
@@ -85,68 +84,6 @@ 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();
|
||||
@@ -158,25 +95,23 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = form.getFieldValue('name');
|
||||
const googleAuthConfig = getGoogleAuthConfig();
|
||||
const samlConfig = form.getFieldValue('samlConfig');
|
||||
const oidcConfig = form.getFieldValue('oidcConfig');
|
||||
const roleMapping = getRoleMapping();
|
||||
const values = form.getFieldsValue(true) as FormValues;
|
||||
const name = values.name ?? '';
|
||||
const config = prepareConfig(values, authnProvider);
|
||||
const roleMapping = prepareRoleMapping(values);
|
||||
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCreate) {
|
||||
createAuthDomain(
|
||||
{
|
||||
data: {
|
||||
name,
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: authnProvider,
|
||||
googleAuthConfig,
|
||||
samlConfig,
|
||||
oidcConfig,
|
||||
roleMapping,
|
||||
},
|
||||
enabled: true,
|
||||
config,
|
||||
roleMapping,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -196,14 +131,9 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
{
|
||||
pathParams: { id: record.id },
|
||||
data: {
|
||||
config: {
|
||||
ssoEnabled: form.getFieldValue('ssoEnabled'),
|
||||
ssoType: authnProvider,
|
||||
googleAuthConfig,
|
||||
samlConfig,
|
||||
oidcConfig,
|
||||
roleMapping,
|
||||
},
|
||||
enabled: values.enabled ?? false,
|
||||
config,
|
||||
roleMapping,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -219,8 +149,6 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
authnProvider,
|
||||
createAuthDomain,
|
||||
form,
|
||||
getGoogleAuthConfig,
|
||||
getRoleMapping,
|
||||
handleError,
|
||||
isCreate,
|
||||
|
||||
@@ -243,10 +171,10 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
>
|
||||
<Form
|
||||
name="auth-domain"
|
||||
data-testid="auth-domain-form"
|
||||
initialValues={defaultTo(prepareInitialValues(record), {
|
||||
name: '',
|
||||
ssoEnabled: false,
|
||||
ssoType: '',
|
||||
enabled: false,
|
||||
})}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
@@ -262,12 +190,22 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
{configureAuthnProvider(authnProvider, isCreate)}
|
||||
<section className="action-buttons">
|
||||
{isCreate && (
|
||||
<Button onClick={onBackHandler} variant="solid" color="secondary">
|
||||
<Button
|
||||
onClick={onBackHandler}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
testId="auth-domain-back"
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
{!isCreate && (
|
||||
<Button onClick={onClose} variant="solid" color="secondary">
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
testId="auth-domain-cancel"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
@@ -276,6 +214,7 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
variant="solid"
|
||||
color="primary"
|
||||
loading={isCreating || isUpdating}
|
||||
testId="auth-domain-save"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { AuthtypesAuthNProviderDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
AuthtypesAuthDomainConfigGoogleDTOKind,
|
||||
AuthtypesAuthDomainConfigOIDCDTOKind,
|
||||
AuthtypesAuthDomainConfigSAMLDTOKind,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import {
|
||||
convertDomainMappingsToList,
|
||||
@@ -82,8 +86,7 @@ describe('prepareInitialValues', () => {
|
||||
it('returns empty defaults when no record is provided', () => {
|
||||
expect(prepareInitialValues(undefined)).toStrictEqual({
|
||||
name: '',
|
||||
ssoEnabled: false,
|
||||
ssoType: '',
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,15 +94,20 @@ describe('prepareInitialValues', () => {
|
||||
const result = prepareInitialValues({
|
||||
id: 'domain-1',
|
||||
name: 'example.com',
|
||||
enabled: true,
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.saml,
|
||||
roleMapping: {
|
||||
defaultRole: 'VIEWER',
|
||||
useRoleAttribute: false,
|
||||
groupMappings: { admins: 'ADMIN', viewers: 'VIEWER' },
|
||||
kind: AuthtypesAuthDomainConfigSAMLDTOKind.saml,
|
||||
spec: {
|
||||
location: 'https://idp.example.com/sso',
|
||||
entityId: 'urn:example:idp',
|
||||
certificate: 'CERT',
|
||||
},
|
||||
},
|
||||
roleMapping: {
|
||||
defaultRole: 'VIEWER',
|
||||
useRoleAttribute: false,
|
||||
groupMappings: { admins: 'ADMIN', viewers: 'VIEWER' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.roleMapping?.groupMappingsList).toStrictEqual([
|
||||
@@ -112,10 +120,10 @@ describe('prepareInitialValues', () => {
|
||||
const result = prepareInitialValues({
|
||||
id: 'domain-1',
|
||||
name: 'example.com',
|
||||
enabled: true,
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.google_auth,
|
||||
googleAuthConfig: {
|
||||
kind: AuthtypesAuthDomainConfigGoogleDTOKind.google,
|
||||
spec: {
|
||||
clientId: 'id',
|
||||
clientSecret: 'secret',
|
||||
domainToAdminEmail: { 'example.com': 'admin@example.com' },
|
||||
@@ -132,11 +140,16 @@ describe('prepareInitialValues', () => {
|
||||
const result = prepareInitialValues({
|
||||
id: 'domain-1',
|
||||
name: 'example.com',
|
||||
enabled: true,
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.oidc,
|
||||
roleMapping: { defaultRole: 'VIEWER', useRoleAttribute: true },
|
||||
kind: AuthtypesAuthDomainConfigOIDCDTOKind.oidc,
|
||||
spec: {
|
||||
issuer: 'https://oidc.example.com',
|
||||
clientId: 'id',
|
||||
clientSecret: 'secret',
|
||||
},
|
||||
},
|
||||
roleMapping: { defaultRole: 'VIEWER', useRoleAttribute: true },
|
||||
});
|
||||
|
||||
expect(result.roleMapping?.groupMappingsList).toStrictEqual([]);
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import {
|
||||
AuthtypesAuthDomainConfigDTO,
|
||||
AuthtypesAuthDomainConfigGoogleDTOKind,
|
||||
AuthtypesAuthDomainConfigOIDCDTOKind,
|
||||
AuthtypesAuthDomainConfigSAMLDTOKind,
|
||||
AuthtypesAuthNProviderDTO,
|
||||
AuthtypesGettableAuthDomainDTO,
|
||||
AuthtypesGoogleConfigDTO,
|
||||
AuthtypesOIDCConfigDTO,
|
||||
@@ -6,11 +11,29 @@ 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;
|
||||
ssoEnabled?: boolean;
|
||||
ssoType?: string;
|
||||
enabled?: boolean;
|
||||
googleAuthConfig?: AuthtypesGoogleConfigDTO & {
|
||||
domainToAdminEmailList?: Array<{ domain?: string; adminEmail?: string }>;
|
||||
};
|
||||
@@ -107,33 +130,141 @@ export function prepareInitialValues(
|
||||
if (!record) {
|
||||
return {
|
||||
name: '',
|
||||
ssoEnabled: false,
|
||||
ssoType: '',
|
||||
enabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
const config = record.config ?? {};
|
||||
const { config } = record;
|
||||
return {
|
||||
name: record.name,
|
||||
ssoEnabled: config.ssoEnabled,
|
||||
ssoType: config.ssoType,
|
||||
samlConfig: config.samlConfig ?? undefined,
|
||||
oidcConfig: config.oidcConfig ?? undefined,
|
||||
googleAuthConfig: config.googleAuthConfig
|
||||
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
|
||||
? {
|
||||
...config.googleAuthConfig,
|
||||
domainToAdminEmailList: convertDomainMappingsToList(
|
||||
config.googleAuthConfig.domainToAdminEmail,
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
roleMapping: config.roleMapping
|
||||
? {
|
||||
...config.roleMapping,
|
||||
...record.roleMapping,
|
||||
groupMappingsList: convertGroupMappingsToList(
|
||||
config.roleMapping.groupMappings,
|
||||
record.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,7 +91,11 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
{ required: true, message: 'Domain is required', whitespace: true },
|
||||
]}
|
||||
>
|
||||
<Input id="google-domain" disabled={!isCreate} />
|
||||
<Input
|
||||
id="google-domain"
|
||||
disabled={!isCreate}
|
||||
testId="google-auth-domain"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
@@ -109,7 +113,7 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
{ required: true, message: 'Client ID is required', whitespace: true },
|
||||
]}
|
||||
>
|
||||
<Input id="google-client-id" />
|
||||
<Input id="google-client-id" testId="google-auth-client-id" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
@@ -131,7 +135,7 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input id="google-client-secret" />
|
||||
<Input id="google-client-secret" testId="google-auth-client-secret" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
@@ -143,6 +147,7 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
>
|
||||
<Checkbox
|
||||
id="google-skip-email-verification"
|
||||
testId="google-auth-skip-email-verified"
|
||||
onChange={(checked: boolean): void => {
|
||||
form.setFieldValue(
|
||||
['googleAuthConfig', 'insecureSkipEmailVerified'],
|
||||
@@ -180,7 +185,10 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
<Collapse.Panel
|
||||
key="workspace-groups"
|
||||
header={
|
||||
<div className="authn-provider__collapse-header">
|
||||
<div
|
||||
className="authn-provider__collapse-header"
|
||||
data-testid="google-auth-workspace-groups-header"
|
||||
>
|
||||
{expandedSection !== 'workspace-groups' ? (
|
||||
<ChevronRight size={16} />
|
||||
) : (
|
||||
@@ -221,6 +229,7 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
>
|
||||
<Checkbox
|
||||
id="google-fetch-groups"
|
||||
testId="google-auth-fetch-groups"
|
||||
onChange={(checked: boolean): void => {
|
||||
form.setFieldValue(['googleAuthConfig', 'fetchGroups'], checked);
|
||||
}}
|
||||
@@ -251,6 +260,7 @@ 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"
|
||||
@@ -270,6 +280,7 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
>
|
||||
<Checkbox
|
||||
id="google-transitive-membership"
|
||||
testId="google-auth-transitive-membership"
|
||||
onChange={(checked: boolean): void => {
|
||||
form.setFieldValue(
|
||||
['googleAuthConfig', 'fetchTransitiveGroupMembership'],
|
||||
@@ -299,7 +310,10 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
name={['googleAuthConfig', 'allowedGroups']}
|
||||
className="authn-provider__form-item"
|
||||
>
|
||||
<EmailTagInput placeholder="Type a group email and press Enter" />
|
||||
<EmailTagInput
|
||||
placeholder="Type a group email and press Enter"
|
||||
testId="google-auth-allowed-groups"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,7 @@ function ConfigureSAMLAuthnProvider({
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['samlConfig', 'samlIdp']}
|
||||
name={['samlConfig', 'location']}
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{
|
||||
@@ -98,7 +98,7 @@ function ConfigureSAMLAuthnProvider({
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['samlConfig', 'samlEntity']}
|
||||
name={['samlConfig', 'entityId']}
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{
|
||||
@@ -120,7 +120,7 @@ function ConfigureSAMLAuthnProvider({
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['samlConfig', 'samlCert']}
|
||||
name={['samlConfig', 'certificate']}
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{
|
||||
|
||||
@@ -9,12 +9,14 @@ 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('');
|
||||
|
||||
@@ -34,7 +36,7 @@ function EmailTagInput({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="email-tag-input">
|
||||
<div className="email-tag-input" data-testid={testId}>
|
||||
<Tooltip
|
||||
title={validationError}
|
||||
open={!!validationError}
|
||||
|
||||
@@ -74,6 +74,7 @@ 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">
|
||||
@@ -138,6 +139,7 @@ function RoleMappingSection({
|
||||
>
|
||||
<Checkbox
|
||||
id="use-role-attribute"
|
||||
testId="role-mapping-use-role-attribute"
|
||||
onChange={(checked: boolean): void => {
|
||||
form.setFieldValue([...fieldNamePrefix, 'useRoleAttribute'], checked);
|
||||
}}
|
||||
@@ -166,13 +168,20 @@ 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">
|
||||
<div
|
||||
key={field.key}
|
||||
className="role-mapping-section__row"
|
||||
data-testid="role-mapping-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" />
|
||||
<Input
|
||||
placeholder="IDP Group Name"
|
||||
testId="role-mapping-group-name"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
@@ -199,6 +208,7 @@ 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>
|
||||
@@ -212,6 +222,7 @@ 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) {
|
||||
if (!record.id || !record.config) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -41,14 +41,9 @@ function SSOEnforcementToggle({
|
||||
{
|
||||
pathParams: { id: record.id },
|
||||
data: {
|
||||
config: {
|
||||
ssoEnabled: checked,
|
||||
ssoType: record.config?.ssoType,
|
||||
googleAuthConfig: record.config?.googleAuthConfig,
|
||||
oidcConfig: record.config?.oidcConfig,
|
||||
samlConfig: record.config?.samlConfig,
|
||||
roleMapping: record.config?.roleMapping,
|
||||
},
|
||||
enabled: checked,
|
||||
config: record.config,
|
||||
roleMapping: record.roleMapping,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -65,7 +60,12 @@ function SSOEnforcementToggle({
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch disabled={isLoading} value={isChecked} onChange={onChangeHandler} />
|
||||
<Switch
|
||||
disabled={isLoading}
|
||||
value={isChecked}
|
||||
onChange={onChangeHandler}
|
||||
testId="auth-domain-enforce-sso"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('AuthDomain', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('reflects ssoEnabled state from nested config in each row toggle', async () => {
|
||||
it('reflects the enabled state 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 → config.ssoEnabled: true
|
||||
// [1] example.com → config.ssoEnabled: false
|
||||
// [2] corp.io → config.ssoEnabled: true
|
||||
// [0] signoz.io → enabled: true
|
||||
// [1] example.com → enabled: false
|
||||
// [2] corp.io → enabled: true
|
||||
const switches = await screen.findAllByRole('switch');
|
||||
expect(switches).toHaveLength(3);
|
||||
expect(switches[0]).toBeChecked();
|
||||
|
||||
@@ -112,9 +112,7 @@ describe('CreateEdit — save payload correctness', () => {
|
||||
await waitFor(() => expect(capturedPayload).not.toBeNull());
|
||||
|
||||
expect(capturedPayload).toMatchObject({
|
||||
config: expect.objectContaining({
|
||||
roleMapping: expect.objectContaining({ groupMappings: {} }),
|
||||
}),
|
||||
roleMapping: expect.objectContaining({ groupMappings: {} }),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -161,7 +159,7 @@ describe('CreateEdit — save payload correctness', () => {
|
||||
|
||||
expect(capturedPayload).toMatchObject({
|
||||
config: expect.objectContaining({
|
||||
googleAuthConfig: expect.objectContaining({
|
||||
spec: 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().config.roleMapping.defaultRole).toBe(editorRole.name);
|
||||
expect(payload.get().config.roleMapping.defaultRole).not.toBe(editorRole.id);
|
||||
expect(payload.get().roleMapping.defaultRole).toBe(editorRole.name);
|
||||
expect(payload.get().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().config.roleMapping.defaultRole).toBe(viewerRole.name);
|
||||
expect(payload.get().config.roleMapping.defaultRole).not.toBe(viewerRole.id);
|
||||
expect(payload.get().roleMapping.defaultRole).toBe(viewerRole.name);
|
||||
expect(payload.get().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().config.roleMapping.defaultRole).toBe(viewerRole.name);
|
||||
expect(payload.get().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().config.roleMapping.defaultRole).toBe(editorRole.name);
|
||||
expect(payload.get().config.roleMapping.groupMappings).toStrictEqual({
|
||||
expect(payload.get().roleMapping.defaultRole).toBe(editorRole.name);
|
||||
expect(payload.get().roleMapping.groupMappings).toStrictEqual({
|
||||
'admin-group': 'signoz-admin',
|
||||
'dev-team': 'signoz-editor',
|
||||
viewers: 'signoz-viewer',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { AuthtypesGettableAuthDomainDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
AuthtypesAuthDomainConfigGoogleDTO,
|
||||
AuthtypesGettableAuthDomainDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import CreateEdit from '../CreateEdit/CreateEdit';
|
||||
import {
|
||||
@@ -48,11 +51,10 @@ jest.mock('@signozhq/ui/button', () => ({
|
||||
|
||||
type SavedPayload = {
|
||||
config: {
|
||||
googleAuthConfig?: Record<string, unknown>;
|
||||
samlConfig?: Record<string, unknown>;
|
||||
oidcConfig?: Record<string, unknown>;
|
||||
roleMapping?: Record<string, unknown>;
|
||||
kind?: string;
|
||||
spec?: Record<string, unknown>;
|
||||
};
|
||||
roleMapping?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
async function submitForm(
|
||||
@@ -81,7 +83,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.googleAuthConfig;
|
||||
const g = payload.config.spec;
|
||||
expect(g?.clientId).toBe('test-client-id');
|
||||
expect(g?.clientSecret).toBe('test-client-secret');
|
||||
expect(g?.allowedGroups).toBeUndefined();
|
||||
@@ -91,18 +93,20 @@ 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: {
|
||||
...mockGoogleAuthWithWorkspaceGroups.config,
|
||||
googleAuthConfig: {
|
||||
...mockGoogleAuthWithWorkspaceGroups.config?.googleAuthConfig,
|
||||
...googleConfig,
|
||||
spec: {
|
||||
...googleConfig.spec,
|
||||
fetchGroups: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const g = payload.config.googleAuthConfig;
|
||||
const g = payload.config.spec;
|
||||
expect(g?.fetchGroups).toBe(false);
|
||||
expect(g?.allowedGroups).toBeUndefined();
|
||||
expect(g?.serviceAccountJson).toBeUndefined();
|
||||
@@ -113,7 +117,7 @@ describe('CreateEdit — payload sanitization', () => {
|
||||
it('includes all workspace fields when fetchGroups is true', async () => {
|
||||
const payload = await submitForm(mockGoogleAuthWithWorkspaceGroups);
|
||||
|
||||
const g = payload.config.googleAuthConfig;
|
||||
const g = payload.config.spec;
|
||||
expect(g?.fetchGroups).toBe(true);
|
||||
expect(g?.serviceAccountJson).toBe('{"type": "service_account"}');
|
||||
expect(g?.fetchTransitiveGroupMembership).toBe(true);
|
||||
@@ -131,10 +135,10 @@ describe('CreateEdit — payload sanitization', () => {
|
||||
it('sends core and attributeMapping fields', async () => {
|
||||
const payload = await submitForm(mockSamlWithAttributeMapping);
|
||||
|
||||
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');
|
||||
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');
|
||||
expect(s?.insecureSkipAuthNRequestsSigned).toBe(true);
|
||||
|
||||
const attr = s?.attributeMapping as Record<string, unknown>;
|
||||
@@ -148,7 +152,7 @@ describe('CreateEdit — payload sanitization', () => {
|
||||
it('sends all fields including claimMapping', async () => {
|
||||
const payload = await submitForm(mockOidcWithClaimMapping);
|
||||
|
||||
const o = payload.config.oidcConfig;
|
||||
const o = payload.config.spec;
|
||||
expect(o?.issuer).toBe('https://oidc.claims.com');
|
||||
expect(o?.issuerAlias).toBe('https://alias.claims.com');
|
||||
expect(o?.clientId).toBe('claims-client-id');
|
||||
@@ -168,24 +172,21 @@ describe('CreateEdit — payload sanitization', () => {
|
||||
it('strips groupMappings when useRoleAttribute is true', async () => {
|
||||
const payload = await submitForm({
|
||||
...mockDomainWithRoleMapping,
|
||||
config: {
|
||||
...mockDomainWithRoleMapping.config,
|
||||
roleMapping: {
|
||||
...mockDomainWithRoleMapping.config?.roleMapping,
|
||||
useRoleAttribute: true,
|
||||
},
|
||||
roleMapping: {
|
||||
...mockDomainWithRoleMapping.roleMapping,
|
||||
useRoleAttribute: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload.config.roleMapping?.useRoleAttribute).toBe(true);
|
||||
expect(payload.config.roleMapping?.groupMappings).toBeUndefined();
|
||||
expect(payload.roleMapping?.useRoleAttribute).toBe(true);
|
||||
expect(payload.roleMapping?.groupMappings).toBeUndefined();
|
||||
});
|
||||
|
||||
it('sends groupMappings when useRoleAttribute is false', async () => {
|
||||
const payload = await submitForm(mockDomainWithRoleMapping);
|
||||
|
||||
expect(payload.config.roleMapping?.useRoleAttribute).toBe(false);
|
||||
expect(payload.config.roleMapping?.groupMappings).toStrictEqual({
|
||||
expect(payload.roleMapping?.useRoleAttribute).toBe(false);
|
||||
expect(payload.roleMapping?.groupMappings).toStrictEqual({
|
||||
'admin-group': 'signoz-admin',
|
||||
'dev-team': 'signoz-editor',
|
||||
viewers: 'signoz-viewer',
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('SSOEnforcementToggle', () => {
|
||||
isDefaultChecked={false}
|
||||
record={{
|
||||
...mockGoogleAuthDomain,
|
||||
config: { ...mockGoogleAuthDomain.config, ssoEnabled: false },
|
||||
enabled: false,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
@@ -95,9 +95,8 @@ describe('SSOEnforcementToggle', () => {
|
||||
expect(mockUpdateAPI).toHaveBeenCalledTimes(1);
|
||||
expect(mockUpdateAPI).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
ssoEnabled: false,
|
||||
}),
|
||||
enabled: false,
|
||||
config: mockGoogleAuthDomain.config,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import {
|
||||
AuthtypesAuthNProviderDTO,
|
||||
AuthtypesAuthDomainConfigGoogleDTOKind,
|
||||
AuthtypesAuthDomainConfigOIDCDTOKind,
|
||||
AuthtypesAuthDomainConfigSAMLDTOKind,
|
||||
AuthtypesGettableAuthDomainDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
// API Endpoints
|
||||
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';
|
||||
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';
|
||||
|
||||
// Mock Auth Domain with Google Auth
|
||||
export const mockGoogleAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-1',
|
||||
name: 'signoz.io',
|
||||
enabled: true,
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.google_auth,
|
||||
googleAuthConfig: {
|
||||
kind: AuthtypesAuthDomainConfigGoogleDTOKind.google,
|
||||
spec: {
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
},
|
||||
@@ -30,13 +32,13 @@ export const mockGoogleAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
export const mockSamlAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-2',
|
||||
name: 'example.com',
|
||||
enabled: false,
|
||||
config: {
|
||||
ssoEnabled: false,
|
||||
ssoType: AuthtypesAuthNProviderDTO.saml,
|
||||
samlConfig: {
|
||||
samlIdp: 'https://idp.example.com/sso',
|
||||
samlEntity: 'urn:example:idp',
|
||||
samlCert: 'MOCK_CERTIFICATE',
|
||||
kind: AuthtypesAuthDomainConfigSAMLDTOKind.saml,
|
||||
spec: {
|
||||
location: 'https://idp.example.com/sso',
|
||||
entityId: 'urn:example:idp',
|
||||
certificate: 'MOCK_CERTIFICATE',
|
||||
},
|
||||
},
|
||||
authNProviderInfo: {
|
||||
@@ -48,10 +50,10 @@ export const mockSamlAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
export const mockOidcAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-3',
|
||||
name: 'corp.io',
|
||||
enabled: true,
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.oidc,
|
||||
oidcConfig: {
|
||||
kind: AuthtypesAuthDomainConfigOIDCDTOKind.oidc,
|
||||
spec: {
|
||||
issuer: 'https://oidc.corp.io',
|
||||
clientId: 'oidc-client-id',
|
||||
clientSecret: 'oidc-client-secret',
|
||||
@@ -66,22 +68,22 @@ export const mockOidcAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
export const mockDomainWithRoleMapping: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-4',
|
||||
name: 'enterprise.com',
|
||||
enabled: true,
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.saml,
|
||||
samlConfig: {
|
||||
samlIdp: 'https://idp.enterprise.com/sso',
|
||||
samlEntity: 'urn:enterprise:idp',
|
||||
samlCert: 'MOCK_CERTIFICATE',
|
||||
kind: AuthtypesAuthDomainConfigSAMLDTOKind.saml,
|
||||
spec: {
|
||||
location: 'https://idp.enterprise.com/sso',
|
||||
entityId: 'urn:enterprise:idp',
|
||||
certificate: '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: {
|
||||
@@ -94,18 +96,18 @@ export const mockDomainWithDirectRoleAttribute: AuthtypesGettableAuthDomainDTO =
|
||||
{
|
||||
id: 'domain-5',
|
||||
name: 'direct-role.com',
|
||||
enabled: true,
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.oidc,
|
||||
oidcConfig: {
|
||||
kind: AuthtypesAuthDomainConfigOIDCDTOKind.oidc,
|
||||
spec: {
|
||||
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',
|
||||
@@ -116,10 +118,10 @@ export const mockDomainWithDirectRoleAttribute: AuthtypesGettableAuthDomainDTO =
|
||||
export const mockOidcWithClaimMapping: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-6',
|
||||
name: 'oidc-claims.com',
|
||||
enabled: true,
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.oidc,
|
||||
oidcConfig: {
|
||||
kind: AuthtypesAuthDomainConfigOIDCDTOKind.oidc,
|
||||
spec: {
|
||||
issuer: 'https://oidc.claims.com',
|
||||
issuerAlias: 'https://alias.claims.com',
|
||||
clientId: 'claims-client-id',
|
||||
@@ -143,13 +145,13 @@ export const mockOidcWithClaimMapping: AuthtypesGettableAuthDomainDTO = {
|
||||
export const mockSamlWithAttributeMapping: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-7',
|
||||
name: 'saml-attrs.com',
|
||||
enabled: true,
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.saml,
|
||||
samlConfig: {
|
||||
samlIdp: 'https://idp.saml-attrs.com/sso',
|
||||
samlEntity: 'urn:saml-attrs:idp',
|
||||
samlCert: 'MOCK_CERTIFICATE_ATTRS',
|
||||
kind: AuthtypesAuthDomainConfigSAMLDTOKind.saml,
|
||||
spec: {
|
||||
location: 'https://idp.saml-attrs.com/sso',
|
||||
entityId: 'urn:saml-attrs:idp',
|
||||
certificate: 'MOCK_CERTIFICATE_ATTRS',
|
||||
insecureSkipAuthNRequestsSigned: true,
|
||||
attributeMapping: {
|
||||
name: 'user_display_name',
|
||||
@@ -168,10 +170,10 @@ export const mockGoogleAuthWithWorkspaceGroups: AuthtypesGettableAuthDomainDTO =
|
||||
{
|
||||
id: 'domain-8',
|
||||
name: 'google-groups.com',
|
||||
enabled: true,
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: AuthtypesAuthNProviderDTO.google_auth,
|
||||
googleAuthConfig: {
|
||||
kind: AuthtypesAuthDomainConfigGoogleDTOKind.google,
|
||||
spec: {
|
||||
clientId: 'google-groups-client-id',
|
||||
clientSecret: 'google-groups-client-secret',
|
||||
insecureSkipEmailVerified: false,
|
||||
@@ -218,7 +220,7 @@ export const mockUpdateSuccessResponse = {
|
||||
status: 'success',
|
||||
data: {
|
||||
...mockGoogleAuthDomain,
|
||||
config: { ...mockGoogleAuthDomain.config, ssoEnabled: false },
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { HTMLAttributes, 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_auth', 'Google Auth'],
|
||||
['google', 'Google Auth'],
|
||||
['saml', 'SAML'],
|
||||
['email_password', 'Email Password'],
|
||||
['oidc', 'OIDC'],
|
||||
@@ -121,8 +121,8 @@ function AuthDomain(): JSX.Element {
|
||||
},
|
||||
{
|
||||
title: 'Enforce SSO',
|
||||
dataIndex: ['config', 'ssoEnabled'],
|
||||
key: 'ssoEnabled',
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
width: 80,
|
||||
render: (
|
||||
value: boolean,
|
||||
@@ -157,13 +157,15 @@ 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?.ssoType || '')}
|
||||
Configure {SSOType.get(record.config?.kind || '')}
|
||||
</Button>
|
||||
<Button
|
||||
className="auth-domain-list-action-link delete"
|
||||
onClick={(): void => showDeleteModal(record)}
|
||||
variant="link"
|
||||
testId="auth-domain-delete"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
@@ -177,7 +179,9 @@ function AuthDomain(): JSX.Element {
|
||||
return (
|
||||
<div className="auth-domain">
|
||||
<section className="auth-domain-header">
|
||||
<h3 className="auth-domain-title">Authenticated Domains</h3>
|
||||
<h3 className="auth-domain-title" data-testid="auth-domain-title">
|
||||
Authenticated Domains
|
||||
</h3>
|
||||
<Button
|
||||
prefix={<Plus size="md" />}
|
||||
onClick={(): void => {
|
||||
@@ -186,6 +190,7 @@ function AuthDomain(): JSX.Element {
|
||||
variant="solid"
|
||||
size="sm"
|
||||
color="primary"
|
||||
testId="auth-domain-add"
|
||||
>
|
||||
Add Domain
|
||||
</Button>
|
||||
@@ -195,7 +200,14 @@ function AuthDomain(): JSX.Element {
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={authDomainListResponse?.data}
|
||||
onRow={undefined}
|
||||
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>
|
||||
}
|
||||
loading={
|
||||
isLoadingAuthDomainListResponse || isFetchingAuthDomainListResponse
|
||||
}
|
||||
@@ -228,6 +240,7 @@ function AuthDomain(): JSX.Element {
|
||||
onClick={hideDeleteModal}
|
||||
className="cancel-btn"
|
||||
prefix={<X size={16} />}
|
||||
testId="auth-domain-delete-cancel"
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
@@ -237,6 +250,7 @@ function AuthDomain(): JSX.Element {
|
||||
onClick={handleDeleteDomain}
|
||||
className="delete-btn"
|
||||
loading={isLoading}
|
||||
testId="auth-domain-delete-confirm"
|
||||
>
|
||||
Delete Domain
|
||||
</Button>,
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func (provider *provider) addAuthDomainRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v1/domains", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.List), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/auth_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/v1/domains", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Create), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/auth_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/v1/domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Get), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/auth_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/v1/domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Update), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/auth_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/v1/domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Delete), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/auth_domains/{id}", handler.New(provider.authzMiddleware.AdminAccess(provider.authDomainHandler.Delete), handler.OpenAPIDef{
|
||||
ID: "DeleteAuthDomain",
|
||||
Tags: []string{"authdomains"},
|
||||
Summary: "Delete auth domain",
|
||||
|
||||
@@ -59,12 +59,11 @@ func (a *AuthN) LoginURL(ctx context.Context, siteURL *url.URL, authDomain *auth
|
||||
return "", err
|
||||
}
|
||||
|
||||
if authDomain.AuthDomainConfig().AuthNProvider != authtypes.AuthNProviderGoogleAuth {
|
||||
return "", errors.Newf(errors.TypeInternal, authtypes.ErrCodeAuthDomainMismatch, "domain type is not google")
|
||||
oauth2Config, err := a.oauth2Config(siteURL, authDomain, oidcProvider)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
oauth2Config := a.oauth2Config(siteURL, authDomain, oidcProvider)
|
||||
|
||||
return oauth2Config.AuthCodeURL(
|
||||
authtypes.NewState(siteURL, authDomain.StorableAuthDomain().ID).URL.String(),
|
||||
oauth2.SetAuthURLParam("hd", authDomain.StorableAuthDomain().Name),
|
||||
@@ -93,7 +92,16 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oauth2Config := a.oauth2Config(state.URL, authDomain, oidcProvider)
|
||||
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
|
||||
}
|
||||
|
||||
token, err := oauth2Config.Exchange(ctx, query.Get("code"))
|
||||
if err != nil {
|
||||
var retrieveError *oauth2.RetrieveError
|
||||
@@ -111,7 +119,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: authDomain.AuthDomainConfig().Google.ClientID})
|
||||
verifier := oidcProvider.Verifier(&oidc.Config{ClientID: googleConfig.ClientID})
|
||||
idToken, err := verifier.Verify(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
a.settings.Logger().ErrorContext(ctx, "google: failed to verify token", errors.Attr(err))
|
||||
@@ -135,7 +143,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 !authDomain.AuthDomainConfig().Google.InsecureSkipEmailVerified {
|
||||
if !googleConfig.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")
|
||||
@@ -148,14 +156,14 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
|
||||
}
|
||||
|
||||
var groups []string
|
||||
if authDomain.AuthDomainConfig().Google.FetchGroups {
|
||||
groups, err = a.fetchGoogleWorkspaceGroups(ctx, claims.Email, authDomain.AuthDomainConfig().Google)
|
||||
if googleConfig.FetchGroups {
|
||||
groups, err = a.fetchGoogleWorkspaceGroups(ctx, claims.Email, googleConfig)
|
||||
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 := authDomain.AuthDomainConfig().Google.AllowedGroups
|
||||
allowedGroups := googleConfig.AllowedGroups
|
||||
if len(allowedGroups) > 0 {
|
||||
groups = filterGroups(groups, allowedGroups)
|
||||
if len(groups) == 0 {
|
||||
@@ -173,10 +181,15 @@ 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 {
|
||||
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
|
||||
}
|
||||
|
||||
return &oauth2.Config{
|
||||
ClientID: authDomain.AuthDomainConfig().Google.ClientID,
|
||||
ClientSecret: authDomain.AuthDomainConfig().Google.ClientSecret,
|
||||
ClientID: googleConfig.ClientID,
|
||||
ClientSecret: googleConfig.ClientSecret,
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: scopes,
|
||||
RedirectURL: (&url.URL{
|
||||
@@ -184,10 +197,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.AuthDomainConfig().RoleMapping.RoleNames() {
|
||||
for _, mappedRole := range domain.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.NewAuthDomainFromConfig(body.Name, &body.Config, valuer.MustNewUUID(claims.OrgID))
|
||||
authDomain, err := authtypes.NewAuthDomainFromPostableAuthDomain(body, 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.Config)
|
||||
err = authDomain.Update(body)
|
||||
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.AuthDomainConfig().AuthNProvider].(authn.CallbackAuthN); ok {
|
||||
if callbackAuthN, ok := module.authNs[domain.Kind()].(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.AuthDomainConfig().AuthNProvider.StringValue() + ".count"
|
||||
key := "authdomain." + domain.Kind().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.AuthDomainConfig().RoleMapping.RoleNames()
|
||||
roleNames := domain.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.AuthNProviderGoogleAuth, values)
|
||||
redirectURL, err := handler.module.CreateCallbackAuthNSession(ctx, authtypes.AuthNProviderGoogle, 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.AuthDomainConfig().RoleMapping
|
||||
roleMapping := authDomain.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.AuthDomainConfig().SSOEnabled {
|
||||
if !authDomain.Enabled() {
|
||||
return authtypes.NewOrgSessionContext(org.ID, org.Name).AddPasswordAuthNSupport(authtypes.AuthNProviderEmailPassword), nil
|
||||
}
|
||||
|
||||
provider, err := getProvider[authn.CallbackAuthN](authDomain.AuthDomainConfig().AuthNProvider, module.authNs)
|
||||
provider, err := getProvider[authn.CallbackAuthN](authDomain.Kind(), 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.AuthDomainConfig().AuthNProvider, loginURL), nil
|
||||
return authtypes.NewOrgSessionContext(org.ID, org.Name).AddCallbackAuthNSupport(authDomain.Kind(), loginURL), nil
|
||||
}
|
||||
|
||||
func getProvider[T authn.AuthN](authNProvider authtypes.AuthNProvider, authNs map[authtypes.AuthNProvider]authn.AuthN) (T, error) {
|
||||
|
||||
@@ -22,6 +22,6 @@ func NewAuthNs(ctx context.Context, providerSettings factory.ProviderSettings, s
|
||||
|
||||
return map[authtypes.AuthNProvider]authn.AuthN{
|
||||
authtypes.AuthNProviderEmailPassword: emailPasswordAuthN,
|
||||
authtypes.AuthNProviderGoogleAuth: googleCallbackAuthN,
|
||||
authtypes.AuthNProviderGoogle: googleCallbackAuthN,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -237,6 +237,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddDashboardTuplesFactory(sqlstore),
|
||||
sqlmigration.NewRestructureSavedViewSpecFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddSavedViewTuplesFactory(sqlstore),
|
||||
sqlmigration.NewRestructureAuthDomainConfigFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
182
pkg/sqlmigration/111_restructure_auth_domain_config.go
Normal file
182
pkg/sqlmigration/111_restructure_auth_domain_config.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type restructureAuthDomainConfig struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
type restructureAuthDomainRow struct {
|
||||
bun.BaseModel `bun:"table:auth_domain"`
|
||||
|
||||
ID string `bun:"id"`
|
||||
Data string `bun:"data"`
|
||||
}
|
||||
|
||||
// The legacy document keyed the discriminator as ssoType with the chosen
|
||||
// provider's config in a sibling field; the restructured document is
|
||||
// {enabled, config: {kind, spec}, roleMapping} with renamed saml spec keys.
|
||||
var legacySSOTypeToKind = map[string]string{
|
||||
"google_auth": "google",
|
||||
"saml": "saml",
|
||||
"oidc": "oidc",
|
||||
}
|
||||
|
||||
var legacySSOTypeToConfigKey = map[string]string{
|
||||
"google_auth": "googleAuthConfig",
|
||||
"saml": "samlConfig",
|
||||
"oidc": "oidcConfig",
|
||||
}
|
||||
|
||||
var legacySamlKeyToKey = map[string]string{
|
||||
"samlEntity": "entityId",
|
||||
"samlIdp": "location",
|
||||
"samlCert": "certificate",
|
||||
}
|
||||
|
||||
func NewRestructureAuthDomainConfigFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("restructure_auth_domain_config"),
|
||||
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &restructureAuthDomainConfig{sqlstore: sqlstore, logger: ps.Logger}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (migration *restructureAuthDomainConfig) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *restructureAuthDomainConfig) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
rows := make([]*restructureAuthDomainRow, 0)
|
||||
if err := tx.NewSelect().Model(&rows).Scan(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
legacy := make(map[string]json.RawMessage)
|
||||
if err := json.Unmarshal([]byte(row.Data), &legacy); err != nil {
|
||||
migration.logger.WarnContext(ctx, "skipping auth domain with unreadable data", slog.String("auth_domain_id", row.ID), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
|
||||
ssoTypeRaw, ok := legacy["ssoType"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var ssoType string
|
||||
if err := json.Unmarshal(ssoTypeRaw, &ssoType); err != nil {
|
||||
migration.logger.WarnContext(ctx, "skipping auth domain with unreadable ssoType", slog.String("auth_domain_id", row.ID), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
|
||||
kind, ok := legacySSOTypeToKind[ssoType]
|
||||
if !ok {
|
||||
migration.logger.WarnContext(ctx, "skipping auth domain with unknown ssoType", slog.String("auth_domain_id", row.ID), slog.String("sso_type", ssoType))
|
||||
continue
|
||||
}
|
||||
|
||||
spec, ok := legacy[legacySSOTypeToConfigKey[ssoType]]
|
||||
if !ok || string(spec) == "null" {
|
||||
migration.logger.WarnContext(ctx, "skipping auth domain with missing provider config", slog.String("auth_domain_id", row.ID), slog.String("sso_type", ssoType))
|
||||
continue
|
||||
}
|
||||
|
||||
if ssoType == "saml" {
|
||||
samlSpec := make(map[string]json.RawMessage)
|
||||
if err := json.Unmarshal(spec, &samlSpec); err != nil {
|
||||
migration.logger.WarnContext(ctx, "skipping auth domain with unreadable saml config", slog.String("auth_domain_id", row.ID), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
|
||||
for legacyKey, key := range legacySamlKeyToKey {
|
||||
if value, ok := samlSpec[legacyKey]; ok {
|
||||
samlSpec[key] = value
|
||||
delete(samlSpec, legacyKey)
|
||||
}
|
||||
}
|
||||
|
||||
if spec, err = json.Marshal(samlSpec); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if ssoType == "google_auth" {
|
||||
googleSpec := make(map[string]json.RawMessage)
|
||||
if err := json.Unmarshal(spec, &googleSpec); err != nil {
|
||||
migration.logger.WarnContext(ctx, "skipping auth domain with unreadable google config", slog.String("auth_domain_id", row.ID), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
|
||||
delete(googleSpec, "redirectURI")
|
||||
|
||||
if spec, err = json.Marshal(googleSpec); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
kindRaw, err := json.Marshal(kind)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := json.Marshal(map[string]json.RawMessage{
|
||||
"kind": kindRaw,
|
||||
"spec": spec,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
restructured := map[string]json.RawMessage{
|
||||
"enabled": json.RawMessage("false"),
|
||||
"config": config,
|
||||
}
|
||||
if enabled, ok := legacy["ssoEnabled"]; ok {
|
||||
restructured["enabled"] = enabled
|
||||
}
|
||||
if roleMapping, ok := legacy["roleMapping"]; ok && string(roleMapping) != "null" {
|
||||
restructured["roleMapping"] = roleMapping
|
||||
}
|
||||
|
||||
newData, err := json.Marshal(restructured)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.NewUpdate().
|
||||
Model((*restructureAuthDomainRow)(nil)).
|
||||
Set("data = ?", string(newData)).
|
||||
Where("id = ?", row.ID).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *restructureAuthDomainConfig) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -16,7 +16,7 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
AuthNProviderGoogleAuth = AuthNProvider{valuer.NewString("google_auth")}
|
||||
AuthNProviderGoogle = AuthNProvider{valuer.NewString("google")}
|
||||
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{
|
||||
AuthNProviderGoogleAuth,
|
||||
AuthNProviderGoogle,
|
||||
AuthNProviderSAML,
|
||||
AuthNProviderEmailPassword,
|
||||
AuthNProviderOIDC,
|
||||
|
||||
@@ -9,6 +9,7 @@ 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"
|
||||
)
|
||||
|
||||
@@ -28,9 +29,58 @@ var (
|
||||
ErrCodeAuthDomainAlreadyExists = errors.MustNewCode("auth_domain_already_exists")
|
||||
)
|
||||
|
||||
// authDomainConfigVariants is the single registry of authn provider kinds:
|
||||
// UnmarshalJSON, JSONSchemaOneOf and the discriminator mapping all derive from
|
||||
// it, so a new provider is one entry here plus its authn registration.
|
||||
var authDomainConfigVariants = []authDomainConfigVariant{
|
||||
{
|
||||
kind: AuthNProviderSAML,
|
||||
decodeSpec: func(data []byte) (any, error) {
|
||||
spec := SamlConfig{}
|
||||
if err := json.Unmarshal(data, &spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return spec, nil
|
||||
},
|
||||
schema: authDomainConfigSAML{},
|
||||
schemaRef: "#/components/schemas/AuthtypesAuthDomainConfigSAML",
|
||||
},
|
||||
{
|
||||
kind: AuthNProviderGoogle,
|
||||
decodeSpec: func(data []byte) (any, error) {
|
||||
spec := GoogleConfig{}
|
||||
if err := json.Unmarshal(data, &spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return spec, nil
|
||||
},
|
||||
schema: authDomainConfigGoogle{},
|
||||
schemaRef: "#/components/schemas/AuthtypesAuthDomainConfigGoogle",
|
||||
},
|
||||
{
|
||||
kind: AuthNProviderOIDC,
|
||||
decodeSpec: func(data []byte) (any, error) {
|
||||
spec := OIDCConfig{}
|
||||
if err := json.Unmarshal(data, &spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return spec, nil
|
||||
},
|
||||
schema: authDomainConfigOIDC{},
|
||||
schemaRef: "#/components/schemas/AuthtypesAuthDomainConfigOIDC",
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
_ jsonschema.OneOfExposer = AuthDomainConfig{}
|
||||
_ jsonschema.Preparer = AuthDomainConfig{}
|
||||
)
|
||||
|
||||
type GettableAuthDomain struct {
|
||||
StorableAuthDomain
|
||||
Enabled bool `json:"enabled"`
|
||||
Config AuthDomainConfig `json:"config"`
|
||||
RoleMapping *RoleMapping `json:"roleMapping"`
|
||||
AuthNProviderInfo *AuthNProviderInfo `json:"authNProviderInfo"`
|
||||
}
|
||||
|
||||
@@ -39,12 +89,16 @@ type AuthNProviderInfo struct {
|
||||
}
|
||||
|
||||
type PostableAuthDomain struct {
|
||||
Config AuthDomainConfig `json:"config"`
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name" required:"true"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Config AuthDomainConfig `json:"config" required:"true"`
|
||||
RoleMapping *RoleMapping `json:"roleMapping"`
|
||||
}
|
||||
|
||||
type UpdatableAuthDomain struct {
|
||||
Config AuthDomainConfig `json:"config"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Config AuthDomainConfig `json:"config" required:"true"`
|
||||
RoleMapping *RoleMapping `json:"roleMapping"`
|
||||
}
|
||||
|
||||
type StorableAuthDomain struct {
|
||||
@@ -57,150 +111,46 @@ type StorableAuthDomain struct {
|
||||
types.TimeAuditable
|
||||
}
|
||||
|
||||
// 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.
|
||||
// StorableAuthDomainConfig is the JSON document persisted in StorableAuthDomain.Data.
|
||||
type StorableAuthDomainConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Config AuthDomainConfig `json:"config"`
|
||||
RoleMapping *RoleMapping `json:"roleMapping"`
|
||||
}
|
||||
|
||||
type AuthDomainConfig struct {
|
||||
SSOEnabled bool `json:"ssoEnabled"`
|
||||
AuthNProvider AuthNProvider `json:"ssoType"`
|
||||
SAML *SamlConfig `json:"samlConfig"`
|
||||
Google *GoogleConfig `json:"googleAuthConfig"`
|
||||
OIDC *OIDCConfig `json:"oidcConfig"`
|
||||
RoleMapping *RoleMapping `json:"roleMapping"`
|
||||
Kind AuthNProvider `json:"kind" required:"true"`
|
||||
Spec any `json:"spec" required:"true"`
|
||||
}
|
||||
|
||||
// authDomainConfigSAML is the OpenAPI schema for an AuthDomainConfig with kind=saml.
|
||||
type authDomainConfigSAML struct {
|
||||
Kind AuthNProvider `json:"kind" description:"The kind of authn provider." required:"true"`
|
||||
Spec SamlConfig `json:"spec" description:"The saml configuration." required:"true"`
|
||||
}
|
||||
|
||||
// authDomainConfigGoogle is the OpenAPI schema for an AuthDomainConfig with kind=google.
|
||||
type authDomainConfigGoogle struct {
|
||||
Kind AuthNProvider `json:"kind" description:"The kind of authn provider." required:"true"`
|
||||
Spec GoogleConfig `json:"spec" description:"The google auth configuration." required:"true"`
|
||||
}
|
||||
|
||||
// authDomainConfigOIDC is the OpenAPI schema for an AuthDomainConfig with kind=oidc.
|
||||
type authDomainConfigOIDC struct {
|
||||
Kind AuthNProvider `json:"kind" description:"The kind of authn provider." required:"true"`
|
||||
Spec OIDCConfig `json:"spec" description:"The oidc configuration." required:"true"`
|
||||
}
|
||||
|
||||
type authDomainConfigVariant struct {
|
||||
kind AuthNProvider
|
||||
decodeSpec func(data []byte) (any, error)
|
||||
schema any
|
||||
schemaRef string
|
||||
}
|
||||
|
||||
type AuthDomain struct {
|
||||
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{},
|
||||
}
|
||||
storableAuthDomain *StorableAuthDomain
|
||||
storableAuthDomainConfig *StorableAuthDomainConfig
|
||||
}
|
||||
|
||||
type AuthDomainStore interface {
|
||||
@@ -228,3 +178,230 @@ type AuthDomainStore interface {
|
||||
// Delete by orgID and id.
|
||||
Delete(context.Context, valuer.UUID, valuer.UUID) error
|
||||
}
|
||||
|
||||
func NewAuthDomainFromPostableAuthDomain(postableAuthDomain *PostableAuthDomain, orgID valuer.UUID) (*AuthDomain, error) {
|
||||
storableAuthDomainConfig := &StorableAuthDomainConfig{
|
||||
Enabled: postableAuthDomain.Enabled,
|
||||
Config: postableAuthDomain.Config,
|
||||
RoleMapping: postableAuthDomain.RoleMapping,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(storableAuthDomainConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AuthDomain{
|
||||
storableAuthDomain: &StorableAuthDomain{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
Name: postableAuthDomain.Name,
|
||||
Data: string(data),
|
||||
OrgID: orgID,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
},
|
||||
storableAuthDomainConfig: storableAuthDomainConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewAuthDomainFromStorableAuthDomain(storableAuthDomain *StorableAuthDomain) (*AuthDomain, error) {
|
||||
storableAuthDomainConfig := new(StorableAuthDomainConfig)
|
||||
if err := json.Unmarshal([]byte(storableAuthDomain.Data), storableAuthDomainConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AuthDomain{
|
||||
storableAuthDomain: storableAuthDomain,
|
||||
storableAuthDomainConfig: storableAuthDomainConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewGettableAuthDomainFromAuthDomain(authDomain *AuthDomain, authNProviderInfo *AuthNProviderInfo) *GettableAuthDomain {
|
||||
return &GettableAuthDomain{
|
||||
StorableAuthDomain: *authDomain.StorableAuthDomain(),
|
||||
Enabled: authDomain.Enabled(),
|
||||
Config: authDomain.Config(),
|
||||
RoleMapping: authDomain.RoleMapping(),
|
||||
AuthNProviderInfo: authNProviderInfo,
|
||||
}
|
||||
}
|
||||
|
||||
// JSONSchemaOneOf returns the oneOf variants for the AuthDomainConfig discriminated union.
|
||||
// Each variant represents a different authn provider kind with its corresponding spec schema.
|
||||
func (AuthDomainConfig) JSONSchemaOneOf() []any {
|
||||
oneOf := make([]any, len(authDomainConfigVariants))
|
||||
for i, variant := range authDomainConfigVariants {
|
||||
oneOf[i] = variant.schema
|
||||
}
|
||||
|
||||
return oneOf
|
||||
}
|
||||
|
||||
// PrepareJSONSchema marks the schema with x-signoz-discriminator;
|
||||
// signoz.attachDiscriminators promotes it to a real OpenAPI 3
|
||||
// discriminator after reflection.
|
||||
func (AuthDomainConfig) PrepareJSONSchema(schema *jsonschema.Schema) error {
|
||||
if schema.ExtraProperties == nil {
|
||||
schema.ExtraProperties = map[string]any{}
|
||||
}
|
||||
|
||||
mapping := make(map[string]string, len(authDomainConfigVariants))
|
||||
for _, variant := range authDomainConfigVariants {
|
||||
mapping[variant.kind.StringValue()] = variant.schemaRef
|
||||
}
|
||||
|
||||
schema.ExtraProperties["x-signoz-discriminator"] = map[string]any{
|
||||
"propertyName": "kind",
|
||||
"mapping": mapping,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (typ *AuthDomainConfig) UnmarshalJSON(data []byte) error {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "failed to unmarshal auth domain config")
|
||||
}
|
||||
|
||||
kindData, ok := raw["kind"]
|
||||
if !ok {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "kind is required")
|
||||
}
|
||||
|
||||
var kind AuthNProvider
|
||||
if err := json.Unmarshal(kindData, &kind); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "failed to unmarshal kind")
|
||||
}
|
||||
|
||||
specData, ok := raw["spec"]
|
||||
if !ok {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "spec is required")
|
||||
}
|
||||
|
||||
for _, variant := range authDomainConfigVariants {
|
||||
if variant.kind != kind {
|
||||
continue
|
||||
}
|
||||
|
||||
spec, err := variant.decodeSpec(specData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
typ.Kind = kind
|
||||
typ.Spec = spec
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "invalid authn provider %q", kind.StringValue())
|
||||
}
|
||||
|
||||
func (config AuthDomainConfig) SamlConfig() (SamlConfig, error) {
|
||||
spec, ok := config.Spec.(SamlConfig)
|
||||
if !ok {
|
||||
return SamlConfig{}, errors.Newf(errors.TypeInternal, ErrCodeAuthDomainMismatch, "auth domain config is not saml")
|
||||
}
|
||||
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
func (config AuthDomainConfig) GoogleConfig() (GoogleConfig, error) {
|
||||
spec, ok := config.Spec.(GoogleConfig)
|
||||
if !ok {
|
||||
return GoogleConfig{}, errors.Newf(errors.TypeInternal, ErrCodeAuthDomainMismatch, "auth domain config is not google")
|
||||
}
|
||||
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
func (config AuthDomainConfig) OIDCConfig() (OIDCConfig, error) {
|
||||
spec, ok := config.Spec.(OIDCConfig)
|
||||
if !ok {
|
||||
return OIDCConfig{}, errors.Newf(errors.TypeInternal, ErrCodeAuthDomainMismatch, "auth domain config is not oidc")
|
||||
}
|
||||
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
func (typ *AuthDomain) StorableAuthDomain() *StorableAuthDomain {
|
||||
return typ.storableAuthDomain
|
||||
}
|
||||
|
||||
func (typ *AuthDomain) Enabled() bool {
|
||||
return typ.storableAuthDomainConfig.Enabled
|
||||
}
|
||||
|
||||
func (typ *AuthDomain) Kind() AuthNProvider {
|
||||
return typ.storableAuthDomainConfig.Config.Kind
|
||||
}
|
||||
|
||||
func (typ *AuthDomain) Config() AuthDomainConfig {
|
||||
return typ.storableAuthDomainConfig.Config
|
||||
}
|
||||
|
||||
func (typ *AuthDomain) RoleMapping() *RoleMapping {
|
||||
return typ.storableAuthDomainConfig.RoleMapping
|
||||
}
|
||||
|
||||
func (typ *AuthDomain) Update(updatableAuthDomain *UpdatableAuthDomain) error {
|
||||
storableAuthDomainConfig := &StorableAuthDomainConfig{
|
||||
Enabled: updatableAuthDomain.Enabled,
|
||||
Config: updatableAuthDomain.Config,
|
||||
RoleMapping: updatableAuthDomain.RoleMapping,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(storableAuthDomainConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
typ.storableAuthDomainConfig = storableAuthDomainConfig
|
||||
typ.storableAuthDomain.Data = string(data)
|
||||
typ.storableAuthDomain.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (typ *PostableAuthDomain) UnmarshalJSON(data []byte) error {
|
||||
type Alias PostableAuthDomain
|
||||
|
||||
var temp Alias
|
||||
if err := json.Unmarshal(data, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !authDomainNameRegex.MatchString(temp.Name) {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidName, "invalid domain name %s", temp.Name)
|
||||
}
|
||||
|
||||
// A present config always carries a kind (its UnmarshalJSON rejects
|
||||
// anything else), so a zero kind means the key was absent.
|
||||
if temp.Config.Kind.IsZero() {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "config is required")
|
||||
}
|
||||
|
||||
*typ = PostableAuthDomain(temp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (typ *UpdatableAuthDomain) UnmarshalJSON(data []byte) error {
|
||||
type Alias UpdatableAuthDomain
|
||||
|
||||
var temp Alias
|
||||
if err := json.Unmarshal(data, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// A present config always carries a kind (its UnmarshalJSON rejects
|
||||
// anything else), so a zero kind means the key was absent.
|
||||
if temp.Config.Kind.IsZero() {
|
||||
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthDomainInvalidConfig, "config is required")
|
||||
}
|
||||
|
||||
*typ = UpdatableAuthDomain(temp)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,13 +12,10 @@ const wildCardDomain = "*"
|
||||
|
||||
type GoogleConfig struct {
|
||||
// ClientID is the application's ID. For example, 292085223830.apps.googleusercontent.com.
|
||||
ClientID string `json:"clientId"`
|
||||
ClientID string `json:"clientId" required:"true"`
|
||||
|
||||
// It is the application's secret.
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
|
||||
// What is the meaning of this? Should we remove this?
|
||||
RedirectURI string `json:"redirectURI"`
|
||||
ClientSecret string `json:"clientSecret" required:"true" format:"password"`
|
||||
|
||||
// Whether to fetch the Google workspace groups (required additional API scopes)
|
||||
FetchGroups bool `json:"fetchGroups"`
|
||||
@@ -26,7 +23,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"`
|
||||
ServiceAccountJSON string `json:"serviceAccountJson,omitempty" format:"password"`
|
||||
|
||||
// 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"`
|
||||
Issuer string `json:"issuer" required:"true"`
|
||||
|
||||
// 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"`
|
||||
ClientID string `json:"clientId" required:"true"`
|
||||
|
||||
// It is the application's secret.
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
ClientSecret string `json:"clientSecret" required:"true" format:"password"`
|
||||
|
||||
// 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="{samlEntity}">
|
||||
SamlEntity string `json:"samlEntity"`
|
||||
// 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 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 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 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"`
|
||||
// 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"`
|
||||
|
||||
// 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,24 +33,34 @@ func (config *SamlConfig) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if temp.SamlEntity == "" {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "samlEntity is required")
|
||||
samlConfig := SamlConfig(temp)
|
||||
if err := samlConfig.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if temp.SamlIdp == "" {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "samlIdp 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.SamlCert == "" {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "samlCert is required")
|
||||
if config.Location == "" {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "location is required")
|
||||
}
|
||||
|
||||
if temp.AttributeMapping == (AttributeMapping{}) {
|
||||
if err := json.Unmarshal([]byte("{}"), &temp.AttributeMapping); err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
*config = SamlConfig(temp)
|
||||
return nil
|
||||
}
|
||||
|
||||
136
tests/e2e/helpers/sso.ts
Normal file
136
tests/e2e/helpers/sso.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
import { authToken } from './common';
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────
|
||||
|
||||
export const ORG_SETTINGS_PATH = '/settings/org-settings';
|
||||
|
||||
// ─── Types ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface GoogleAuthDomainSeed {
|
||||
/** Domain name (e.g. `sso-edit.example.com`). Keep unique per test. */
|
||||
name: string;
|
||||
/** Enforce-SSO flag. Defaults to false. */
|
||||
enabled?: boolean;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
/**
|
||||
* Enables Google Workspace group fetching. The backend then requires
|
||||
* `serviceAccountJson` and `domainToAdminEmail`, and only with it may
|
||||
* `allowedGroups` be set.
|
||||
*/
|
||||
fetchGroups?: boolean;
|
||||
serviceAccountJson?: string;
|
||||
domainToAdminEmail?: Record<string, string>;
|
||||
allowedGroups?: string[];
|
||||
roleMapping?: {
|
||||
defaultRole?: string;
|
||||
groupMappings?: Record<string, string>;
|
||||
useRoleAttribute?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// ─── API helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Seed a Google auth domain via POST /api/v2/auth_domains. Returns the new
|
||||
* domain ID. Pair with {@link deleteAuthDomainByNameViaApi} for cleanup.
|
||||
*/
|
||||
export async function createGoogleAuthDomainViaApi(
|
||||
page: Page,
|
||||
seed: GoogleAuthDomainSeed,
|
||||
): Promise<string> {
|
||||
const token = await authToken(page);
|
||||
|
||||
const spec: Record<string, unknown> = {
|
||||
clientId: seed.clientId ?? 'e2e-client-id.apps.googleusercontent.com',
|
||||
clientSecret: seed.clientSecret ?? 'e2e-client-secret',
|
||||
fetchGroups: seed.fetchGroups ?? false,
|
||||
insecureSkipEmailVerified: false,
|
||||
};
|
||||
if (seed.serviceAccountJson) {
|
||||
spec.serviceAccountJson = seed.serviceAccountJson;
|
||||
}
|
||||
if (seed.domainToAdminEmail) {
|
||||
spec.domainToAdminEmail = seed.domainToAdminEmail;
|
||||
}
|
||||
if (seed.allowedGroups) {
|
||||
spec.allowedGroups = seed.allowedGroups;
|
||||
}
|
||||
|
||||
const res = await page.request.post('/api/v2/auth_domains', {
|
||||
data: {
|
||||
name: seed.name,
|
||||
enabled: seed.enabled ?? false,
|
||||
config: { kind: 'google', spec },
|
||||
roleMapping: seed.roleMapping,
|
||||
},
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok()) {
|
||||
throw new Error(
|
||||
`POST /api/v2/auth_domains ${res.status()}: ${await res.text()}`,
|
||||
);
|
||||
}
|
||||
const json = (await res.json()) as { data: { id: string } };
|
||||
return json.data.id;
|
||||
}
|
||||
|
||||
/** Delete an auth domain by ID (best-effort cleanup). */
|
||||
export async function deleteAuthDomainViaApi(
|
||||
page: Page,
|
||||
id: string,
|
||||
): Promise<void> {
|
||||
const token = await authToken(page);
|
||||
await page.request.delete(`/api/v2/auth_domains/${id}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete any auth domain named `name`; a no-op when absent. Doubles as the
|
||||
* leftover guard before seeding (domain names are unique per org, so a
|
||||
* crashed earlier run would otherwise make the seed conflict).
|
||||
*/
|
||||
export async function deleteAuthDomainByNameViaApi(
|
||||
page: Page,
|
||||
name: string,
|
||||
): Promise<void> {
|
||||
const token = await authToken(page);
|
||||
const res = await page.request.get('/api/v2/auth_domains', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok()) {
|
||||
throw new Error(
|
||||
`GET /api/v2/auth_domains ${res.status()}: ${await res.text()}`,
|
||||
);
|
||||
}
|
||||
const json = (await res.json()) as {
|
||||
data: Array<{ id: string; name: string }> | null;
|
||||
};
|
||||
const match = (json.data ?? []).find((domain) => domain.name === name);
|
||||
if (match) {
|
||||
await deleteAuthDomainViaApi(page, match.id);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Navigation ────────────────────────────────────────────────────────────
|
||||
|
||||
/** Open org settings and wait for the Authenticated Domains section. */
|
||||
export async function gotoAuthDomains(page: Page): Promise<void> {
|
||||
await page.goto(ORG_SETTINGS_PATH);
|
||||
await expect(page.getByTestId('auth-domain-title')).toBeVisible();
|
||||
}
|
||||
|
||||
/** Open the Configure (edit) modal for the domain row named `name`. */
|
||||
export async function openConfigureAuthDomain(
|
||||
page: Page,
|
||||
name: string,
|
||||
): Promise<void> {
|
||||
await page
|
||||
.getByTestId(`auth-domain-row-${name}`)
|
||||
.getByTestId('auth-domain-configure')
|
||||
.click();
|
||||
await expect(page.getByTestId('auth-domain-form')).toBeVisible();
|
||||
}
|
||||
235
tests/e2e/tests/sso/auth-domains.spec.ts
Normal file
235
tests/e2e/tests/sso/auth-domains.spec.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { expect, test } from '../../fixtures/auth';
|
||||
import {
|
||||
createGoogleAuthDomainViaApi,
|
||||
deleteAuthDomainByNameViaApi,
|
||||
gotoAuthDomains,
|
||||
openConfigureAuthDomain,
|
||||
ORG_SETTINGS_PATH,
|
||||
} from '../../helpers/sso';
|
||||
|
||||
// Every test seeds its own uniquely-named domain so the file can run fully
|
||||
// parallel. Names are registered here and removed by the afterEach guard;
|
||||
// the delete-by-name call before each seed clears leftovers of crashed runs.
|
||||
const cleanupNames: string[] = [];
|
||||
|
||||
test.afterEach(async ({ authedPage: page }) => {
|
||||
for (const name of cleanupNames.splice(0)) {
|
||||
await deleteAuthDomainByNameViaApi(page, name);
|
||||
}
|
||||
});
|
||||
|
||||
test('TC-01 org settings shows the authenticated domains section', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
await page.goto(ORG_SETTINGS_PATH);
|
||||
await expect(page.getByTestId('auth-domain-title')).toBeVisible();
|
||||
await expect(page.getByTestId('auth-domain-add')).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-02 create a google auth domain via the UI', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const domain = 'sso-create.example.com';
|
||||
cleanupNames.push(domain);
|
||||
await deleteAuthDomainByNameViaApi(page, domain);
|
||||
|
||||
await gotoAuthDomains(page);
|
||||
await page.getByTestId('auth-domain-add').click();
|
||||
await page.getByTestId('authn-provider-configure-google').click();
|
||||
|
||||
await page.getByTestId('google-auth-domain').fill(domain);
|
||||
await page
|
||||
.getByTestId('google-auth-client-id')
|
||||
.fill('e2e-client-id.apps.googleusercontent.com');
|
||||
await page.getByTestId('google-auth-client-secret').fill('e2e-client-secret');
|
||||
await page.getByTestId('auth-domain-save').click();
|
||||
|
||||
await expect(page.getByText('Domain created successfully')).toBeVisible();
|
||||
const row = page.getByTestId(`auth-domain-row-${domain}`);
|
||||
await expect(row).toBeVisible();
|
||||
await expect(row.getByTestId('auth-domain-configure')).toHaveText(
|
||||
'Configure Google Auth',
|
||||
);
|
||||
});
|
||||
|
||||
test('TC-03 editing the client id persists across reopen', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const domain = 'sso-edit.example.com';
|
||||
cleanupNames.push(domain);
|
||||
await deleteAuthDomainByNameViaApi(page, domain);
|
||||
await createGoogleAuthDomainViaApi(page, { name: domain });
|
||||
|
||||
await gotoAuthDomains(page);
|
||||
await openConfigureAuthDomain(page, domain);
|
||||
await expect(page.getByTestId('google-auth-client-id')).toHaveValue(
|
||||
'e2e-client-id.apps.googleusercontent.com',
|
||||
);
|
||||
await page
|
||||
.getByTestId('google-auth-client-id')
|
||||
.fill('rotated-client-id.apps.googleusercontent.com');
|
||||
await page.getByTestId('auth-domain-save').click();
|
||||
await expect(page.getByText('Domain updated successfully')).toBeVisible();
|
||||
await expect(page.getByTestId('auth-domain-form')).toBeHidden();
|
||||
|
||||
await openConfigureAuthDomain(page, domain);
|
||||
await expect(page.getByTestId('google-auth-client-id')).toHaveValue(
|
||||
'rotated-client-id.apps.googleusercontent.com',
|
||||
);
|
||||
});
|
||||
|
||||
test('TC-04 removing a group mapping persists after save', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const domain = 'sso-rolemap.example.com';
|
||||
cleanupNames.push(domain);
|
||||
await deleteAuthDomainByNameViaApi(page, domain);
|
||||
await createGoogleAuthDomainViaApi(page, {
|
||||
name: domain,
|
||||
roleMapping: {
|
||||
defaultRole: 'signoz-viewer',
|
||||
groupMappings: {
|
||||
engineers: 'signoz-editor',
|
||||
support: 'signoz-viewer',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await gotoAuthDomains(page);
|
||||
await openConfigureAuthDomain(page, domain);
|
||||
await page.getByTestId('role-mapping-header').click();
|
||||
|
||||
const rows = page.getByTestId('role-mapping-row');
|
||||
await expect(rows).toHaveCount(2);
|
||||
// Go marshals map keys sorted, so "engineers" is always the first row.
|
||||
await expect(rows.nth(0).getByTestId('role-mapping-group-name')).toHaveValue(
|
||||
'engineers',
|
||||
);
|
||||
await rows.nth(0).getByTestId('role-mapping-remove').click();
|
||||
await expect(rows).toHaveCount(1);
|
||||
|
||||
// The PUT body is the #2402 contract: the removed mapping must be gone
|
||||
// from the payload, not just from the form state.
|
||||
const putRequest = page.waitForRequest(
|
||||
(req) =>
|
||||
req.method() === 'PUT' && req.url().includes('/api/v2/auth_domains/'),
|
||||
);
|
||||
await page.getByTestId('auth-domain-save').click();
|
||||
const body = JSON.parse((await putRequest).postData() ?? '{}');
|
||||
expect(body.roleMapping?.groupMappings).toEqual({
|
||||
support: 'signoz-viewer',
|
||||
});
|
||||
await expect(page.getByText('Domain updated successfully')).toBeVisible();
|
||||
await expect(page.getByTestId('auth-domain-form')).toBeHidden();
|
||||
|
||||
await openConfigureAuthDomain(page, domain);
|
||||
await page.getByTestId('role-mapping-header').click();
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(rows.getByTestId('role-mapping-group-name')).toHaveValue(
|
||||
'support',
|
||||
);
|
||||
});
|
||||
|
||||
test('TC-05 disabling fetch groups clears the allowed groups', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const domain = 'sso-groups.example.com';
|
||||
cleanupNames.push(domain);
|
||||
await deleteAuthDomainByNameViaApi(page, domain);
|
||||
await createGoogleAuthDomainViaApi(page, {
|
||||
name: domain,
|
||||
fetchGroups: true,
|
||||
serviceAccountJson: '{"type":"service_account","project_id":"e2e"}',
|
||||
domainToAdminEmail: { '*': 'admin@sso-groups.example.com' },
|
||||
allowedGroups: ['engineering@sso-groups.example.com'],
|
||||
});
|
||||
|
||||
await gotoAuthDomains(page);
|
||||
await openConfigureAuthDomain(page, domain);
|
||||
await page.getByTestId('google-auth-workspace-groups-header').click();
|
||||
|
||||
const fetchGroups = page
|
||||
.getByTestId('google-auth-fetch-groups')
|
||||
.getByRole('checkbox');
|
||||
await expect(fetchGroups).toBeChecked();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('google-auth-allowed-groups')
|
||||
.locator('.ant-select-selection-item'),
|
||||
).toHaveCount(1);
|
||||
await fetchGroups.click();
|
||||
await expect(fetchGroups).not.toBeChecked();
|
||||
|
||||
// The PUT body is the #2402 contract: with fetchGroups off, the payload
|
||||
// must drop allowedGroups instead of resending the stale list.
|
||||
const putRequest = page.waitForRequest(
|
||||
(req) =>
|
||||
req.method() === 'PUT' && req.url().includes('/api/v2/auth_domains/'),
|
||||
);
|
||||
await page.getByTestId('auth-domain-save').click();
|
||||
const spec = JSON.parse((await putRequest).postData() ?? '{}').config?.spec;
|
||||
expect(spec?.fetchGroups).toBeFalsy();
|
||||
expect(spec?.allowedGroups).toBeUndefined();
|
||||
expect(spec?.domainToAdminEmail).toEqual({});
|
||||
await expect(page.getByText('Domain updated successfully')).toBeVisible();
|
||||
await expect(page.getByTestId('auth-domain-form')).toBeHidden();
|
||||
|
||||
await openConfigureAuthDomain(page, domain);
|
||||
await page.getByTestId('google-auth-workspace-groups-header').click();
|
||||
await expect(fetchGroups).not.toBeChecked();
|
||||
// Re-enable to reveal the group fields: the allowed-groups list must be
|
||||
// empty, not repopulated from the pre-disable state.
|
||||
await fetchGroups.click();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('google-auth-allowed-groups')
|
||||
.locator('.ant-select-selection-item'),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('TC-06 enforce sso toggle persists across reload', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const domain = 'sso-toggle.example.com';
|
||||
cleanupNames.push(domain);
|
||||
await deleteAuthDomainByNameViaApi(page, domain);
|
||||
await createGoogleAuthDomainViaApi(page, { name: domain, enabled: false });
|
||||
|
||||
await gotoAuthDomains(page);
|
||||
const toggle = page
|
||||
.getByTestId(`auth-domain-row-${domain}`)
|
||||
.getByTestId('auth-domain-enforce-sso');
|
||||
await expect(toggle).not.toBeChecked();
|
||||
|
||||
const putResponse = page.waitForResponse(
|
||||
(res) =>
|
||||
res.request().method() === 'PUT' &&
|
||||
res.url().includes('/api/v2/auth_domains/'),
|
||||
);
|
||||
await toggle.click();
|
||||
expect((await putResponse).status()).toBe(204);
|
||||
|
||||
await page.reload();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId(`auth-domain-row-${domain}`)
|
||||
.getByTestId('auth-domain-enforce-sso'),
|
||||
).toBeChecked();
|
||||
});
|
||||
|
||||
test('TC-07 delete a domain via the UI', async ({ authedPage: page }) => {
|
||||
const domain = 'sso-delete.example.com';
|
||||
cleanupNames.push(domain);
|
||||
await deleteAuthDomainByNameViaApi(page, domain);
|
||||
await createGoogleAuthDomainViaApi(page, { name: domain });
|
||||
|
||||
await gotoAuthDomains(page);
|
||||
await page
|
||||
.getByTestId(`auth-domain-row-${domain}`)
|
||||
.getByTestId('auth-domain-delete')
|
||||
.click();
|
||||
await page.getByTestId('auth-domain-delete-confirm').click();
|
||||
|
||||
await expect(page.getByText('Domain deleted successfully')).toBeVisible();
|
||||
await expect(page.getByTestId(`auth-domain-row-${domain}`)).toHaveCount(0);
|
||||
});
|
||||
27
tests/fixtures/cloudintegrations.py
vendored
27
tests/fixtures/cloudintegrations.py
vendored
@@ -26,14 +26,14 @@ class ProviderAccountSpec:
|
||||
provider: str
|
||||
# params for the account created by default.
|
||||
initial_params: dict
|
||||
# params for the config an update (PUT) test sends.
|
||||
updated_params: dict
|
||||
# params -> the provider-keyed `config` block for a POST/PUT body.
|
||||
build_config: Callable[[dict], dict]
|
||||
# params -> the full config block the API is expected to return under
|
||||
# config[provider] on GET/list. This may differ from what build_config sends:
|
||||
# e.g. AWS accepts deploymentRegion on POST but the API does not echo it back.
|
||||
expected_config: Callable[[dict], dict]
|
||||
# only the suites that exercise updates need to supply it.
|
||||
updated_params: dict = field(default_factory=dict)
|
||||
# id shown in parametrized test names; defaults to the provider slug.
|
||||
id: str = field(default="")
|
||||
|
||||
@@ -42,29 +42,6 @@ class ProviderAccountSpec:
|
||||
object.__setattr__(self, "id", self.provider)
|
||||
|
||||
|
||||
# Per-provider service shape.
|
||||
@dataclass(frozen=True)
|
||||
class ProviderServiceSpec:
|
||||
provider: str
|
||||
service_id: str
|
||||
# GCP ships every service with supportedSignals.logs false, so a logs block
|
||||
# is neither required on write nor persisted.
|
||||
supports_logs: bool
|
||||
account_config: dict
|
||||
# id shown in parametrized test names; defaults to the provider slug.
|
||||
id: str = field(default="")
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.id:
|
||||
object.__setattr__(self, "id", self.provider)
|
||||
|
||||
def build_service_config(self, metrics_enabled: bool, logs_enabled: bool | None = None) -> dict:
|
||||
config: dict = {"metrics": {"enabled": metrics_enabled}}
|
||||
if self.supports_logs:
|
||||
config["logs"] = {"enabled": metrics_enabled if logs_enabled is None else logs_enabled}
|
||||
return {self.provider: config}
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def deprecated_create_cloud_integration_account(
|
||||
request: pytest.FixtureRequest,
|
||||
|
||||
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/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_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/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_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/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_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/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/signoz/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "oidc.basepath.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "oidc",
|
||||
"oidcConfig": {
|
||||
"kind": "oidc",
|
||||
"spec": {
|
||||
"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/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/signoz/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "saml.basepath.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": settings["entityID"],
|
||||
"samlIdp": settings["singleSignOnServiceLocation"],
|
||||
"samlCert": settings["certificate"],
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": settings["entityID"],
|
||||
"location": settings["singleSignOnServiceLocation"],
|
||||
"certificate": settings["certificate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from collections.abc import Callable
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
@@ -14,30 +15,35 @@ def test_create_and_get_domain(
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Get domains which should be an empty list
|
||||
# Reruns against a reused stack find domains from previous runs; drop them
|
||||
# all so the suite starts from a clean slate.
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
data = response.json()["data"]
|
||||
assert len(data) == 0
|
||||
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
|
||||
|
||||
# Create a domain with google auth config
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "domain-google.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "google_auth",
|
||||
"googleAuthConfig": {
|
||||
"kind": "google",
|
||||
"spec": {
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
"redirectURI": "redirect-uri",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -49,16 +55,16 @@ def test_create_and_get_domain(
|
||||
|
||||
# Create a domain with saml config
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "domain-saml.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": "saml-entity",
|
||||
"samlIdp": "saml-idp",
|
||||
"samlCert": "saml-cert",
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -70,7 +76,7 @@ def test_create_and_get_domain(
|
||||
|
||||
# List the domains
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
@@ -86,7 +92,7 @@ def test_create_and_get_domain(
|
||||
"domain-google.integration.test",
|
||||
"domain-saml.integration.test",
|
||||
]
|
||||
assert domain["config"]["ssoType"] in ["google_auth", "saml"]
|
||||
assert domain["config"]["kind"] in ["google", "saml"]
|
||||
|
||||
|
||||
def test_create_invalid(
|
||||
@@ -96,15 +102,15 @@ def test_create_invalid(
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create a domain with type saml and body for oidc, this should fail because oidcConfig is not allowed for saml
|
||||
# Create a domain with kind saml and a spec for oidc, this should fail because the spec does not match the kind
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "domain.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"oidcConfig": {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
"issuer": "issuer",
|
||||
@@ -117,18 +123,34 @@ def test_create_invalid(
|
||||
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
|
||||
# Create a domain with a kind but no spec
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "domain.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
|
||||
# Create a domain with invalid name
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "$%^invalid",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": "saml-entity",
|
||||
"samlIdp": "saml-idp",
|
||||
"samlCert": "saml-cert",
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -140,17 +162,17 @@ def test_create_invalid(
|
||||
|
||||
# Create a domain with no name
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": "saml-entity",
|
||||
"samlIdp": "saml-idp",
|
||||
"samlCert": "saml-cert",
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
@@ -160,7 +182,7 @@ def test_create_invalid(
|
||||
|
||||
# Create a domain with no config
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "domain.integration.test",
|
||||
},
|
||||
@@ -180,21 +202,21 @@ def test_create_invalid_role_mapping(
|
||||
|
||||
# Create domain with invalid defaultRole
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "invalid-role-test.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": "saml-entity",
|
||||
"samlIdp": "saml-idp",
|
||||
"samlCert": "saml-cert",
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "SUPERADMIN", # Invalid role
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "SUPERADMIN", # Invalid role
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
@@ -204,22 +226,22 @@ def test_create_invalid_role_mapping(
|
||||
|
||||
# Create domain with invalid role in groupMappings
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "invalid-group-role.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": "saml-entity",
|
||||
"samlIdp": "saml-idp",
|
||||
"samlCert": "saml-cert",
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"admins": "SUPERUSER", # Invalid role
|
||||
},
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"admins": "SUPERUSER", # Invalid role
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -231,23 +253,23 @@ def test_create_invalid_role_mapping(
|
||||
|
||||
# Valid role mapping should succeed
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "valid-role-mapping.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": "saml-entity",
|
||||
"samlIdp": "saml-idp",
|
||||
"samlCert": "saml-cert",
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
},
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -256,3 +278,376 @@ def test_create_invalid_role_mapping(
|
||||
)
|
||||
|
||||
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",
|
||||
},
|
||||
"fetchTransitiveGroupMembership": True,
|
||||
"allowedGroups": ["group-one", "group-two"],
|
||||
"insecureSkipEmailVerified": True,
|
||||
},
|
||||
},
|
||||
None,
|
||||
{
|
||||
"kind": "google",
|
||||
"spec": {
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
"fetchGroups": True,
|
||||
"serviceAccountJson": '{"type": "service_account"}',
|
||||
"domainToAdminEmail": {
|
||||
"roundtrip.integration.test": "admin@roundtrip.integration.test",
|
||||
"*": "fallback@roundtrip.integration.test",
|
||||
},
|
||||
"fetchTransitiveGroupMembership": True,
|
||||
"allowedGroups": ["group-one", "group-two"],
|
||||
"insecureSkipEmailVerified": True,
|
||||
},
|
||||
},
|
||||
None,
|
||||
id="google_full",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "https://idp.integration.test/sso",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
},
|
||||
None,
|
||||
{
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "https://idp.integration.test/sso",
|
||||
"certificate": "saml-cert",
|
||||
"insecureSkipAuthNRequestsSigned": False,
|
||||
"attributeMapping": {"email": "email", "name": "name", "groups": "groups", "role": "role"},
|
||||
},
|
||||
},
|
||||
None,
|
||||
id="saml_minimal",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "https://idp.integration.test/sso",
|
||||
"certificate": "saml-cert",
|
||||
"insecureSkipAuthNRequestsSigned": True,
|
||||
"attributeMapping": {"email": "mail"},
|
||||
},
|
||||
},
|
||||
None,
|
||||
{
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "https://idp.integration.test/sso",
|
||||
"certificate": "saml-cert",
|
||||
"insecureSkipAuthNRequestsSigned": True,
|
||||
"attributeMapping": {"email": "mail", "name": "name", "groups": "groups", "role": "role"},
|
||||
},
|
||||
},
|
||||
None,
|
||||
id="saml_partial_attribute_mapping",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"kind": "oidc",
|
||||
"spec": {
|
||||
"issuer": "https://issuer.integration.test",
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
},
|
||||
},
|
||||
None,
|
||||
{
|
||||
"kind": "oidc",
|
||||
"spec": {
|
||||
"issuer": "https://issuer.integration.test",
|
||||
"issuerAlias": "",
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
"claimMapping": {"email": "email", "name": "name", "groups": "groups", "role": "role"},
|
||||
"insecureSkipEmailVerified": False,
|
||||
"getUserInfo": False,
|
||||
},
|
||||
},
|
||||
None,
|
||||
id="oidc_minimal",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"kind": "oidc",
|
||||
"spec": {
|
||||
"issuer": "https://issuer.integration.test",
|
||||
"issuerAlias": "https://alias.integration.test",
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
"claimMapping": {"email": "eml", "name": "nm", "groups": "grps", "role": "rl"},
|
||||
"insecureSkipEmailVerified": True,
|
||||
"getUserInfo": True,
|
||||
},
|
||||
},
|
||||
None,
|
||||
{
|
||||
"kind": "oidc",
|
||||
"spec": {
|
||||
"issuer": "https://issuer.integration.test",
|
||||
"issuerAlias": "https://alias.integration.test",
|
||||
"clientId": "client-id",
|
||||
"clientSecret": "client-secret",
|
||||
"claimMapping": {"email": "eml", "name": "nm", "groups": "grps", "role": "rl"},
|
||||
"insecureSkipEmailVerified": True,
|
||||
"getUserInfo": True,
|
||||
},
|
||||
},
|
||||
None,
|
||||
id="oidc_full",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "https://idp.integration.test/sso",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
},
|
||||
{
|
||||
"defaultRole": "EDITOR",
|
||||
"groupMappings": {"platform-team": "ADMIN"},
|
||||
"useRoleAttribute": False,
|
||||
},
|
||||
{
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "https://idp.integration.test/sso",
|
||||
"certificate": "saml-cert",
|
||||
"insecureSkipAuthNRequestsSigned": False,
|
||||
"attributeMapping": {"email": "email", "name": "name", "groups": "groups", "role": "role"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"defaultRole": "signoz-editor",
|
||||
"groupMappings": {"platform-team": "signoz-admin"},
|
||||
"useRoleAttribute": False,
|
||||
},
|
||||
id="role_mapping_names_normalized",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "https://idp.integration.test/sso",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
},
|
||||
{"defaultRole": "VIEWER", "useRoleAttribute": True},
|
||||
{
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "https://idp.integration.test/sso",
|
||||
"certificate": "saml-cert",
|
||||
"insecureSkipAuthNRequestsSigned": False,
|
||||
"attributeMapping": {"email": "email", "name": "name", "groups": "groups", "role": "role"},
|
||||
},
|
||||
},
|
||||
{"defaultRole": "signoz-viewer", "groupMappings": None, "useRoleAttribute": True},
|
||||
id="role_mapping_null_group_mappings",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_domain_roundtrip( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
config: dict,
|
||||
role_mapping: dict | None,
|
||||
expected_config: dict,
|
||||
expected_role_mapping: dict | None,
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Drop a same-named leftover so reruns against a reused stack stay green.
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
for domain in response.json()["data"]:
|
||||
if domain["name"] == "roundtrip.integration.test":
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "roundtrip.integration.test",
|
||||
"enabled": True,
|
||||
"config": config,
|
||||
"roleMapping": role_mapping,
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED
|
||||
domain_id = response.json()["data"]["id"]
|
||||
|
||||
# Clients (e.g. the terraform provider) read state back with a follow-up
|
||||
# GET after every write, so posted values must round-trip exactly; the
|
||||
# server-side defaulting and role-name normalization pinned here are part
|
||||
# of that contract.
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
data = response.json()["data"]
|
||||
|
||||
assert data["name"] == "roundtrip.integration.test"
|
||||
assert data["enabled"] is True
|
||||
assert data["config"] == expected_config
|
||||
assert data["roleMapping"] == expected_role_mapping
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
def test_update_enabled(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Drop a same-named leftover so reruns against a reused stack stay green.
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
for domain in response.json()["data"]:
|
||||
if domain["name"] == "update-enabled.integration.test":
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "update-enabled.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED
|
||||
domain_id = response.json()["data"]["id"]
|
||||
|
||||
# Flipping enforcement goes through the same full update as any other
|
||||
# change; the echoed provider config must survive the round trip.
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
json={
|
||||
"enabled": False,
|
||||
"config": {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
},
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
data = response.json()["data"]
|
||||
|
||||
assert data["enabled"] is False
|
||||
assert data["config"] == {
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": "saml-entity",
|
||||
"location": "saml-idp",
|
||||
"certificate": "saml-cert",
|
||||
"insecureSkipAuthNRequestsSigned": False,
|
||||
"attributeMapping": {"email": "email", "name": "name", "groups": "groups", "role": "role"},
|
||||
},
|
||||
}
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
@@ -50,16 +50,16 @@ def test_create_auth_domain(
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "saml.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": settings["entityID"],
|
||||
"samlIdp": settings["singleSignOnServiceLocation"],
|
||||
"samlCert": settings["certificate"],
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": settings["entityID"],
|
||||
"location": settings["singleSignOnServiceLocation"],
|
||||
"certificate": 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/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_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/v1/domains/{domain['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
json={
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": settings["entityID"],
|
||||
"samlIdp": settings["singleSignOnServiceLocation"],
|
||||
"samlCert": settings["certificate"],
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": settings["entityID"],
|
||||
"location": settings["singleSignOnServiceLocation"],
|
||||
"certificate": settings["certificate"],
|
||||
"attributeMapping": {
|
||||
"name": "givenName",
|
||||
"groups": "groups",
|
||||
"role": "signoz_role",
|
||||
},
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
"signoz-viewers": "VIEWER",
|
||||
},
|
||||
"useRoleAttribute": False,
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
"signoz-viewers": "VIEWER",
|
||||
},
|
||||
"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/v1/domains/{domain['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
json={
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "saml",
|
||||
"samlConfig": {
|
||||
"samlEntity": settings["entityID"],
|
||||
"samlIdp": settings["singleSignOnServiceLocation"],
|
||||
"samlCert": settings["certificate"],
|
||||
"kind": "saml",
|
||||
"spec": {
|
||||
"entityId": settings["entityID"],
|
||||
"location": settings["singleSignOnServiceLocation"],
|
||||
"certificate": settings["certificate"],
|
||||
"attributeMapping": {
|
||||
"name": "displayName",
|
||||
"groups": "groups",
|
||||
"role": "signoz_role",
|
||||
},
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
},
|
||||
"useRoleAttribute": True,
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
},
|
||||
"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/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": "oidc.integration.test",
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "oidc",
|
||||
"oidcConfig": {
|
||||
"kind": "oidc",
|
||||
"spec": {
|
||||
"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/v1/domains/{domain['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
json={
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "oidc",
|
||||
"oidcConfig": {
|
||||
"kind": "oidc",
|
||||
"spec": {
|
||||
"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",
|
||||
},
|
||||
"useRoleAttribute": False,
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
"signoz-viewers": "VIEWER",
|
||||
},
|
||||
"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/v1/domains/{domain['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
json={
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "oidc",
|
||||
"oidcConfig": {
|
||||
"kind": "oidc",
|
||||
"spec": {
|
||||
"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",
|
||||
},
|
||||
"useRoleAttribute": True,
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "VIEWER",
|
||||
"groupMappings": {
|
||||
"signoz-admins": "ADMIN",
|
||||
"signoz-editors": "EDITOR",
|
||||
},
|
||||
"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/v1/domains/{domain['id']}"),
|
||||
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/v1/domains"),
|
||||
signoz.self.host_configs["8080"].get("/api/v2/auth_domains"),
|
||||
json={
|
||||
"name": GOOGLE_DOMAIN,
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "google_auth",
|
||||
"googleAuthConfig": {
|
||||
"kind": "google",
|
||||
"spec": {
|
||||
"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/v1/domains/{domain['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
json={
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "google_auth",
|
||||
"googleAuthConfig": {
|
||||
"kind": "google",
|
||||
"spec": {
|
||||
"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/v1/domains/{domain['id']}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/auth_domains/{domain['id']}"),
|
||||
json={
|
||||
"enabled": True,
|
||||
"config": {
|
||||
"ssoEnabled": True,
|
||||
"ssoType": "google_auth",
|
||||
"googleAuthConfig": {
|
||||
"kind": "google",
|
||||
"spec": {
|
||||
"clientId": GOOGLE_CLIENT_ID,
|
||||
"clientSecret": GOOGLE_CLIENT_SECRET,
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "EDITOR",
|
||||
},
|
||||
},
|
||||
"roleMapping": {
|
||||
"defaultRole": "EDITOR",
|
||||
},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
|
||||
@@ -1,52 +1,14 @@
|
||||
from collections.abc import Callable
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license
|
||||
from fixtures.cloudintegrations import ProviderAccountSpec
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
AWS_ACCOUNT_SPEC = ProviderAccountSpec(
|
||||
provider="aws",
|
||||
initial_params={"deployment_region": "us-east-1", "regions": ["us-east-1", "us-west-2"]},
|
||||
build_config=lambda p: {"aws": {"deploymentRegion": p["deployment_region"], "regions": p["regions"]}},
|
||||
expected_config=lambda p: {"regions": p["regions"]},
|
||||
)
|
||||
|
||||
GCP_ACCOUNT_SPEC = ProviderAccountSpec(
|
||||
provider="gcp",
|
||||
initial_params={
|
||||
"deployment_project_id": "signoz-test-project",
|
||||
"deployment_region": "us-central1",
|
||||
"project_ids": ["signoz-test-project"],
|
||||
},
|
||||
build_config=lambda p: {
|
||||
"gcp": {
|
||||
"deploymentProjectId": p["deployment_project_id"],
|
||||
"deploymentRegion": p["deployment_region"],
|
||||
"projectIds": p["project_ids"],
|
||||
}
|
||||
},
|
||||
expected_config=lambda p: {
|
||||
"deploymentProjectId": p["deployment_project_id"],
|
||||
"deploymentRegion": p["deployment_region"],
|
||||
"projectIds": p["project_ids"],
|
||||
},
|
||||
)
|
||||
|
||||
PROVIDER_ACCOUNT_SPECS = [AWS_ACCOUNT_SPEC, GCP_ACCOUNT_SPEC]
|
||||
|
||||
provider_spec = pytest.mark.parametrize(
|
||||
"spec",
|
||||
PROVIDER_ACCOUNT_SPECS,
|
||||
ids=[s.id for s in PROVIDER_ACCOUNT_SPECS],
|
||||
)
|
||||
|
||||
|
||||
def test_apply_license(
|
||||
signoz: types.SigNoz,
|
||||
@@ -58,19 +20,19 @@ def test_apply_license(
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_create_account(
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
spec: ProviderAccountSpec,
|
||||
) -> None:
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
cloud_provider = "aws"
|
||||
|
||||
data = create_cloud_integration_account(
|
||||
admin_token,
|
||||
spec.provider,
|
||||
config=spec.build_config(spec.initial_params),
|
||||
cloud_provider,
|
||||
deployment_region="us-east-1",
|
||||
regions=["us-east-1", "us-west-2"],
|
||||
)
|
||||
|
||||
assert "id" in data, "Response data should contain 'id' field"
|
||||
@@ -78,17 +40,12 @@ def test_create_account(
|
||||
|
||||
assert "connectionArtifact" in data, "Response data should contain 'connectionArtifact' field"
|
||||
artifact = data["connectionArtifact"]
|
||||
assert "aws" in artifact, "connectionArtifact should contain 'aws' field"
|
||||
assert "connectionUrl" in artifact["aws"], "connectionArtifact.aws should contain 'connectionUrl'"
|
||||
|
||||
if spec.provider == "aws":
|
||||
assert "aws" in artifact, "connectionArtifact should contain 'aws' field"
|
||||
assert "connectionUrl" in artifact["aws"], "connectionArtifact.aws should contain 'connectionUrl'"
|
||||
|
||||
connection_url = artifact["aws"]["connectionUrl"]
|
||||
assert "console.aws.amazon.com/cloudformation" in connection_url, "connectionUrl should be an AWS CloudFormation URL"
|
||||
assert f"region={spec.initial_params['deployment_region']}" in connection_url, "connectionUrl should contain the deployment region"
|
||||
else:
|
||||
# GCP is a manual flow: no one-click install artifact.
|
||||
assert artifact.get("gcp") is None, f"GCP should not return a connection artifact, got: {artifact}"
|
||||
connection_url = artifact["aws"]["connectionUrl"]
|
||||
assert "console.aws.amazon.com/cloudformation" in connection_url, "connectionUrl should be an AWS CloudFormation URL"
|
||||
assert "region=us-east-1" in connection_url, "connectionUrl should contain the deployment region"
|
||||
|
||||
|
||||
def test_create_account_unsupported_provider(
|
||||
@@ -119,36 +76,3 @@ def test_create_account_unsupported_provider(
|
||||
|
||||
response_data = response.json()
|
||||
assert "error" in response_data, "Response should contain 'error' field"
|
||||
|
||||
|
||||
def test_create_gcp_account_without_project_ids(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""GCP account config requires at least one project ID to monitor."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/cloud_integrations/gcp/accounts"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json={
|
||||
"config": {
|
||||
"gcp": {
|
||||
"deploymentProjectId": "signoz-test-project",
|
||||
"deploymentRegion": "us-central1",
|
||||
"projectIds": [],
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"sigNozApiURL": "https://test.signoz.cloud",
|
||||
"sigNozApiKey": "test-key",
|
||||
"ingestionUrl": "https://ingest.test.signoz.cloud",
|
||||
"ingestionKey": "test-ingestion-key",
|
||||
},
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, f"Expected 400 for empty projectIds, got {response.status_code}: {response.text}"
|
||||
assert "error" in response.json(), "Response should contain 'error' field"
|
||||
|
||||
@@ -2,53 +2,14 @@ import uuid
|
||||
from collections.abc import Callable
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license
|
||||
from fixtures.cloudintegrations import (
|
||||
ProviderAccountSpec,
|
||||
simulate_agent_checkin,
|
||||
)
|
||||
from fixtures.cloudintegrations import simulate_agent_checkin
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
AWS_ACCOUNT_SPEC = ProviderAccountSpec(
|
||||
provider="aws",
|
||||
initial_params={"deployment_region": "us-east-1", "regions": ["us-east-1"]},
|
||||
build_config=lambda p: {"aws": {"deploymentRegion": p["deployment_region"], "regions": p["regions"]}},
|
||||
expected_config=lambda p: {"regions": p["regions"]},
|
||||
)
|
||||
|
||||
GCP_ACCOUNT_SPEC = ProviderAccountSpec(
|
||||
provider="gcp",
|
||||
initial_params={
|
||||
"deployment_project_id": "signoz-test-project",
|
||||
"deployment_region": "us-central1",
|
||||
"project_ids": ["signoz-test-project"],
|
||||
},
|
||||
build_config=lambda p: {
|
||||
"gcp": {
|
||||
"deploymentProjectId": p["deployment_project_id"],
|
||||
"deploymentRegion": p["deployment_region"],
|
||||
"projectIds": p["project_ids"],
|
||||
}
|
||||
},
|
||||
expected_config=lambda p: {
|
||||
"deploymentProjectId": p["deployment_project_id"],
|
||||
"deploymentRegion": p["deployment_region"],
|
||||
"projectIds": p["project_ids"],
|
||||
},
|
||||
)
|
||||
|
||||
PROVIDER_ACCOUNT_SPECS = [AWS_ACCOUNT_SPEC, GCP_ACCOUNT_SPEC]
|
||||
|
||||
provider_spec = pytest.mark.parametrize(
|
||||
"spec",
|
||||
PROVIDER_ACCOUNT_SPECS,
|
||||
ids=[s.id for s in PROVIDER_ACCOUNT_SPECS],
|
||||
)
|
||||
CLOUD_PROVIDER = "aws"
|
||||
|
||||
|
||||
def test_apply_license(
|
||||
@@ -61,28 +22,22 @@ def test_apply_license(
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_agent_check_in(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
spec: ProviderAccountSpec,
|
||||
) -> None:
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
account = create_cloud_integration_account(
|
||||
admin_token,
|
||||
spec.provider,
|
||||
config=spec.build_config(spec.initial_params),
|
||||
)
|
||||
account = create_cloud_integration_account(admin_token, CLOUD_PROVIDER, regions=["us-east-1"])
|
||||
account_id = account["id"]
|
||||
provider_account_id = str(uuid.uuid4())
|
||||
|
||||
response = simulate_agent_checkin(
|
||||
signoz,
|
||||
admin_token,
|
||||
spec.provider,
|
||||
CLOUD_PROVIDER,
|
||||
account_id,
|
||||
provider_account_id,
|
||||
data={"version": "v0.0.8"},
|
||||
@@ -92,63 +47,57 @@ def test_agent_check_in(
|
||||
|
||||
data = response.json()["data"]
|
||||
|
||||
# New camelCase fields
|
||||
assert data["cloudIntegrationId"] == account_id, "cloudIntegrationId should match"
|
||||
assert data["providerAccountId"] == provider_account_id, "providerAccountId should match"
|
||||
assert "integrationConfig" in data, "Response should contain 'integrationConfig'"
|
||||
assert data["removedAt"] is None, "removedAt should be null for a live account"
|
||||
|
||||
if spec.provider == "aws":
|
||||
# Backward compat for agents deployed before the camelCase response; AWS only.
|
||||
assert data["account_id"] == account_id, "account_id (compat) should match"
|
||||
assert data["cloud_account_id"] == provider_account_id, "cloud_account_id (compat) should match"
|
||||
assert "integration_config" in data, "Response should contain 'integration_config' (compat)"
|
||||
assert "removed_at" in data, "Response should contain 'removed_at' (compat)"
|
||||
# Backward-compat snake_case fields
|
||||
assert data["account_id"] == account_id, "account_id (compat) should match"
|
||||
assert data["cloud_account_id"] == provider_account_id, "cloud_account_id (compat) should match"
|
||||
assert "integration_config" in data, "Response should contain 'integration_config' (compat)"
|
||||
assert "removed_at" in data, "Response should contain 'removed_at' (compat)"
|
||||
|
||||
integration_config = data["integrationConfig"]
|
||||
assert "aws" in integration_config, "integrationConfig should contain 'aws' block"
|
||||
assert integration_config["aws"]["enabledRegions"] == spec.initial_params["regions"], "enabledRegions should match account config"
|
||||
else:
|
||||
# GCP is a manual flow: the agent carries its own configuration.
|
||||
assert data["integrationConfig"].get("gcp") is None, f"GCP should not return an integration config, got: {data['integrationConfig']}"
|
||||
# integrationConfig should reflect the configured regions
|
||||
integration_config = data["integrationConfig"]
|
||||
assert "aws" in integration_config, "integrationConfig should contain 'aws' block"
|
||||
assert integration_config["aws"]["enabledRegions"] == ["us-east-1"], "enabledRegions should match account config"
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_agent_check_in_account_not_found(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
spec: ProviderAccountSpec,
|
||||
) -> None:
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
fake_id = str(uuid.uuid4())
|
||||
|
||||
response = simulate_agent_checkin(signoz, admin_token, spec.provider, fake_id, str(uuid.uuid4()))
|
||||
response = simulate_agent_checkin(signoz, admin_token, CLOUD_PROVIDER, fake_id, str(uuid.uuid4()))
|
||||
|
||||
assert response.status_code == HTTPStatus.NOT_FOUND, f"Expected 404, got {response.status_code}: {response.text}"
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_duplicate_cloud_account_checkins(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
spec: ProviderAccountSpec,
|
||||
) -> None:
|
||||
"""Test that two different accounts cannot check in with the same providerAccountId."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
account1 = create_cloud_integration_account(admin_token, spec.provider, config=spec.build_config(spec.initial_params))
|
||||
account2 = create_cloud_integration_account(admin_token, spec.provider, config=spec.build_config(spec.initial_params))
|
||||
account1 = create_cloud_integration_account(admin_token, CLOUD_PROVIDER)
|
||||
account2 = create_cloud_integration_account(admin_token, CLOUD_PROVIDER)
|
||||
|
||||
assert account1["id"] != account2["id"], "Two accounts should have different IDs"
|
||||
|
||||
same_provider_account_id = str(uuid.uuid4())
|
||||
|
||||
# First check-in: account1 claims the provider account ID
|
||||
response = simulate_agent_checkin(signoz, admin_token, spec.provider, account1["id"], same_provider_account_id)
|
||||
response = simulate_agent_checkin(signoz, admin_token, CLOUD_PROVIDER, account1["id"], same_provider_account_id)
|
||||
assert response.status_code == HTTPStatus.OK, f"Expected 200 for first check-in, got {response.status_code}: {response.text}"
|
||||
|
||||
# Second check-in: account2 tries to claim the same provider account ID → 409
|
||||
response = simulate_agent_checkin(signoz, admin_token, spec.provider, account2["id"], same_provider_account_id)
|
||||
response = simulate_agent_checkin(signoz, admin_token, CLOUD_PROVIDER, account2["id"], same_provider_account_id)
|
||||
assert response.status_code == HTTPStatus.CONFLICT, f"Expected 409 for duplicate providerAccountId, got {response.status_code}: {response.text}"
|
||||
|
||||
@@ -2,47 +2,18 @@ import uuid
|
||||
from collections.abc import Callable
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from sqlalchemy import bindparam, sql
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license
|
||||
from fixtures.cloudintegrations import (
|
||||
ProviderServiceSpec,
|
||||
simulate_agent_checkin,
|
||||
)
|
||||
from fixtures.cloudintegrations import simulate_agent_checkin
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
AWS_SERVICE_SPEC = ProviderServiceSpec(
|
||||
provider="aws",
|
||||
service_id="rds",
|
||||
supports_logs=True,
|
||||
account_config={"aws": {"deploymentRegion": "us-east-1", "regions": ["us-east-1"]}},
|
||||
)
|
||||
|
||||
GCP_SERVICE_SPEC = ProviderServiceSpec(
|
||||
provider="gcp",
|
||||
service_id="cloudsql_postgres",
|
||||
supports_logs=False,
|
||||
account_config={
|
||||
"gcp": {
|
||||
"deploymentProjectId": "signoz-test-project",
|
||||
"deploymentRegion": "us-central1",
|
||||
"projectIds": ["signoz-test-project"],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
PROVIDER_SERVICE_SPECS = [AWS_SERVICE_SPEC, GCP_SERVICE_SPEC]
|
||||
|
||||
provider_spec = pytest.mark.parametrize(
|
||||
"spec",
|
||||
PROVIDER_SERVICE_SPECS,
|
||||
ids=[s.id for s in PROVIDER_SERVICE_SPECS],
|
||||
)
|
||||
CLOUD_PROVIDER = "aws"
|
||||
SERVICE_ID = "rds"
|
||||
|
||||
|
||||
def test_apply_license(
|
||||
@@ -55,18 +26,16 @@ def test_apply_license(
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_list_services_without_account(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
spec: ProviderServiceSpec,
|
||||
) -> None:
|
||||
"""List the cloud provider's supported services"""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/services"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/services"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
@@ -84,37 +53,35 @@ def test_list_services_without_account(
|
||||
assert "icon" in service, "Service should have 'icon' field"
|
||||
assert "enabled" in service, "Service should have 'enabled' field"
|
||||
|
||||
listed_ids = {s["id"] for s in data["services"]}
|
||||
assert spec.service_id in listed_ids, f"'{spec.service_id}' should be listed for {spec.provider}"
|
||||
|
||||
EC2_SERVICE_ID = "ec2"
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_list_account_services(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
spec: ProviderServiceSpec,
|
||||
) -> None:
|
||||
"""ListAccountServicesMetadata reflects enabled state per service."""
|
||||
"""ListAccountServicesMetadata reflects enabled state after enabling a service."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
account = create_cloud_integration_account(admin_token, spec.provider, config=spec.account_config)
|
||||
account = create_cloud_integration_account(admin_token, CLOUD_PROVIDER)
|
||||
account_id = account["id"]
|
||||
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, spec.provider, account_id, str(uuid.uuid4()))
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, CLOUD_PROVIDER, account_id, str(uuid.uuid4()))
|
||||
assert checkin.status_code == HTTPStatus.OK, f"Check-in failed: {checkin.text}"
|
||||
|
||||
put_response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}/services/{spec.service_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}/services/{EC2_SERVICE_ID}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json={"config": spec.build_service_config(True)},
|
||||
json={"config": {"aws": {"metrics": {"enabled": True}, "logs": {"enabled": True}}}},
|
||||
timeout=10,
|
||||
)
|
||||
assert put_response.status_code == HTTPStatus.NO_CONTENT, f"Enable {spec.service_id} failed: {put_response.status_code}: {put_response.text}"
|
||||
assert put_response.status_code == HTTPStatus.NO_CONTENT, f"Enable ec2 failed: {put_response.status_code}: {put_response.text}"
|
||||
|
||||
list_response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}/services"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}/services"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
@@ -125,28 +92,21 @@ def test_list_account_services(
|
||||
assert isinstance(data["services"], list), "services should be a list"
|
||||
assert len(data["services"]) > 0, "services list should be non-empty"
|
||||
|
||||
enabled_service = next((s for s in data["services"] if s["id"] == spec.service_id), None)
|
||||
assert enabled_service is not None, f"Service '{spec.service_id}' not found in services list"
|
||||
assert enabled_service["enabled"] is True, f"Service should be enabled, got: {enabled_service['enabled']}"
|
||||
|
||||
# The listing must report state per service, not blanket-enable or echo the write.
|
||||
untouched_service = next((s for s in data["services"] if s["id"] != spec.service_id), None)
|
||||
assert untouched_service is not None, "Expected more than one service in the listing"
|
||||
assert untouched_service["enabled"] is False, f"Service '{untouched_service['id']}' was never enabled, got: {untouched_service['enabled']}"
|
||||
ec2_service = next((s for s in data["services"] if s["id"] == EC2_SERVICE_ID), None)
|
||||
assert ec2_service is not None, f"EC2 service '{EC2_SERVICE_ID}' not found in services list"
|
||||
assert ec2_service["enabled"] is True, f"EC2 service should be enabled, got: {ec2_service['enabled']}"
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_get_service_details_without_account(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
spec: ProviderServiceSpec,
|
||||
) -> None:
|
||||
"""Get full service definition without specifying an account."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/services/{spec.service_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/services/{SERVICE_ID}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
@@ -154,36 +114,31 @@ def test_get_service_details_without_account(
|
||||
assert response.status_code == HTTPStatus.OK, f"Expected 200, got {response.status_code}"
|
||||
|
||||
data = response.json()["data"]
|
||||
assert data["id"] == spec.service_id, f"id should be '{spec.service_id}'"
|
||||
assert data["id"] == SERVICE_ID, f"id should be '{SERVICE_ID}'"
|
||||
assert "title" in data, "Service should have 'title'"
|
||||
assert "overview" in data, "Service should have 'overview' (markdown)"
|
||||
assert "assets" in data, "Service should have 'assets'"
|
||||
assert isinstance(data["assets"]["dashboards"], list), "assets.dashboards should be a list"
|
||||
assert data["cloudIntegrationService"] is None, "cloudIntegrationService should be null without account context"
|
||||
|
||||
assert data["supportedSignals"]["metrics"] is True, "metrics should be a supported signal"
|
||||
assert data["supportedSignals"]["logs"] is spec.supports_logs, f"logs support should be {spec.supports_logs} for {spec.provider}"
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_get_account_service(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
spec: ProviderServiceSpec,
|
||||
) -> None:
|
||||
"""Get service for a specific account — all disabled by default."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
account = create_cloud_integration_account(admin_token, spec.provider, config=spec.account_config)
|
||||
account = create_cloud_integration_account(admin_token, CLOUD_PROVIDER)
|
||||
account_id = account["id"]
|
||||
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, spec.provider, account_id, str(uuid.uuid4()))
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, CLOUD_PROVIDER, account_id, str(uuid.uuid4()))
|
||||
assert checkin.status_code == HTTPStatus.OK, f"Check-in failed: {checkin.text}"
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}/services/{spec.service_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}/services/{SERVICE_ID}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
@@ -191,22 +146,20 @@ def test_get_account_service(
|
||||
assert response.status_code == HTTPStatus.OK, f"Expected 200, got {response.status_code}"
|
||||
|
||||
data = response.json()["data"]
|
||||
assert data["id"] == spec.service_id, f"id should be '{spec.service_id}'"
|
||||
assert data["id"] == SERVICE_ID, f"id should be '{SERVICE_ID}'"
|
||||
assert data["cloudIntegrationService"] is None, "cloudIntegrationService should be null before any config is set"
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_get_service_not_found(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
spec: ProviderServiceSpec,
|
||||
) -> None:
|
||||
"""Get a non-existent service ID returns 400 (invalid service ID is a bad request)."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/services/non-existent-service"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/services/non-existent-service"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
@@ -214,34 +167,32 @@ def test_get_service_not_found(
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, f"Expected 400, got {response.status_code}"
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_update_service_config(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
spec: ProviderServiceSpec,
|
||||
) -> None:
|
||||
"""Enable a service and verify the config is persisted via GET."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
account = create_cloud_integration_account(admin_token, spec.provider, config=spec.account_config)
|
||||
account = create_cloud_integration_account(admin_token, CLOUD_PROVIDER)
|
||||
account_id = account["id"]
|
||||
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, spec.provider, account_id, str(uuid.uuid4()))
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, CLOUD_PROVIDER, account_id, str(uuid.uuid4()))
|
||||
assert checkin.status_code == HTTPStatus.OK, f"Check-in failed: {checkin.text}"
|
||||
|
||||
put_response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}/services/{spec.service_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}/services/{SERVICE_ID}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json={"config": spec.build_service_config(True)},
|
||||
json={"config": {"aws": {"metrics": {"enabled": True}, "logs": {"enabled": True}}}},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert put_response.status_code == HTTPStatus.NO_CONTENT, f"Expected 204, got {put_response.status_code}: {put_response.text}"
|
||||
|
||||
get_response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}/services/{spec.service_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}/services/{SERVICE_ID}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
@@ -250,39 +201,33 @@ def test_update_service_config(
|
||||
data = get_response.json()["data"]
|
||||
svc = data["cloudIntegrationService"]
|
||||
assert svc is not None, "cloudIntegrationService should be non-null after UpdateService"
|
||||
assert svc["config"][spec.provider]["metrics"]["enabled"] is True, "metrics should be enabled"
|
||||
assert svc["config"]["aws"]["metrics"]["enabled"] is True, "metrics should be enabled"
|
||||
assert svc["config"]["aws"]["logs"]["enabled"] is True, "logs should be enabled"
|
||||
assert svc["cloudIntegrationId"] == account_id, "cloudIntegrationId should match the account"
|
||||
|
||||
if spec.supports_logs:
|
||||
assert svc["config"][spec.provider]["logs"]["enabled"] is True, "logs should be enabled"
|
||||
else:
|
||||
assert svc["config"][spec.provider].get("logs") is None, f"logs should not be stored for {spec.provider}, got: {svc['config'][spec.provider]}"
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_update_service_config_disable(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
spec: ProviderServiceSpec,
|
||||
) -> None:
|
||||
"""Enable then disable a service — config change is persisted."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
account = create_cloud_integration_account(admin_token, spec.provider, config=spec.account_config)
|
||||
account = create_cloud_integration_account(admin_token, CLOUD_PROVIDER)
|
||||
account_id = account["id"]
|
||||
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, spec.provider, account_id, str(uuid.uuid4()))
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, CLOUD_PROVIDER, account_id, str(uuid.uuid4()))
|
||||
assert checkin.status_code == HTTPStatus.OK, f"Check-in failed: {checkin.text}"
|
||||
|
||||
endpoint = signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}/services/{spec.service_id}")
|
||||
endpoint = signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}/services/{SERVICE_ID}")
|
||||
|
||||
# Enable
|
||||
r = requests.put(
|
||||
endpoint,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json={"config": spec.build_service_config(True)},
|
||||
json={"config": {"aws": {"metrics": {"enabled": True}, "logs": {"enabled": True}}}},
|
||||
timeout=10,
|
||||
)
|
||||
assert r.status_code == HTTPStatus.NO_CONTENT, f"Enable failed: {r.status_code}: {r.text}"
|
||||
@@ -291,13 +236,13 @@ def test_update_service_config_disable(
|
||||
r = requests.put(
|
||||
endpoint,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json={"config": spec.build_service_config(False)},
|
||||
json={"config": {"aws": {"metrics": {"enabled": False}, "logs": {"enabled": False}}}},
|
||||
timeout=10,
|
||||
)
|
||||
assert r.status_code == HTTPStatus.NO_CONTENT, f"Disable failed: {r.status_code}: {r.text}"
|
||||
|
||||
get_response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}/services/{spec.service_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}/services/{SERVICE_ID}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
@@ -305,57 +250,28 @@ def test_update_service_config_disable(
|
||||
assert get_response.status_code == HTTPStatus.OK
|
||||
svc = get_response.json()["data"]["cloudIntegrationService"]
|
||||
assert svc is not None, "cloudIntegrationService should still be present after disable"
|
||||
assert svc["config"][spec.provider]["metrics"]["enabled"] is False, "metrics should be disabled"
|
||||
|
||||
if spec.supports_logs:
|
||||
assert svc["config"][spec.provider]["logs"]["enabled"] is False, "logs should be disabled"
|
||||
assert svc["config"]["aws"]["metrics"]["enabled"] is False, "metrics should be disabled"
|
||||
assert svc["config"]["aws"]["logs"]["enabled"] is False, "logs should be disabled"
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_update_service_account_not_found(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
spec: ProviderServiceSpec,
|
||||
) -> None:
|
||||
"""PUT with a non-existent account UUID returns 404."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{uuid.uuid4()}/services/{spec.service_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{uuid.uuid4()}/services/{SERVICE_ID}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json={"config": spec.build_service_config(True)},
|
||||
json={"config": {"aws": {"metrics": {"enabled": True}}}},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.NOT_FOUND, f"Expected 404, got {response.status_code}"
|
||||
|
||||
|
||||
def test_update_gcp_service_without_metrics_config(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
) -> None:
|
||||
"""GCP services support metrics only, so a config omitting metrics is rejected."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
account = create_cloud_integration_account(admin_token, "gcp", config=GCP_SERVICE_SPEC.account_config)
|
||||
account_id = account["id"]
|
||||
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, "gcp", account_id, str(uuid.uuid4()))
|
||||
assert checkin.status_code == HTTPStatus.OK, f"Check-in failed: {checkin.text}"
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/gcp/accounts/{account_id}/services/{GCP_SERVICE_SPEC.service_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json={"config": {"gcp": {"logs": {"enabled": True}}}},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, f"Expected 400 when metrics config is missing, got {response.status_code}: {response.text}"
|
||||
|
||||
|
||||
def test_list_services_unsupported_provider(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
@@ -373,32 +289,30 @@ def test_list_services_unsupported_provider(
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, f"Expected 400, got {response.status_code}"
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_list_services_account_removed(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
spec: ProviderServiceSpec,
|
||||
) -> None:
|
||||
"""List services for a deleted account returns 404."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
account = create_cloud_integration_account(admin_token, spec.provider, config=spec.account_config)
|
||||
account = create_cloud_integration_account(admin_token, CLOUD_PROVIDER)
|
||||
account_id = account["id"]
|
||||
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, spec.provider, account_id, str(uuid.uuid4()))
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, CLOUD_PROVIDER, account_id, str(uuid.uuid4()))
|
||||
assert checkin.status_code == HTTPStatus.OK, f"Check-in failed: {checkin.text}"
|
||||
|
||||
delete_response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
assert delete_response.status_code == HTTPStatus.NO_CONTENT, f"Expected 204 on delete, got {delete_response.status_code}"
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}/services"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}/services"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
@@ -406,32 +320,30 @@ def test_list_services_account_removed(
|
||||
assert response.status_code == HTTPStatus.NOT_FOUND, f"Expected 404, got {response.status_code}"
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_get_service_details_account_removed(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
spec: ProviderServiceSpec,
|
||||
) -> None:
|
||||
"""Get service details for a deleted account returns 404."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
account = create_cloud_integration_account(admin_token, spec.provider, config=spec.account_config)
|
||||
account = create_cloud_integration_account(admin_token, CLOUD_PROVIDER)
|
||||
account_id = account["id"]
|
||||
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, spec.provider, account_id, str(uuid.uuid4()))
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, CLOUD_PROVIDER, account_id, str(uuid.uuid4()))
|
||||
assert checkin.status_code == HTTPStatus.OK, f"Check-in failed: {checkin.text}"
|
||||
|
||||
delete_response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
assert delete_response.status_code == HTTPStatus.NO_CONTENT, f"Expected 204 on delete, got {delete_response.status_code}"
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}/services/{spec.service_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}/services/{SERVICE_ID}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
@@ -439,68 +351,64 @@ def test_get_service_details_account_removed(
|
||||
assert response.status_code == HTTPStatus.NOT_FOUND, f"Expected 404, got {response.status_code}"
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_update_service_account_removed(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
spec: ProviderServiceSpec,
|
||||
) -> None:
|
||||
"""PUT service config for a deleted account returns 404."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
account = create_cloud_integration_account(admin_token, spec.provider, config=spec.account_config)
|
||||
account = create_cloud_integration_account(admin_token, CLOUD_PROVIDER)
|
||||
account_id = account["id"]
|
||||
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, spec.provider, account_id, str(uuid.uuid4()))
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, CLOUD_PROVIDER, account_id, str(uuid.uuid4()))
|
||||
assert checkin.status_code == HTTPStatus.OK, f"Check-in failed: {checkin.text}"
|
||||
|
||||
delete_response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
assert delete_response.status_code == HTTPStatus.NO_CONTENT, f"Expected 204 on delete, got {delete_response.status_code}"
|
||||
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}/services/{spec.service_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}/services/{SERVICE_ID}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json={"config": spec.build_service_config(True)},
|
||||
json={"config": {"aws": {"metrics": {"enabled": True}}}},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.NOT_FOUND, f"Expected 404, got {response.status_code}"
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_enable_metrics_provisions_dashboards(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
spec: ProviderServiceSpec,
|
||||
) -> None:
|
||||
"""Enabling metrics provisions dashboards visible in GetService and present in the DB."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
account = create_cloud_integration_account(admin_token, spec.provider, config=spec.account_config)
|
||||
account = create_cloud_integration_account(admin_token, CLOUD_PROVIDER)
|
||||
account_id = account["id"]
|
||||
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, spec.provider, account_id, str(uuid.uuid4()))
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, CLOUD_PROVIDER, account_id, str(uuid.uuid4()))
|
||||
assert checkin.status_code == HTTPStatus.OK, f"Check-in failed: {checkin.text}"
|
||||
|
||||
put_response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}/services/{spec.service_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}/services/{SERVICE_ID}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json={"config": spec.build_service_config(True, logs_enabled=False)},
|
||||
json={"config": {"aws": {"metrics": {"enabled": True}, "logs": {"enabled": False}}}},
|
||||
timeout=10,
|
||||
)
|
||||
assert put_response.status_code == HTTPStatus.NO_CONTENT, f"Expected 204, got {put_response.status_code}: {put_response.text}"
|
||||
|
||||
# Assertion 1: GetService returns provisioned dashboard UUIDs
|
||||
get_svc_response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}/services/{spec.service_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}/services/{SERVICE_ID}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
@@ -509,7 +417,7 @@ def test_enable_metrics_provisions_dashboards(
|
||||
data = get_svc_response.json()["data"]
|
||||
svc = data["cloudIntegrationService"]
|
||||
assert svc is not None, "cloudIntegrationService should be non-null after enabling metrics"
|
||||
assert svc["config"][spec.provider]["metrics"]["enabled"] is True
|
||||
assert svc["config"]["aws"]["metrics"]["enabled"] is True
|
||||
|
||||
dashboards_in_service = data["assets"]["dashboards"]
|
||||
assert isinstance(dashboards_in_service, list) and len(dashboards_in_service) > 0, "assets.dashboards should be non-empty after enabling metrics"
|
||||
@@ -537,37 +445,35 @@ def test_enable_metrics_provisions_dashboards(
|
||||
assert provisioned_ids == db_ids, f"Dashboards {provisioned_ids - db_ids} are missing from the DB"
|
||||
|
||||
|
||||
@provider_spec
|
||||
def test_disable_metrics_deprovisions_dashboards(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
spec: ProviderServiceSpec,
|
||||
) -> None:
|
||||
"""Disabling metrics removes provisioned dashboards from both GetService and the dashboards list."""
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
account = create_cloud_integration_account(admin_token, spec.provider, config=spec.account_config)
|
||||
account = create_cloud_integration_account(admin_token, CLOUD_PROVIDER)
|
||||
account_id = account["id"]
|
||||
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, spec.provider, account_id, str(uuid.uuid4()))
|
||||
checkin = simulate_agent_checkin(signoz, admin_token, CLOUD_PROVIDER, account_id, str(uuid.uuid4()))
|
||||
assert checkin.status_code == HTTPStatus.OK, f"Check-in failed: {checkin.text}"
|
||||
|
||||
endpoint = signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}/services/{spec.service_id}")
|
||||
endpoint = signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}/services/{SERVICE_ID}")
|
||||
|
||||
# Enable metrics to provision dashboards first
|
||||
enable_response = requests.put(
|
||||
endpoint,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json={"config": spec.build_service_config(True, logs_enabled=False)},
|
||||
json={"config": {"aws": {"metrics": {"enabled": True}, "logs": {"enabled": False}}}},
|
||||
timeout=10,
|
||||
)
|
||||
assert enable_response.status_code == HTTPStatus.NO_CONTENT, f"Enable failed: {enable_response.status_code}: {enable_response.text}"
|
||||
|
||||
# Capture the provisioned dashboard IDs before disabling
|
||||
get_svc_response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}/services/{spec.service_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}/services/{SERVICE_ID}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
@@ -579,14 +485,14 @@ def test_disable_metrics_deprovisions_dashboards(
|
||||
disable_response = requests.put(
|
||||
endpoint,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json={"config": spec.build_service_config(False)},
|
||||
json={"config": {"aws": {"metrics": {"enabled": False}, "logs": {"enabled": False}}}},
|
||||
timeout=10,
|
||||
)
|
||||
assert disable_response.status_code == HTTPStatus.NO_CONTENT, f"Disable failed: {disable_response.status_code}: {disable_response.text}"
|
||||
|
||||
# Assertion 1: GetService no longer returns UUID dashboard IDs
|
||||
get_svc_after = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{spec.provider}/accounts/{account_id}/services/{spec.service_id}"),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/cloud_integrations/{CLOUD_PROVIDER}/accounts/{account_id}/services/{SERVICE_ID}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user