Wire in auth
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-08-07 19:05:27 -04:00
parent 24cbea6e96
commit b614bb33a0
32 changed files with 1946 additions and 504 deletions

View File

@@ -30,13 +30,16 @@ services:
"--pg-password","happypostgresuser",
# other hosts
"--nats-host","nats:4222",
"--auth-rpc-host","authrpc:8081",
"--data-rpc-host","dataservice:9000",
# OIDC. The auth service is no longer on the request path: access tokens
# verify locally against the issuer's key set, so there is no auth rpc
# host any more. OIDC_ISSUER / OIDC_AUDIENCE / OIDC_CLIENT_ID come from
# the shared env file, as they already do for the developer portal and
# moderation.
]
env_file:
- /home/quat/auth-compose/strafesnet_staging.env
depends_on:
- authrpc
- nats
networks:
- maps-service-network

View File

@@ -1,4 +1,6 @@
package main
//go:generate go run github.com/swaggo/swag/cmd/swag@latest init -g ./cmd/maps-service/service.go
//go:generate go run github.com/ogen-go/ogen/cmd/ogen@latest --target pkg/api --clean openapi.yaml
// Pinned: `@latest` silently regenerates the whole API against whatever ogen
// released today. Keep this in step with the ogen version in go.mod.
//go:generate go run github.com/ogen-go/ogen/cmd/ogen@v1.23.0 --target pkg/api --clean openapi.yaml

4
go.mod
View File

@@ -51,9 +51,11 @@ require (
github.com/bytedance/sonic/loader v0.5.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.7 // indirect
github.com/coreos/go-oidc/v3 v3.20.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-openapi/jsonpointer v1.0.0 // indirect
github.com/go-openapi/jsonreference v1.0.0 // indirect
github.com/go-openapi/spec v0.22.6 // indirect
@@ -98,12 +100,14 @@ require (
golang.org/x/arch v0.29.0 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/tools v0.48.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260715232425-e75dac1f907d // indirect
google.golang.org/protobuf v1.36.11 // indirect
)
require (
git.itzana.me/strafesnet/auth-sdk v0.1.1
github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect

8
go.sum
View File

@@ -1,5 +1,7 @@
git.itzana.me/StrafesNET/dev-service v0.0.0-20260307022628-fe71e9dbcb0b h1:z0iQFOeFvr8dfsWI8tG3DqMyssHqjU13/p6L9Wrt4MM=
git.itzana.me/StrafesNET/dev-service v0.0.0-20260307022628-fe71e9dbcb0b/go.mod h1:KJal0K++M6HEzSry6JJ2iDPZtOQn5zSstNlDbU3X4Jg=
git.itzana.me/strafesnet/auth-sdk v0.1.1 h1:4Mlsnop3vdiJnun7548nrO7e6FuJNMBGf9T6Uhx9gCY=
git.itzana.me/strafesnet/auth-sdk v0.1.1/go.mod h1:BnwE/YFpXeaPaBNWcGkA2Z53k/qwqJp1uHOOkHhHh6U=
git.itzana.me/strafesnet/go-grpc v0.0.0-20260414205100-57b3171fc3f2 h1:AIJ6mUohEPblXKk+4oSnWpSokh8C6Jk9MjbzCvHXX1s=
git.itzana.me/strafesnet/go-grpc v0.0.0-20260414205100-57b3171fc3f2/go.mod h1:X7XTRUScRkBWq8q8bplbeso105RPDlnY7J6Wy1IwBMs=
git.itzana.me/strafesnet/utils v0.0.0-20220716194944-d8ca164052f9 h1:7lU6jyR7S7Rhh1dnUp7GyIRHUTBXZagw8F4n4hOyxLw=
@@ -56,6 +58,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
github.com/coreos/go-oidc/v3 v3.20.0 h1:EtE0WIBHk03N+DqGkY4+UONzzZHk7amKt6IyNd7OsZE=
github.com/coreos/go-oidc/v3 v3.20.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -83,6 +87,8 @@ github.com/go-faster/jx v1.2.0 h1:T2YHJPrFaYu21fJtUxC9GzmluKu8rVIFDwwGBKTDseI=
github.com/go-faster/jx v1.2.0/go.mod h1:UWLOVDmMG597a5tBFPLIWJdUxz5/2emOpfsj9Neg0PE=
github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -275,6 +281,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=

View File

@@ -27,7 +27,7 @@ tags:
- name: Users
description: User operations
security:
- cookieAuth: []
- bearerAuth: []
paths:
/stats:
get:
@@ -49,6 +49,31 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
/session/config:
get:
summary: Get the OIDC parameters the web client needs to sign in
description: >-
Unauthenticated by necessity: the client calls this before it has a
token, to discover which issuer and client id to use. Contains no
secrets — all three values are public parameters of the authorization
code flow and are visible in the sign-in redirect.
operationId: sessionConfig
tags:
- Session
security: []
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: "#/components/schemas/OIDCConfig"
default:
description: General Error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/session/user:
get:
summary: Get information about the currently logged in user
@@ -1776,10 +1801,15 @@ paths:
$ref: "#/components/schemas/Error"
components:
securitySchemes:
cookieAuth:
type: apiKey
in: cookie
name: session_id
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: >-
StrafesNET OIDC access token, as issued to the maps web client and
already used by the developer portal and moderation. Roles ride in the
token's roles claim, so no session lookup is needed to authorize a
request.
parameters:
MapID:
name: MapID
@@ -1944,6 +1974,29 @@ components:
AvatarURL:
type: string
maxLength: 256
OIDCConfig:
description: >-
Public parameters of the authorization code flow. Field names match the
keys oidc-client-ts expects, and the shape the developer portal and
moderation already serve, so the web clients share one config loader.
required:
- authority
- client_id
- audience
type: object
properties:
authority:
description: Issuer URL that mints and signs access tokens.
type: string
maxLength: 256
client_id:
description: Public client id registered for the maps web application.
type: string
maxLength: 128
audience:
description: Audience the maps API requires on an access token.
type: string
maxLength: 128
Map:
required:
- ID

View File

@@ -380,6 +380,14 @@ type Invoker interface {
//
// POST /maps-admin/seed-combobulator
SeedCombobulator(ctx context.Context) error
// SessionConfig invokes sessionConfig operation.
//
// Unauthenticated by necessity: the client calls this before it has a token, to discover which issuer
// and client id to use. Contains no secrets — all three values are public parameters of the
// authorization code flow and are visible in the sign-in redirect.
//
// GET /session/config
SessionConfig(ctx context.Context) (*OIDCConfig, error)
// SessionRoles invokes sessionRoles operation.
//
// Get list of roles for the current session.
@@ -563,14 +571,14 @@ func (c *Client) sendActionMapfixAccepted(ctx context.Context, params ActionMapf
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionMapfixAcceptedOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionMapfixAcceptedOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -695,14 +703,14 @@ func (c *Client) sendActionMapfixReject(ctx context.Context, params ActionMapfix
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionMapfixRejectOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionMapfixRejectOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -827,14 +835,14 @@ func (c *Client) sendActionMapfixRequestChanges(ctx context.Context, params Acti
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionMapfixRequestChangesOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionMapfixRequestChangesOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -960,14 +968,14 @@ func (c *Client) sendActionMapfixResetSubmitting(ctx context.Context, params Act
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionMapfixResetSubmittingOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionMapfixResetSubmittingOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -1092,14 +1100,14 @@ func (c *Client) sendActionMapfixRetryValidate(ctx context.Context, params Actio
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionMapfixRetryValidateOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionMapfixRetryValidateOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -1224,14 +1232,14 @@ func (c *Client) sendActionMapfixRevoke(ctx context.Context, params ActionMapfix
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionMapfixRevokeOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionMapfixRevokeOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -1356,14 +1364,14 @@ func (c *Client) sendActionMapfixTriggerRelease(ctx context.Context, params Acti
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionMapfixTriggerReleaseOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionMapfixTriggerReleaseOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -1488,14 +1496,14 @@ func (c *Client) sendActionMapfixTriggerSubmit(ctx context.Context, params Actio
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionMapfixTriggerSubmitOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionMapfixTriggerSubmitOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -1620,14 +1628,14 @@ func (c *Client) sendActionMapfixTriggerSubmitUnchecked(ctx context.Context, par
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionMapfixTriggerSubmitUncheckedOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionMapfixTriggerSubmitUncheckedOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -1752,14 +1760,14 @@ func (c *Client) sendActionMapfixTriggerUpload(ctx context.Context, params Actio
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionMapfixTriggerUploadOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionMapfixTriggerUploadOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -1884,14 +1892,14 @@ func (c *Client) sendActionMapfixTriggerValidate(ctx context.Context, params Act
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionMapfixTriggerValidateOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionMapfixTriggerValidateOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -2016,14 +2024,14 @@ func (c *Client) sendActionMapfixUploaded(ctx context.Context, params ActionMapf
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionMapfixUploadedOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionMapfixUploadedOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -2148,14 +2156,14 @@ func (c *Client) sendActionMapfixValidated(ctx context.Context, params ActionMap
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionMapfixValidatedOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionMapfixValidatedOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -2280,14 +2288,14 @@ func (c *Client) sendActionSubmissionAccepted(ctx context.Context, params Action
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionSubmissionAcceptedOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionSubmissionAcceptedOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -2412,14 +2420,14 @@ func (c *Client) sendActionSubmissionReject(ctx context.Context, params ActionSu
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionSubmissionRejectOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionSubmissionRejectOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -2544,14 +2552,14 @@ func (c *Client) sendActionSubmissionRequestChanges(ctx context.Context, params
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionSubmissionRequestChangesOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionSubmissionRequestChangesOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -2677,14 +2685,14 @@ func (c *Client) sendActionSubmissionResetSubmitting(ctx context.Context, params
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionSubmissionResetSubmittingOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionSubmissionResetSubmittingOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -2809,14 +2817,14 @@ func (c *Client) sendActionSubmissionRetryValidate(ctx context.Context, params A
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionSubmissionRetryValidateOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionSubmissionRetryValidateOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -2941,14 +2949,14 @@ func (c *Client) sendActionSubmissionRevoke(ctx context.Context, params ActionSu
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionSubmissionRevokeOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionSubmissionRevokeOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -3073,14 +3081,14 @@ func (c *Client) sendActionSubmissionTriggerSubmit(ctx context.Context, params A
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionSubmissionTriggerSubmitOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionSubmissionTriggerSubmitOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -3205,14 +3213,14 @@ func (c *Client) sendActionSubmissionTriggerSubmitUnchecked(ctx context.Context,
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionSubmissionTriggerSubmitUncheckedOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionSubmissionTriggerSubmitUncheckedOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -3337,14 +3345,14 @@ func (c *Client) sendActionSubmissionTriggerUpload(ctx context.Context, params A
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionSubmissionTriggerUploadOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionSubmissionTriggerUploadOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -3469,14 +3477,14 @@ func (c *Client) sendActionSubmissionTriggerValidate(ctx context.Context, params
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionSubmissionTriggerValidateOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionSubmissionTriggerValidateOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -3602,14 +3610,14 @@ func (c *Client) sendActionSubmissionValidated(ctx context.Context, params Actio
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ActionSubmissionValidatedOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ActionSubmissionValidatedOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -3983,14 +3991,14 @@ func (c *Client) sendCombobulateMap(ctx context.Context, params CombobulateMapPa
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, CombobulateMapOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, CombobulateMapOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -4099,14 +4107,14 @@ func (c *Client) sendCreateMapfix(ctx context.Context, request *MapfixTriggerCre
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, CreateMapfixOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, CreateMapfixOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -4234,14 +4242,14 @@ func (c *Client) sendCreateMapfixAuditComment(ctx context.Context, request Creat
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, CreateMapfixAuditCommentOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, CreateMapfixAuditCommentOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -4350,14 +4358,14 @@ func (c *Client) sendCreateScript(ctx context.Context, request *ScriptCreate) (r
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, CreateScriptOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, CreateScriptOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -4466,14 +4474,14 @@ func (c *Client) sendCreateScriptPolicy(ctx context.Context, request *ScriptPoli
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, CreateScriptPolicyOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, CreateScriptPolicyOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -4582,14 +4590,14 @@ func (c *Client) sendCreateSubmission(ctx context.Context, request *SubmissionTr
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, CreateSubmissionOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, CreateSubmissionOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -4698,14 +4706,14 @@ func (c *Client) sendCreateSubmissionAdmin(ctx context.Context, request *Submiss
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, CreateSubmissionAdminOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, CreateSubmissionAdminOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -4833,14 +4841,14 @@ func (c *Client) sendCreateSubmissionAuditComment(ctx context.Context, request C
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, CreateSubmissionAuditCommentOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, CreateSubmissionAuditCommentOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -4964,14 +4972,14 @@ func (c *Client) sendDeleteMapfix(ctx context.Context, params DeleteMapfixParams
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, DeleteMapfixOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, DeleteMapfixOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -5095,14 +5103,14 @@ func (c *Client) sendDeleteScript(ctx context.Context, params DeleteScriptParams
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, DeleteScriptOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, DeleteScriptOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -5226,14 +5234,14 @@ func (c *Client) sendDeleteScriptPolicy(ctx context.Context, params DeleteScript
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, DeleteScriptPolicyOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, DeleteScriptPolicyOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -5357,14 +5365,14 @@ func (c *Client) sendDeleteSubmission(ctx context.Context, params DeleteSubmissi
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, DeleteSubmissionOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, DeleteSubmissionOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -5489,14 +5497,14 @@ func (c *Client) sendDownloadMapAsset(ctx context.Context, params DownloadMapAss
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, DownloadMapAssetOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, DownloadMapAssetOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -5935,14 +5943,14 @@ func (c *Client) sendGetOperation(ctx context.Context, params GetOperationParams
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, GetOperationOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, GetOperationOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -7876,14 +7884,14 @@ func (c *Client) sendReleaseSubmissions(ctx context.Context, request []ReleaseIn
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, ReleaseSubmissionsOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, ReleaseSubmissionsOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -7989,14 +7997,14 @@ func (c *Client) sendSeedCombobulator(ctx context.Context) (res *SeedCombobulato
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, SeedCombobulatorOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, SeedCombobulatorOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -8041,6 +8049,88 @@ func (c *Client) sendSeedCombobulator(ctx context.Context) (res *SeedCombobulato
return result, nil
}
// SessionConfig invokes sessionConfig operation.
//
// Unauthenticated by necessity: the client calls this before it has a token, to discover which issuer
// and client id to use. Contains no secrets — all three values are public parameters of the
// authorization code flow and are visible in the sign-in redirect.
//
// GET /session/config
func (c *Client) SessionConfig(ctx context.Context) (*OIDCConfig, error) {
res, err := c.sendSessionConfig(ctx)
return res, err
}
func (c *Client) sendSessionConfig(ctx context.Context) (res *OIDCConfig, err error) {
otelAttrs := []attribute.KeyValue{
otelogen.OperationID("sessionConfig"),
semconv.HTTPRequestMethodKey.String("GET"),
semconv.URLTemplateKey.String("/session/config"),
}
otelAttrs = append(otelAttrs, c.cfg.Attributes...)
// Run stopwatch.
startTime := time.Now()
defer func() {
// Use floating point division here for higher precision (instead of Millisecond method).
elapsedDuration := time.Since(startTime)
c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...))
}()
// Increment request counter.
c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
// Start a span for this request.
ctx, span := c.cfg.Tracer.Start(ctx, SessionConfigOperation,
trace.WithAttributes(otelAttrs...),
clientSpanKind,
)
// Track stage for error reporting.
var stage string
defer func() {
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, stage)
c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...))
}
span.End()
}()
stage = "BuildURL"
u := uri.Clone(c.requestURL(ctx))
var pathParts [1]string
pathParts[0] = "/session/config"
uri.AddPathParts(u, pathParts[:]...)
stage = "EncodeRequest"
r, err := ht.NewRequest(ctx, "GET", u)
if err != nil {
return res, errors.Wrap(err, "create request")
}
stage = "SendRequest"
resp, err := c.cfg.Client.Do(r)
if err != nil {
return res, errors.Wrap(err, "do request")
}
body := resp.Body
defer func() {
// Drain the body to EOF before closing, so the underlying
// connection can be reused by the Transport regardless of the
// response status code. See https://github.com/ogen-go/ogen/issues/1670.
_, _ = io.Copy(io.Discard, body)
_ = body.Close()
}()
stage = "DecodeResponse"
result, err := decodeSessionConfigResponse(resp)
if err != nil {
return res, errors.Wrap(err, "decode response")
}
return result, nil
}
// SessionRoles invokes sessionRoles operation.
//
// Get list of roles for the current session.
@@ -8102,14 +8192,14 @@ func (c *Client) sendSessionRoles(ctx context.Context) (res *Roles, err error) {
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, SessionRolesOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, SessionRolesOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -8215,14 +8305,14 @@ func (c *Client) sendSessionUser(ctx context.Context) (res *User, err error) {
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, SessionUserOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, SessionUserOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -8328,14 +8418,14 @@ func (c *Client) sendSessionValidate(ctx context.Context) (res bool, err error)
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, SessionValidateOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, SessionValidateOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -8460,14 +8550,14 @@ func (c *Client) sendSetMapfixCompleted(ctx context.Context, params SetMapfixCom
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, SetMapfixCompletedOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, SetMapfixCompletedOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -8592,14 +8682,14 @@ func (c *Client) sendSetSubmissionCompleted(ctx context.Context, params SetSubmi
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, SetSubmissionCompletedOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, SetSubmissionCompletedOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -8727,14 +8817,14 @@ func (c *Client) sendUpdateMapfixDescription(ctx context.Context, request Update
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, UpdateMapfixDescriptionOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, UpdateMapfixDescriptionOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -8891,14 +8981,14 @@ func (c *Client) sendUpdateMapfixModel(ctx context.Context, params UpdateMapfixM
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, UpdateMapfixModelOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, UpdateMapfixModelOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -9025,14 +9115,14 @@ func (c *Client) sendUpdateScript(ctx context.Context, request *ScriptUpdate, pa
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, UpdateScriptOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, UpdateScriptOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -9159,14 +9249,14 @@ func (c *Client) sendUpdateScriptPolicy(ctx context.Context, request *ScriptPoli
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, UpdateScriptPolicyOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, UpdateScriptPolicyOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}
@@ -9323,14 +9413,14 @@ func (c *Client) sendUpdateSubmissionModel(ctx context.Context, params UpdateSub
type bitset = [1]uint8
var satisfied bitset
{
stage = "Security:CookieAuth"
switch err := c.securityCookieAuth(ctx, UpdateSubmissionModelOperation, r); {
stage = "Security:BearerAuth"
switch err := c.securityBearerAuth(ctx, UpdateSubmissionModelOperation, r); {
case err == nil: // if NO error
satisfied[0] |= 1 << 0
case errors.Is(err, ogenerrors.ErrSkipClientSecurity):
// Skip this security.
default:
return res, errors.Wrap(err, "security \"CookieAuth\"")
return res, errors.Wrap(err, "security \"BearerAuth\"")
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2069,6 +2069,136 @@ func (s *Mapfixes) UnmarshalJSON(data []byte) error {
return s.Decode(d)
}
// Encode implements json.Marshaler.
func (s *OIDCConfig) Encode(e *jx.Encoder) {
e.ObjStart()
s.encodeFields(e)
e.ObjEnd()
}
// encodeFields encodes fields.
func (s *OIDCConfig) encodeFields(e *jx.Encoder) {
{
e.FieldStart("authority")
e.Str(s.Authority)
}
{
e.FieldStart("client_id")
e.Str(s.ClientID)
}
{
e.FieldStart("audience")
e.Str(s.Audience)
}
}
var jsonFieldsNameOfOIDCConfig = [3]string{
0: "authority",
1: "client_id",
2: "audience",
}
// Decode decodes OIDCConfig from json.
func (s *OIDCConfig) Decode(d *jx.Decoder) error {
if s == nil {
return errors.New("invalid: unable to decode OIDCConfig to nil")
}
var requiredBitSet [1]uint8
if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error {
switch string(k) {
case "authority":
requiredBitSet[0] |= 1 << 0
if err := func() error {
v, err := d.Str()
s.Authority = string(v)
if err != nil {
return err
}
return nil
}(); err != nil {
return errors.Wrap(err, "decode field \"authority\"")
}
case "client_id":
requiredBitSet[0] |= 1 << 1
if err := func() error {
v, err := d.Str()
s.ClientID = string(v)
if err != nil {
return err
}
return nil
}(); err != nil {
return errors.Wrap(err, "decode field \"client_id\"")
}
case "audience":
requiredBitSet[0] |= 1 << 2
if err := func() error {
v, err := d.Str()
s.Audience = string(v)
if err != nil {
return err
}
return nil
}(); err != nil {
return errors.Wrap(err, "decode field \"audience\"")
}
default:
return d.Skip()
}
return nil
}); err != nil {
return errors.Wrap(err, "decode OIDCConfig")
}
// Validate required fields.
var failures []validate.FieldError
for i, mask := range [1]uint8{
0b00000111,
} {
if result := (requiredBitSet[i] & mask) ^ mask; result != 0 {
// Mask only required fields and check equality to mask using XOR.
//
// If XOR result is not zero, result is not equal to expected, so some fields are missed.
// Bits of fields which would be set are actually bits of missed fields.
missed := bits.OnesCount8(result)
for bitN := 0; bitN < missed; bitN++ {
bitIdx := bits.TrailingZeros8(result)
fieldIdx := i*8 + bitIdx
var name string
if fieldIdx < len(jsonFieldsNameOfOIDCConfig) {
name = jsonFieldsNameOfOIDCConfig[fieldIdx]
} else {
name = strconv.Itoa(fieldIdx)
}
failures = append(failures, validate.FieldError{
Name: name,
Error: validate.ErrFieldRequired,
})
// Reset bit.
result &^= 1 << bitIdx
}
}
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}
// MarshalJSON implements stdjson.Marshaler.
func (s *OIDCConfig) MarshalJSON() ([]byte, error) {
e := jx.Encoder{}
s.Encode(&e)
return e.Bytes(), nil
}
// UnmarshalJSON implements stdjson.Unmarshaler.
func (s *OIDCConfig) UnmarshalJSON(data []byte) error {
d := jx.DecodeBytes(data)
return s.Decode(d)
}
// Encode implements json.Marshaler.
func (s *Operation) Encode(e *jx.Encoder) {
e.ObjStart()

View File

@@ -64,6 +64,7 @@ const (
ListSubmissionsOperation OperationName = "ListSubmissions"
ReleaseSubmissionsOperation OperationName = "ReleaseSubmissions"
SeedCombobulatorOperation OperationName = "SeedCombobulator"
SessionConfigOperation OperationName = "SessionConfig"
SessionRolesOperation OperationName = "SessionRoles"
SessionUserOperation OperationName = "SessionUser"
SessionValidateOperation OperationName = "SessionValidate"

View File

@@ -4632,6 +4632,107 @@ func decodeSeedCombobulatorResponse(resp *http.Response) (res *SeedCombobulatorN
return res, errors.Wrap(defRes, "error")
}
func decodeSessionConfigResponse(resp *http.Response) (res *OIDCConfig, _ error) {
switch resp.StatusCode {
case 200:
// Code 200.
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response OIDCConfig
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &response, nil
default:
return res, validate.InvalidContentType(ct)
}
}
// Convenient error response.
defRes, err := func() (res *ErrorStatusCode, err error) {
ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return res, errors.Wrap(err, "parse media type")
}
switch {
case ct == "application/json":
buf, err := io.ReadAll(resp.Body)
if err != nil {
return res, err
}
d := jx.DecodeBytes(buf)
var response Error
if err := func() error {
if err := response.Decode(d); err != nil {
return err
}
if err := d.Skip(); err != io.EOF {
return errors.New("unexpected trailing data")
}
return nil
}(); err != nil {
err = &ogenerrors.DecodeBodyError{
ContentType: ct,
Body: buf,
Err: err,
}
return res, err
}
// Validate response.
if err := func() error {
if err := response.Validate(); err != nil {
return err
}
return nil
}(); err != nil {
return res, errors.Wrap(err, "validate")
}
return &ErrorStatusCode{
StatusCode: resp.StatusCode,
Response: response,
}, nil
default:
return res, validate.InvalidContentType(ct)
}
}()
if err != nil {
return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode)
}
return res, errors.Wrap(defRes, "error")
}
func decodeSessionRolesResponse(resp *http.Response) (res *Roles, _ error) {
switch resp.StatusCode {
case 200:

View File

@@ -593,6 +593,19 @@ func encodeSeedCombobulatorResponse(response *SeedCombobulatorNoContent, w http.
return nil
}
func encodeSessionConfigResponse(response *OIDCConfig, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
e := new(jx.Encoder)
response.Encode(e)
if _, err := e.WriteTo(w); err != nil {
return errors.Wrap(err, "write")
}
return nil
}
func encodeSessionRolesResponse(response *Roles, w http.ResponseWriter, span trace.Span) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)

View File

@@ -12,37 +12,150 @@ import (
var (
rn46AllowedHeaders = map[string]string{
"POST": "Content-Type",
"POST": "Authorization,Content-Type",
}
rn2AllowedHeaders = map[string]string{
"DELETE": "Authorization",
}
rn48AllowedHeaders = map[string]string{
"POST": "Content-Type",
"POST": "Authorization,Content-Type",
}
rn85AllowedHeaders = map[string]string{
"PATCH": "Content-Type",
rn83AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn86AllowedHeaders = map[string]string{
"PATCH": "Authorization,Content-Type",
}
rn87AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn5AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn6AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn18AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn8AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn19AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn3AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn9AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn10AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn12AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn14AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn15AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn16AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn17AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn76AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn45AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn62AllowedHeaders = map[string]string{
"GET": "Authorization",
}
rn67AllowedHeaders = map[string]string{
"GET": "Authorization",
}
rn75AllowedHeaders = map[string]string{
"POST": "Content-Type",
"POST": "Authorization,Content-Type",
}
rn52AllowedHeaders = map[string]string{
"POST": "Content-Type",
"POST": "Authorization,Content-Type",
}
rn60AllowedHeaders = map[string]string{
"POST": "Content-Type",
"DELETE": "Authorization",
"POST": "Authorization,Content-Type",
}
rn50AllowedHeaders = map[string]string{
"POST": "Content-Type",
"POST": "Authorization,Content-Type",
}
rn58AllowedHeaders = map[string]string{
"POST": "Content-Type",
"DELETE": "Authorization",
"POST": "Authorization,Content-Type",
}
rn79AllowedHeaders = map[string]string{
"GET": "Authorization",
}
rn80AllowedHeaders = map[string]string{
"GET": "Authorization",
}
rn81AllowedHeaders = map[string]string{
"GET": "Authorization",
}
rn53AllowedHeaders = map[string]string{
"POST": "Content-Type",
"POST": "Authorization,Content-Type",
}
rn54AllowedHeaders = map[string]string{
"POST": "Content-Type",
"POST": "Authorization,Content-Type",
}
rn22AllowedHeaders = map[string]string{
"DELETE": "Authorization",
}
rn56AllowedHeaders = map[string]string{
"POST": "Content-Type",
"POST": "Authorization,Content-Type",
}
rn85AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn88AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn25AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn26AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn28AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn37AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn23AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn29AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn30AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn32AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn33AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn35AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn36AllowedHeaders = map[string]string{
"POST": "Authorization",
}
rn38AllowedHeaders = map[string]string{
"POST": "Content-Type",
@@ -174,7 +287,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "DELETE,GET",
allowedHeaders: nil,
allowedHeaders: rn2AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -279,7 +392,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn83AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -308,7 +421,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "PATCH",
allowedHeaders: rn85AllowedHeaders,
allowedHeaders: rn86AllowedHeaders,
acceptPost: "",
acceptPatch: "text/plain",
})
@@ -335,7 +448,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn87AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -386,7 +499,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn5AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -413,7 +526,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn6AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -452,7 +565,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn18AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -479,7 +592,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn8AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -506,7 +619,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn19AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -533,7 +646,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn3AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -562,7 +675,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn9AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -589,7 +702,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn10AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -630,7 +743,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn12AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -656,7 +769,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn14AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -683,7 +796,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn15AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -712,7 +825,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn16AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -739,7 +852,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn17AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -798,7 +911,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn76AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -872,7 +985,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn45AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -899,7 +1012,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
allowedHeaders: rn62AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -943,7 +1056,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
allowedHeaders: rn67AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1159,6 +1272,31 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
break
}
switch elem[0] {
case 'c': // Prefix: "config"
if l := len("config"); len(elem) >= l && elem[0:l] == "config" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch r.Method {
case "GET":
s.handleSessionConfigRequest([0]string{}, elemIsEscaped, w, r)
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
acceptPost: "",
acceptPatch: "",
})
}
return
}
case 'r': // Prefix: "roles"
if l := len("roles"); len(elem) >= l && elem[0:l] == "roles" {
@@ -1175,7 +1313,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
allowedHeaders: rn79AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1200,7 +1338,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
allowedHeaders: rn80AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1225,7 +1363,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "GET",
allowedHeaders: nil,
allowedHeaders: rn81AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1342,7 +1480,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "DELETE,GET",
allowedHeaders: nil,
allowedHeaders: rn22AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1447,7 +1585,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn85AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1476,7 +1614,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn88AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1527,7 +1665,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn25AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1554,7 +1692,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn26AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1593,7 +1731,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn28AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1620,7 +1758,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn37AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1647,7 +1785,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn23AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1676,7 +1814,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn29AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1703,7 +1841,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn30AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1743,7 +1881,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn32AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1770,7 +1908,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn33AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1799,7 +1937,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn35AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -1826,7 +1964,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
s.notAllowed(w, r, notAllowedParams{
allowedMethods: "POST",
allowedHeaders: nil,
allowedHeaders: rn36AllowedHeaders,
acceptPost: "",
acceptPatch: "",
})
@@ -3185,6 +3323,31 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
break
}
switch elem[0] {
case 'c': // Prefix: "config"
if l := len("config"); len(elem) >= l && elem[0:l] == "config" {
elem = elem[l:]
} else {
break
}
if len(elem) == 0 {
// Leaf node.
switch method {
case "GET":
r.name = SessionConfigOperation
r.summary = "Get the OIDC parameters the web client needs to sign in"
r.operationID = "sessionConfig"
r.operationGroup = ""
r.pathPattern = "/session/config"
r.args = args
r.count = 0
return r, true
default:
return
}
}
case 'r': // Prefix: "roles"
if l := len("roles"); len(elem) >= l && elem[0:l] == "roles" {

View File

@@ -441,34 +441,34 @@ func (s *BatchUsernamesReq) SetUserIds(val []uint64) {
s.UserIds = val
}
// CombobulateMapNoContent is response for CombobulateMap operation.
type CombobulateMapNoContent struct{}
type CookieAuth struct {
APIKey string
Roles []string
type BearerAuth struct {
Token string
Roles []string
}
// GetAPIKey returns the value of APIKey.
func (s *CookieAuth) GetAPIKey() string {
return s.APIKey
// GetToken returns the value of Token.
func (s *BearerAuth) GetToken() string {
return s.Token
}
// GetRoles returns the value of Roles.
func (s *CookieAuth) GetRoles() []string {
func (s *BearerAuth) GetRoles() []string {
return s.Roles
}
// SetAPIKey sets the value of APIKey.
func (s *CookieAuth) SetAPIKey(val string) {
s.APIKey = val
// SetToken sets the value of Token.
func (s *BearerAuth) SetToken(val string) {
s.Token = val
}
// SetRoles sets the value of Roles.
func (s *CookieAuth) SetRoles(val []string) {
func (s *BearerAuth) SetRoles(val []string) {
s.Roles = val
}
// CombobulateMapNoContent is response for CombobulateMap operation.
type CombobulateMapNoContent struct{}
// CreateMapfixAuditCommentNoContent is response for CreateMapfixAuditComment operation.
type CreateMapfixAuditCommentNoContent struct{}
@@ -1076,6 +1076,49 @@ func (s *Mapfixes) SetMapfixes(val []Mapfix) {
s.Mapfixes = val
}
// Public parameters of the authorization code flow. Field names match the keys oidc-client-ts expects,
// and the shape the developer portal and moderation already serve, so the web clients share one config
// loader.
// Ref: #/components/schemas/OIDCConfig
type OIDCConfig struct {
// Issuer URL that mints and signs access tokens.
Authority string `json:"authority"`
// Public client id registered for the maps web application.
ClientID string `json:"client_id"`
// Audience the maps API requires on an access token.
Audience string `json:"audience"`
}
// GetAuthority returns the value of Authority.
func (s *OIDCConfig) GetAuthority() string {
return s.Authority
}
// GetClientID returns the value of ClientID.
func (s *OIDCConfig) GetClientID() string {
return s.ClientID
}
// GetAudience returns the value of Audience.
func (s *OIDCConfig) GetAudience() string {
return s.Audience
}
// SetAuthority sets the value of Authority.
func (s *OIDCConfig) SetAuthority(val string) {
s.Authority = val
}
// SetClientID sets the value of ClientID.
func (s *OIDCConfig) SetClientID(val string) {
s.ClientID = val
}
// SetAudience sets the value of Audience.
func (s *OIDCConfig) SetAudience(val string) {
s.Audience = val
}
// Ref: #/components/schemas/Operation
type Operation struct {
OperationID int32 `json:"OperationID"`

View File

@@ -13,8 +13,11 @@ import (
// SecurityHandler is handler for security parameters.
type SecurityHandler interface {
// HandleCookieAuth handles cookieAuth security.
HandleCookieAuth(ctx context.Context, operationName OperationName, t CookieAuth) (context.Context, error)
// HandleBearerAuth handles bearerAuth security.
// StrafesNET OIDC access token, as issued to the maps web client and already used by the developer
// portal and moderation. Roles ride in the token's roles claim, so no session lookup is needed to
// authorize a request.
HandleBearerAuth(ctx context.Context, operationName OperationName, t BearerAuth) (context.Context, error)
}
func findAuthorization(h http.Header, prefix string) (string, bool) {
@@ -32,8 +35,8 @@ func findAuthorization(h http.Header, prefix string) (string, bool) {
return "", false
}
// operationRolesCookieAuth is a private map storing roles per operation.
var operationRolesCookieAuth = map[string][]string{
// operationRolesBearerAuth is a private map storing roles per operation.
var operationRolesBearerAuth = map[string][]string{
ActionMapfixAcceptedOperation: []string{},
ActionMapfixRejectOperation: []string{},
ActionMapfixRequestChangesOperation: []string{},
@@ -86,18 +89,18 @@ var operationRolesCookieAuth = map[string][]string{
UpdateSubmissionModelOperation: []string{},
}
// GetRolesForCookieAuth returns the required roles for the given operation.
// GetRolesForBearerAuth returns the required roles for the given operation.
//
// This is useful for authorization scenarios where you need to know which roles
// are required for an operation.
//
// Example:
//
// requiredRoles := GetRolesForCookieAuth(AddPetOperation)
// requiredRoles := GetRolesForBearerAuth(AddPetOperation)
//
// Returns nil if the operation has no role requirements or if the operation is unknown.
func GetRolesForCookieAuth(operation string) []string {
roles, ok := operationRolesCookieAuth[operation]
func GetRolesForBearerAuth(operation string) []string {
roles, ok := operationRolesBearerAuth[operation]
if !ok {
return nil
}
@@ -107,21 +110,15 @@ func GetRolesForCookieAuth(operation string) []string {
return result
}
func (s *Server) securityCookieAuth(ctx context.Context, operationName OperationName, req *http.Request) (context.Context, bool, error) {
var t CookieAuth
const parameterName = "session_id"
var value string
switch cookie, err := req.Cookie(parameterName); {
case err == nil: // if NO error
value = cookie.Value
case errors.Is(err, http.ErrNoCookie):
func (s *Server) securityBearerAuth(ctx context.Context, operationName OperationName, req *http.Request) (context.Context, bool, error) {
var t BearerAuth
token, ok := findAuthorization(req.Header, "Bearer")
if !ok {
return ctx, false, nil
default:
return nil, false, errors.Wrap(err, "get cookie value")
}
t.APIKey = value
t.Roles = operationRolesCookieAuth[operationName]
rctx, err := s.sec.HandleCookieAuth(ctx, operationName, t)
t.Token = token
t.Roles = operationRolesBearerAuth[operationName]
rctx, err := s.sec.HandleBearerAuth(ctx, operationName, t)
if errors.Is(err, ogenerrors.ErrSkipServerSecurity) {
return nil, false, nil
} else if err != nil {
@@ -132,18 +129,18 @@ func (s *Server) securityCookieAuth(ctx context.Context, operationName Operation
// SecuritySource is provider of security values (tokens, passwords, etc.).
type SecuritySource interface {
// CookieAuth provides cookieAuth security value.
CookieAuth(ctx context.Context, operationName OperationName) (CookieAuth, error)
// BearerAuth provides bearerAuth security value.
// StrafesNET OIDC access token, as issued to the maps web client and already used by the developer
// portal and moderation. Roles ride in the token's roles claim, so no session lookup is needed to
// authorize a request.
BearerAuth(ctx context.Context, operationName OperationName) (BearerAuth, error)
}
func (s *Client) securityCookieAuth(ctx context.Context, operationName OperationName, req *http.Request) error {
t, err := s.sec.CookieAuth(ctx, operationName)
func (s *Client) securityBearerAuth(ctx context.Context, operationName OperationName, req *http.Request) error {
t, err := s.sec.BearerAuth(ctx, operationName)
if err != nil {
return errors.Wrap(err, "security source \"CookieAuth\"")
return errors.Wrap(err, "security source \"BearerAuth\"")
}
req.AddCookie(&http.Cookie{
Name: "session_id",
Value: t.APIKey,
})
req.Header.Set("Authorization", "Bearer "+t.Token)
return nil
}

View File

@@ -359,6 +359,14 @@ type Handler interface {
//
// POST /maps-admin/seed-combobulator
SeedCombobulator(ctx context.Context) error
// SessionConfig implements sessionConfig operation.
//
// Unauthenticated by necessity: the client calls this before it has a token, to discover which issuer
// and client id to use. Contains no secrets — all three values are public parameters of the
// authorization code flow and are visible in the sign-in redirect.
//
// GET /session/config
SessionConfig(ctx context.Context) (*OIDCConfig, error)
// SessionRoles implements sessionRoles operation.
//
// Get list of roles for the current session.

View File

@@ -538,6 +538,17 @@ func (UnimplementedHandler) SeedCombobulator(ctx context.Context) error {
return ht.ErrNotImplemented
}
// SessionConfig implements sessionConfig operation.
//
// Unauthenticated by necessity: the client calls this before it has a token, to discover which issuer
// and client id to use. Contains no secrets — all three values are public parameters of the
// authorization code flow and are visible in the sign-in redirect.
//
// GET /session/config
func (UnimplementedHandler) SessionConfig(ctx context.Context) (r *OIDCConfig, _ error) {
return r, ht.ErrNotImplemented
}
// SessionRoles implements sessionRoles operation.
//
// Get list of roles for the current session.

View File

@@ -866,6 +866,87 @@ func (s *Mapfixes) Validate() error {
return nil
}
func (s *OIDCConfig) Validate() error {
if s == nil {
return validate.ErrNilPointer
}
var failures []validate.FieldError
if err := func() error {
if err := (validate.String{
MinLength: 0,
MinLengthSet: false,
MaxLength: 256,
MaxLengthSet: true,
Email: false,
Hostname: false,
Regex: nil,
MinNumeric: 0,
MinNumericSet: false,
MaxNumeric: 0,
MaxNumericSet: false,
}).Validate(string(s.Authority)); err != nil {
return errors.Wrap(err, "string")
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: "authority",
Error: err,
})
}
if err := func() error {
if err := (validate.String{
MinLength: 0,
MinLengthSet: false,
MaxLength: 128,
MaxLengthSet: true,
Email: false,
Hostname: false,
Regex: nil,
MinNumeric: 0,
MinNumericSet: false,
MaxNumeric: 0,
MaxNumericSet: false,
}).Validate(string(s.ClientID)); err != nil {
return errors.Wrap(err, "string")
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: "client_id",
Error: err,
})
}
if err := func() error {
if err := (validate.String{
MinLength: 0,
MinLengthSet: false,
MaxLength: 128,
MaxLengthSet: true,
Email: false,
Hostname: false,
Regex: nil,
MinNumeric: 0,
MinNumericSet: false,
MaxNumeric: 0,
MaxNumericSet: false,
}).Validate(string(s.Audience)); err != nil {
return errors.Wrap(err, "string")
}
return nil
}(); err != nil {
failures = append(failures, validate.FieldError{
Name: "audience",
Error: err,
})
}
if len(failures) > 0 {
return &validate.Error{Fields: failures}
}
return nil
}
func (s *Operation) Validate() error {
if s == nil {
return validate.ErrNilPointer

View File

@@ -5,7 +5,7 @@ import (
"net"
"net/http"
"git.itzana.me/strafesnet/go-grpc/auth"
"git.itzana.me/strafesnet/auth-sdk/oidcmiddleware"
"git.itzana.me/strafesnet/go-grpc/maps"
"git.itzana.me/strafesnet/go-grpc/maps_extended"
"git.itzana.me/strafesnet/go-grpc/mapfixes"
@@ -84,10 +84,22 @@ func NewServeCommand() *cli.Command {
EnvVars: []string{"PORT_INTERNAL"},
},
&cli.StringFlag{
Name: "auth-rpc-host",
Usage: "Host of auth rpc",
EnvVars: []string{"AUTH_RPC_HOST"},
Value: "auth-service:8090",
Name: "oidc-issuer",
Usage: "OIDC issuer URL",
EnvVars: []string{"OIDC_ISSUER"},
Required: true,
},
&cli.StringFlag{
Name: "oidc-audience",
Usage: "Audience required on maps API access tokens",
EnvVars: []string{"OIDC_AUDIENCE"},
Required: true,
},
&cli.StringFlag{
Name: "oidc-client-id",
Usage: "Public OIDC client ID used by the maps web application",
EnvVars: []string{"OIDC_CLIENT_ID"},
Required: true,
},
&cli.StringFlag{
Name: "data-rpc-host",
@@ -205,15 +217,25 @@ func serve(ctx *cli.Context) error {
HttpClient: http.DefaultClient,
ApiKey: ctx.String("rbx-api-key"),
},
api.OIDCConfig{
Authority: ctx.String("oidc-issuer"),
ClientID: ctx.String("oidc-client-id"),
Audience: ctx.String("oidc-audience"),
},
)
conn, err = grpc.Dial(ctx.String("auth-rpc-host"), grpc.WithTransportCredentials(insecure.NewCredentials()))
// Access tokens verify locally against the issuer's key set, so the auth
// service is no longer on the request path — the gRPC connection it needed
// is gone with the session cookie it served.
authenticator, err := oidcmiddleware.New(
ctx.Context,
ctx.String("oidc-issuer"),
ctx.String("oidc-audience"),
)
if err != nil {
log.Fatal(err)
}
sec := web_api.SecurityHandler{
Client: auth.NewAuthServiceClient(conn),
log.WithError(err).Fatal("failed to initialize OIDC authenticator")
}
sec := web_api.SecurityHandler{Authenticator: authenticator}
srv_external, err := api.NewServer(&svc_external, sec, api.WithPathPrefix("/v1"))
if err != nil {

View File

@@ -4,89 +4,78 @@ import (
"context"
"fmt"
"git.itzana.me/strafesnet/go-grpc/auth"
"git.itzana.me/strafesnet/auth-sdk/oidcmiddleware"
"git.itzana.me/strafesnet/maps-service/pkg/api"
"git.itzana.me/strafesnet/maps-service/pkg/model"
)
var (
// ErrMissingSessionID there is no session id
ErrMissingSessionID = fmt.Errorf("%w: SessionID missing", ErrUserInfo)
// ErrInvalidSession caller does not have a valid session
ErrInvalidSession = fmt.Errorf("%w: Session invalid", ErrUserInfo)
// ErrMissingToken there is no bearer token on the request
ErrMissingToken = fmt.Errorf("%w: bearer token missing", ErrUserInfo)
// ErrInvalidToken the bearer token did not verify against the issuer
ErrInvalidToken = fmt.Errorf("%w: bearer token invalid", ErrUserInfo)
// ErrUnverifiedRoblox the token is valid but carries no linked Roblox account
ErrUnverifiedRoblox = fmt.Errorf("%w: token has no verified Roblox account", ErrUserInfo)
)
// UserInfoHandle is the authenticated caller for one request.
//
// Everything needed to authorize a request rides in the access token, so unlike
// the session-cookie flow this replaces, nothing here leaves the process: no
// call is made to answer "who is this" or "what may they do". Display fields
// are not token claims and are resolved by SessionUser, the one endpoint that
// returns them.
type UserInfoHandle struct {
// Would love to know a better way to do this
svc *SecurityHandler
ctx *context.Context
sessionId string
user *oidcmiddleware.User
}
type UserInfo struct {
UserID uint64
Username string
AvatarURL string
}
func (usr UserInfoHandle) GetUserInfo() (userInfo UserInfo, err error) {
session, err := usr.svc.Client.GetSessionUser(*usr.ctx, &auth.IdMessage{
SessionID: usr.sessionId,
})
if err != nil {
return userInfo, err
}
userInfo.UserID = session.UserID
userInfo.Username = session.Username
userInfo.AvatarURL = session.AvatarURL
return userInfo, nil
}
func (usr UserInfoHandle) GetUserID() (uint64, error) {
session, err := usr.svc.Client.GetSessionUser(*usr.ctx, &auth.IdMessage{
SessionID: usr.sessionId,
})
if err != nil {
return 0, err
}
return session.UserID, nil
return usr.user.RobloxUserID, nil
}
// Validate reports whether the caller still holds a usable identity.
//
// The token's signature, issuer, audience and expiry were already checked
// before this handle existed, so reaching here means the credential is good.
func (usr UserInfoHandle) Validate() (bool, error) {
validate, err := usr.svc.Client.ValidateSession(*usr.ctx, &auth.IdMessage{
SessionID: usr.sessionId,
})
if err != nil {
return false, err
}
return validate.Valid, nil
return usr.user != nil && usr.user.RobloxVerified, nil
}
func (usr UserInfoHandle) HasRoles(wantRoles model.Roles) (bool, error) {
haveroles, err := usr.GetRoles()
if err != nil {
return false, err
}
return haveroles & wantRoles == wantRoles, nil
return haveroles&wantRoles == wantRoles, nil
}
func (usr UserInfoHandle) GetRoles() (model.Roles, error) {
roles, err := usr.svc.Client.GetGroupRole(*usr.ctx, &auth.IdMessage{
SessionID: usr.sessionId,
})
if err != nil {
return model.RolesEmpty, err
// GetRoles folds the token's StrafesNET roles into the permission bitflag.
//
// The rank ladder is unchanged from the session flow — the ranks now arrive as
// a token claim rather than a GetGroupRole round trip.
func (usr UserInfoHandle) GetRoles() (model.Roles, error) {
if usr.user == nil {
return model.RolesEmpty, nil
}
// map roles into bitflag
rolesBitflag := model.RolesEmpty;
for _, r := range roles.Roles {
switch model.GroupRole(r.Rank){
rolesBitflag := model.RolesEmpty
for _, r := range usr.user.Roles {
switch model.GroupRole(r.Rank) {
case model.RoleQuat, model.RoleItzaname, model.RoleStagingDeveloper:
rolesBitflag|=model.RolesAll
rolesBitflag |= model.RolesAll
case model.RoleMapAdmin:
rolesBitflag|=model.RolesMapAdmin
rolesBitflag |= model.RolesMapAdmin
case model.RoleMapCouncil:
rolesBitflag|=model.RolesMapCouncil
rolesBitflag |= model.RolesMapCouncil
case model.RoleMapAccess:
rolesBitflag|=model.RolesMapAccess
rolesBitflag |= model.RolesMapAccess
}
}
return rolesBitflag, nil
@@ -117,6 +106,7 @@ func (usr UserInfoHandle) HasRoleSubmissionReview() (bool, error) {
func (usr UserInfoHandle) HasRoleScriptWrite() (bool, error) {
return usr.HasRoles(model.RolesScriptWrite)
}
/// Not implemented
func (usr UserInfoHandle) HasRoleMaptest() (bool, error) {
println("HasRoleMaptest is not implemented!")
@@ -124,20 +114,33 @@ func (usr UserInfoHandle) HasRoleMaptest() (bool, error) {
}
type SecurityHandler struct {
Client auth.AuthServiceClient
// Authenticator verifies access tokens against the StrafesNET issuer.
Authenticator *oidcmiddleware.Middleware
}
func (svc SecurityHandler) HandleCookieAuth(ctx context.Context, operationName api.OperationName, t api.CookieAuth) (context.Context, error) {
sessionId := t.GetAPIKey()
if sessionId == "" {
return nil, ErrMissingSessionID
// HandleBearerAuth verifies the access token and attaches the caller.
//
// This replaces HandleCookieAuth. The session cookie carried only an opaque id
// that had to be exchanged with the auth service on every permission check;
// the access token is self-describing and verified locally against the
// issuer's key set.
func (svc SecurityHandler) HandleBearerAuth(ctx context.Context, operationName api.OperationName, t api.BearerAuth) (context.Context, error) {
token := t.GetToken()
if token == "" {
return nil, ErrMissingToken
}
newCtx := context.WithValue(ctx, "UserInfo", UserInfoHandle{
svc: &svc,
ctx: &ctx,
sessionId: sessionId,
})
user, err := svc.Authenticator.Verify(ctx, token)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrInvalidToken, err)
}
return newCtx, nil
// Every operation here is keyed to a Roblox identity. A token without one
// verified cannot be mapped to a submitter, so reject it at the edge
// rather than letting it fail deeper with a confusing error.
if !user.RobloxVerified {
return nil, ErrUnverifiedRoblox
}
return context.WithValue(ctx, "UserInfo", UserInfoHandle{user: user}), nil
}

View File

@@ -0,0 +1,153 @@
package web_api
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"git.itzana.me/strafesnet/auth-sdk/oidcmiddleware"
"git.itzana.me/strafesnet/maps-service/pkg/api"
"git.itzana.me/strafesnet/maps-service/pkg/model"
)
// The rank ladder moved from a GetGroupRole gRPC response to an access-token
// claim during the OIDC migration. The mapping is what decides whether someone
// may release a map, so it is checked directly rather than inferred from a
// screen.
func TestGetRolesMapsRankLadderToBitflag(t *testing.T) {
cases := []struct {
name string
ranks []int
want model.Roles
}{
{"no roles", nil, model.RolesEmpty},
{"unknown rank grants nothing", []int{1, 7, 31}, model.RolesEmpty},
{"map access", []int{int(model.RoleMapAccess)}, model.RolesMapAccess},
{"map council", []int{int(model.RoleMapCouncil)}, model.RolesMapCouncil},
{"map admin", []int{int(model.RoleMapAdmin)}, model.RolesMapAdmin},
{"quat is everything", []int{int(model.RoleQuat)}, model.RolesAll},
{"itzaname is everything", []int{int(model.RoleItzaname)}, model.RolesAll},
{"staging developer is everything", []int{int(model.RoleStagingDeveloper)}, model.RolesAll},
{
"multiple roles union",
[]int{int(model.RoleMapAccess), int(model.RoleMapCouncil)},
model.RolesMapAccess | model.RolesMapCouncil,
},
{
"an unknown rank never dilutes a known one",
[]int{int(model.RoleMapAdmin), 3},
model.RolesMapAdmin,
},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
roles := make([]oidcmiddleware.Role, 0, len(testCase.ranks))
for _, rank := range testCase.ranks {
roles = append(roles, oidcmiddleware.Role{Rank: rank})
}
handle := UserInfoHandle{user: &oidcmiddleware.User{Roles: roles}}
got, err := handle.GetRoles()
if err != nil {
t.Fatalf("GetRoles() error = %v", err)
}
if got != testCase.want {
t.Errorf("GetRoles() = %b, want %b", got, testCase.want)
}
})
}
}
// Release permission implies review and upload. A regression here would let a
// council member release, so the derived answer is pinned, not just the bits.
func TestHasRolesDerivesFromRankNotEquality(t *testing.T) {
council := UserInfoHandle{user: &oidcmiddleware.User{
Roles: []oidcmiddleware.Role{{Rank: int(model.RoleMapCouncil)}},
}}
admin := UserInfoHandle{user: &oidcmiddleware.User{
Roles: []oidcmiddleware.Role{{Rank: int(model.RoleMapAdmin)}},
}}
if mayReview, _ := council.HasRoleMapfixReview(); !mayReview {
t.Error("map council cannot review mapfixes")
}
if mayRelease, _ := council.HasRoleSubmissionRelease(); mayRelease {
t.Error("map council can release submissions, which is an escalation")
}
if mayRelease, _ := admin.HasRoleSubmissionRelease(); !mayRelease {
t.Error("map admin cannot release submissions")
}
}
func TestHandleBearerAuthRejectsMissingToken(t *testing.T) {
handler := SecurityHandler{}
if _, err := handler.HandleBearerAuth(context.Background(), "sessionUser", api.BearerAuth{}); err == nil {
t.Fatal("an empty bearer token was accepted")
}
}
// The web client must be able to read its sign-in parameters before it holds a
// token; if this endpoint ever starts demanding one, sign-in deadlocks.
func TestSessionConfigIsServedWithoutATokenAndProtectedRoutesAreNot(t *testing.T) {
// inner is nil deliberately: neither path under test reaches it. The config
// handler reads only the value below, and an unauthenticated request is
// rejected by the security handler before any handler runs.
svc := Service{oidc: api.OIDCConfig{
Authority: "https://auth.example.test",
ClientID: "maps-web",
Audience: "maps-api",
}}
// No authenticator: a unit test cannot reach a live issuer to fetch its key
// set. Nothing here needs one — an anonymous request carries no token, so
// it is refused before verification would be attempted. That is precisely
// the property under test: no token, no entry.
securityHandler := SecurityHandler{}
server, err := api.NewServer(&svc, securityHandler, api.WithPathPrefix("/v1"))
if err != nil {
t.Fatalf("NewServer() error = %v", err)
}
httpServer := httptest.NewServer(server)
defer httpServer.Close()
response, err := http.Get(httpServer.URL + "/v1/session/config")
if err != nil {
t.Fatalf("GET /v1/session/config: %v", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
t.Fatalf("GET /v1/session/config status = %d, want 200", response.StatusCode)
}
var config struct {
Authority string `json:"authority"`
ClientID string `json:"client_id"`
Audience string `json:"audience"`
}
if err := json.NewDecoder(response.Body).Decode(&config); err != nil {
t.Fatalf("decoding config: %v", err)
}
// The key names are the contract oidc-client-ts reads; renaming them in the
// spec would break sign-in without breaking the build.
if config.Authority != "https://auth.example.test" ||
config.ClientID != "maps-web" ||
config.Audience != "maps-api" {
t.Errorf("config = %+v, want the configured issuer, client id and audience", config)
}
protected, err := http.Get(httpServer.URL + "/v1/session/user")
if err != nil {
t.Fatalf("GET /v1/session/user: %v", err)
}
defer protected.Body.Close()
if protected.StatusCode == http.StatusOK {
t.Error("GET /v1/session/user succeeded without a bearer token")
}
}

View File

@@ -34,15 +34,19 @@ var (
type Service struct {
inner *service.Service
roblox roblox.Client
// oidc is served verbatim to the web client so it can start a sign-in.
oidc api.OIDCConfig
}
func NewService(
inner *service.Service,
roblox roblox.Client,
oidc api.OIDCConfig,
) Service {
return Service{
inner: inner,
roblox: roblox,
oidc: oidc,
}
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"git.itzana.me/strafesnet/maps-service/pkg/api"
"git.itzana.me/strafesnet/maps-service/pkg/roblox"
)
// SessionRoles implements getSessionRoles operation.
@@ -17,7 +18,7 @@ func (svc *Service) SessionRoles(ctx context.Context) (*api.Roles, error) {
return nil, ErrUserInfo
}
roles, err := userInfo.GetRoles();
roles, err := userInfo.GetRoles()
if err != nil {
return nil, err
}
@@ -25,44 +26,71 @@ func (svc *Service) SessionRoles(ctx context.Context) (*api.Roles, error) {
return &api.Roles{Roles: int32(roles)}, nil
}
// SessionConfig implements sessionConfig operation.
//
// Serves the public OIDC parameters so the web client can start a sign-in
// before it holds any token. Deliberately unauthenticated — see the spec.
//
// GET /session/config
func (svc *Service) SessionConfig(ctx context.Context) (*api.OIDCConfig, error) {
config := svc.oidc
return &config, nil
}
// SessionUser implements sessionUser operation.
//
// Get information about the currently logged in user.
//
// GET /session/roles
// The id comes from the verified access token. Username and avatar are not
// token claims, so they are resolved here — the only place in the service that
// still needs a lookup to answer who the caller is.
//
// GET /session/user
func (svc *Service) SessionUser(ctx context.Context) (*api.User, error) {
userInfoHandle, ok := ctx.Value("UserInfo").(UserInfoHandle)
if !ok {
return nil, ErrUserInfo
}
userInfo, err := userInfoHandle.GetUserInfo();
userID, err := userInfoHandle.GetUserID()
if err != nil {
return nil, err
}
ids := []uint64{userID}
// A missing display name or avatar must not fail sign-in: the caller is
// already authenticated and the id alone is enough to use the site. Both
// lookups degrade to empty rather than erroring the request.
username := ""
if usernames, err := svc.inner.GetUsernames(ctx, ids); err == nil {
username = usernames[userID]
}
avatarURL := ""
if thumbnails, err := svc.inner.GetUserAvatarThumbnails(ctx, ids, roblox.Size150x150); err == nil {
avatarURL = thumbnails[userID]
}
return &api.User{
UserID:int64(userInfo.UserID),
Username:userInfo.Username,
AvatarURL:userInfo.AvatarURL,
UserID: int64(userID),
Username: username,
AvatarURL: avatarURL,
}, nil
}
// SessionUser implements sessionUser operation.
// SessionValidate implements sessionValidate operation.
//
// Get information about the currently logged in user.
// Reports whether the caller still holds a usable identity. Reaching this
// handler means the token already verified against the issuer, so this is a
// cheap confirmation rather than a round trip.
//
// GET /session/roles
// GET /session/validate
func (svc *Service) SessionValidate(ctx context.Context) (bool, error) {
userInfoHandle, ok := ctx.Value("UserInfo").(UserInfoHandle)
if !ok {
return false, ErrUserInfo
}
valid, err := userInfoHandle.Validate();
if err != nil {
return false, err
}
return valid, nil
return userInfoHandle.Validate()
}

View File

@@ -10,6 +10,7 @@
"@tanstack/react-query": "^5.101.2",
"date-fns": "^4.4.0",
"lucide-react": "^1.30.0",
"oidc-client-ts": "^3.5.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router-dom": "^7.18.1",
@@ -499,6 +500,8 @@
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jwt-decode": ["jwt-decode@4.0.0", "", {}, "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
@@ -529,6 +532,8 @@
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
"oidc-client-ts": ["oidc-client-ts@3.5.0", "", { "dependencies": { "jwt-decode": "^4.0.0" } }, "sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],

22
web/package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@tanstack/react-query": "^5.101.2",
"date-fns": "^4.4.0",
"lucide-react": "^1.30.0",
"oidc-client-ts": "^3.5.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router-dom": "^7.18.1"
@@ -3423,6 +3424,15 @@
"node": ">=6"
}
},
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3566,6 +3576,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/oidc-client-ts": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.5.0.tgz",
"integrity": "sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==",
"license": "Apache-2.0",
"dependencies": {
"jwt-decode": "^4.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",

View File

@@ -15,6 +15,7 @@
"@tanstack/react-query": "^5.101.2",
"date-fns": "^4.4.0",
"lucide-react": "^1.30.0",
"oidc-client-ts": "^3.5.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router-dom": "^7.18.1"

View File

@@ -2,6 +2,7 @@ import { Suspense, lazy, useEffect } from 'react'
import { Routes, Route, useLocation } from 'react-router-dom'
import { Skeleton, Stack, Tooltip, ToastProvider } from '@strafesnet/ui'
import Webpage from '@/app/_components/webpage'
import AuthCallback from '@/app/auth/callback/page'
/* Routes are split. Two reasons: the review screens pull in Monaco and the
whole audit-event machinery, which no visitor browsing the catalogue should
@@ -75,6 +76,9 @@ function App() {
<Route path="/review" element={<ReviewerDashboardPage />} />
<Route path="/dashboard" element={<UserDashboardPage />} />
<Route path="/script-review" element={<ScriptReviewPage />} />
{/* Not lazy: this is the identity provider's return URL, so it
must render on the very first frame of a cold load. */}
<Route path="/auth/callback" element={<AuthCallback />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>

View File

@@ -13,12 +13,15 @@ import {
Gamepad2,
LayoutDashboard,
LogIn,
LogOut,
Menu,
Plus,
UserCog,
} from "lucide-react"
import { useUser } from "@/app/hooks/useUser"
import { useRoles } from "@/app/hooks/useRoles"
import { useAuthConfig } from "@/app/hooks/useAuthConfig"
import { signIn, signOut } from "@/app/auth"
import { hasAnyReviewerRole } from "@/app/ts/Roles"
import styles from "./header.module.css"
@@ -50,20 +53,10 @@ export default function Header() {
const { user, isLoggedIn, isLoading } = useUser()
const { roles } = useRoles()
const authUrl =
window.location.hostname === "maps.strafes.net"
? "https://auth.strafes.net"
: "https://auth.staging.strafes.net"
const { authority } = useAuthConfig()
const isReviewer = hasAnyReviewerRole(roles)
const visibleNav = navItems.filter((item) => !item.reviewerOnly || isReviewer)
const login = () => {
window.location.href = `${authUrl}/oauth2/login?redirect=${encodeURIComponent(
window.location.href,
)}`
}
return (
<>
<Toolbar.SkipLink />
@@ -182,8 +175,22 @@ export default function Header() {
<Link to="/submit">Submit a map</Link>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item asChild startIcon={<UserCog size={14} />} shortcut={<ExternalLink size={12} />}>
<a href={authUrl}>Manage account</a>
<DropdownMenu.Item
asChild
disabled={!authority}
startIcon={<UserCog size={14} />}
shortcut={<ExternalLink size={12} />}
>
<a href={authority} target="_blank" rel="noreferrer noopener">
Manage account
</a>
</DropdownMenu.Item>
<DropdownMenu.Item
tone="danger"
startIcon={<LogOut size={14} />}
onSelect={() => void signOut()}
>
Sign out
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
@@ -191,7 +198,7 @@ export default function Header() {
/* Outline, not solid. Red marks the page's primary action; a
second solid accent control in the chrome competes with it
and puts the surface over its red budget. */
<Button size="sm" variant="outline" startIcon={<LogIn size={14} />} onClick={login}>
<Button size="sm" variant="outline" startIcon={<LogIn size={14} />} onClick={() => void signIn()}>
Log in
</Button>
)}

217
web/src/app/auth.ts Normal file
View File

@@ -0,0 +1,217 @@
import { ErrorResponse, User, UserManager, WebStorageStateStore } from 'oidc-client-ts';
/**
* StrafesNET OIDC sign-in, the same flow the developer portal and moderation
* use. Replaces the old `auth.strafes.net/oauth2/login?redirect=` handoff,
* which set an opaque session cookie the API had to exchange with the auth
* service on every permission check.
*/
interface OIDCConfig {
authority: string;
client_id: string;
audience: string;
}
export interface AuthenticationFailure {
code: string;
title: string;
message: string;
retryable: boolean;
}
export interface AuthenticationCallbackResult {
returnPath: string;
failure?: AuthenticationFailure;
}
let managerPromise: Promise<UserManager> | null = null;
let renewalPromise: Promise<User | null> | null = null;
let callbackPromise: Promise<AuthenticationCallbackResult> | null = null;
async function createManager(): Promise<UserManager> {
const response = await fetch('/v1/session/config', {
headers: { Accept: 'application/json' },
cache: 'no-store',
});
if (!response.ok) {
throw new Error(`Failed to load OIDC configuration: ${response.status}`);
}
const config = await response.json() as OIDCConfig;
if (!config.authority || !config.client_id || !config.audience) {
throw new Error('OIDC configuration is incomplete');
}
const storage = new WebStorageStateStore({ store: window.sessionStorage });
return new UserManager({
authority: config.authority,
client_id: config.client_id,
redirect_uri: `${window.location.origin}/auth/callback`,
post_logout_redirect_uri: `${window.location.origin}/`,
response_type: 'code',
scope: 'openid profile roles offline_access',
extraQueryParams: { audience: config.audience },
userStore: storage,
stateStore: storage,
automaticSilentRenew: true,
revokeTokensOnSignout: true,
monitorSession: false,
});
}
function manager(): Promise<UserManager> {
if (!managerPromise) {
managerPromise = createManager().catch(error => {
managerPromise = null;
throw error;
});
}
return managerPromise;
}
/** Only same-origin paths are safe to return to after a redirect. */
function safeReturnPath(value: unknown): string {
if (typeof value !== 'string' || !value.startsWith('/') || value.startsWith('//')) {
return '/';
}
return value;
}
export function authenticationCallbackFailure(error: unknown): AuthenticationCallbackResult {
const response = error instanceof ErrorResponse ? error : null;
const code = response?.error || 'callback_failed';
const knownFailures: Record<string, Omit<AuthenticationFailure, 'code'>> = {
access_denied: {
title: 'Sign-in was canceled',
message: 'Access was not granted, so you are still signed out.',
retryable: true,
},
login_required: {
title: 'Sign-in is required',
message: 'Your StrafesNET session is no longer available. Sign in again to continue.',
retryable: true,
},
consent_required: {
title: 'Permission is required',
message: 'Maps was not granted the permissions it needs to sign you in.',
retryable: true,
},
interaction_required: {
title: 'Confirmation is required',
message: 'StrafesNET needs additional confirmation before maps can sign you in.',
retryable: true,
},
server_error: {
title: 'Authentication is unavailable',
message: 'The authentication service could not complete the request. Please try again.',
retryable: true,
},
temporarily_unavailable: {
title: 'Authentication is unavailable',
message: 'The authentication service is temporarily unavailable. Please try again.',
retryable: true,
},
invalid_request: {
title: 'Invalid sign-in request',
message: 'The sign-in request could not be accepted. If this continues, contact a StrafesNET administrator.',
retryable: false,
},
invalid_client: {
title: 'Invalid sign-in configuration',
message: 'Maps is not configured correctly for sign-in. Contact a StrafesNET administrator.',
retryable: false,
},
unauthorized_client: {
title: 'Sign-in is not authorized',
message: 'Maps is not authorized to use this sign-in flow. Contact a StrafesNET administrator.',
retryable: false,
},
unsupported_response_type: {
title: 'Unsupported sign-in request',
message: 'Maps requested an unsupported sign-in flow. Contact a StrafesNET administrator.',
retryable: false,
},
invalid_scope: {
title: 'Invalid permission request',
message: 'Maps requested permissions that the authentication service could not grant.',
retryable: false,
},
};
const failure = knownFailures[code] ?? {
title: 'Sign-in could not be completed',
message: 'The authentication response could not be verified. Start the sign-in again.',
retryable: true,
};
return {
returnPath: safeReturnPath(response?.state),
failure: { code, ...failure },
};
}
/**
* Completes the redirect if this load is the callback. Returns null on every
* other URL, so the app can call it unconditionally at boot.
*/
export async function initializeAuthentication(): Promise<AuthenticationCallbackResult | null> {
if (window.location.pathname !== '/auth/callback') {
return null;
}
if (!callbackPromise) {
callbackPromise = (async () => {
try {
const user = await (await manager()).signinRedirectCallback();
return { returnPath: safeReturnPath(user.state) };
} catch (error) {
return authenticationCallbackFailure(error);
}
})();
}
return callbackPromise;
}
export async function signIn(returnPath?: string): Promise<void> {
const destination = returnPath ??
`${window.location.pathname}${window.location.search}${window.location.hash}`;
await (await manager()).signinRedirect({ state: safeReturnPath(destination) });
}
export async function signOut(): Promise<void> {
const auth = await manager();
const user = await auth.getUser();
if (!user) {
window.location.assign('/');
return;
}
await auth.signoutRedirect({ id_token_hint: user.id_token });
}
/** The current user, silently renewing an expired token once before failing. */
async function currentUser(): Promise<User | null> {
const auth = await manager();
const user = await auth.getUser();
if (!user || !user.expired) {
return user;
}
if (!renewalPromise) {
renewalPromise = auth.signinSilent()
.catch(async () => {
await auth.removeUser();
return null;
})
.finally(() => {
renewalPromise = null;
});
}
return renewalPromise;
}
export async function authenticatedFetch(input: RequestInfo | URL, init: RequestInit = {}): Promise<Response> {
const user = await currentUser();
const headers = new Headers(init.headers);
if (user?.access_token) {
headers.set('Authorization', `Bearer ${user.access_token}`);
}
return fetch(input, { ...init, headers });
}

View File

@@ -0,0 +1,86 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useQueryClient } from "@tanstack/react-query";
import { Button, EmptyState, Spinner, Stack } from "@strafesnet/ui";
import { LogIn, ShieldAlert } from "lucide-react";
import Webpage from "@/app/_components/webpage";
import { useTitle } from "@/app/hooks/useTitle";
import {
initializeAuthentication,
signIn,
type AuthenticationFailure,
} from "@/app/auth";
/**
* Where the identity provider returns after sign-in.
*
* The authorization code is exchanged here, then the visitor is sent back to
* whatever page started the flow — `replace`, so the callback URL with its
* one-time code never lands in history and cannot be re-entered by going back.
*/
export default function AuthCallback() {
useTitle("Signing in");
const navigate = useNavigate();
const queryClient = useQueryClient();
const [failure, setFailure] = useState<AuthenticationFailure | null>(null);
const [retrying, setRetrying] = useState(false);
useEffect(() => {
let cancelled = false;
void (async () => {
const result = await initializeAuthentication();
if (cancelled || !result) return;
if (result.failure) {
setFailure(result.failure);
return;
}
// The identity changed, so anything keyed to the old one is stale —
// most visibly the header, which would otherwise still say Log in.
await queryClient.invalidateQueries({ queryKey: ["user"] });
await queryClient.invalidateQueries({ queryKey: ["session", "roles"] });
navigate(result.returnPath, { replace: true });
})();
return () => {
cancelled = true;
};
}, [navigate, queryClient]);
if (!failure) {
return (
<Webpage width="narrow">
<Stack gap={5} align="center" aria-busy="true">
<Spinner size="lg" />
<span>Completing sign-in</span>
</Stack>
</Webpage>
);
}
return (
<Webpage width="narrow">
<EmptyState
icon={<ShieldAlert />}
title={failure.title}
description={failure.message}
action={
failure.retryable ? (
<Button
startIcon={<LogIn size={15} />}
loading={retrying}
onClick={() => {
setRetrying(true);
void signIn("/").catch(() => setRetrying(false));
}}
>
Try again
</Button>
) : undefined
}
/>
</Webpage>
);
}

View File

@@ -0,0 +1,37 @@
import { useQuery } from '@tanstack/react-query';
export interface AuthConfig {
authority: string;
client_id: string;
audience: string;
}
/**
* The public OIDC parameters, served by `/v1/session/config`.
*
* The account-management link needs the issuer's address, which used to be
* guessed from `window.location.hostname` in the header — so a new deployment
* hostname silently pointed people at the wrong environment. The server knows
* which issuer it actually verifies against, so it is the one that answers.
*
* Unauthenticated and effectively immutable for the life of the page, hence no
* refetching.
*/
export function useAuthConfig() {
const { data } = useQuery({
queryKey: ['session', 'config'],
queryFn: async (): Promise<AuthConfig | null> => {
const response = await fetch('/v1/session/config', {
headers: { Accept: 'application/json' },
});
if (!response.ok) {
return null;
}
return await response.json() as AuthConfig;
},
staleTime: Infinity,
gcTime: Infinity,
});
return { authority: data?.authority };
}

View File

@@ -1,9 +1,12 @@
import { useQuery } from '@tanstack/react-query';
import { RolesConstants, type Roles } from '@/app/ts/Roles';
import { useUser } from '@/app/hooks/useUser';
import { authenticatedFetch } from '@/app/auth';
/* Roles now ride in the access token's claims, so this is a read of the
caller's own token rather than a session lookup on the auth service. */
async function fetchRoles(): Promise<Roles> {
const response = await fetch('/v1/session/roles');
const response = await authenticatedFetch('/v1/session/roles');
if (!response.ok) {
return RolesConstants.Empty;
}

View File

@@ -1,9 +1,14 @@
import { useQuery } from '@tanstack/react-query';
import { UserInfo } from '@/app/ts/User';
import { authenticatedFetch } from '@/app/auth';
/**
* Identity comes from the OIDC access token now rather than a session cookie,
* so the request carries a bearer header and a 401 just means signed out.
*/
async function fetchUser(): Promise<UserInfo | null> {
try {
const response = await fetch('/v1/session/user');
const response = await authenticatedFetch('/v1/session/user');
if (!response.ok) {
return null;