diff --git a/.gitignore b/.gitignore index c947f0fd..395d3437 100644 --- a/.gitignore +++ b/.gitignore @@ -6,11 +6,12 @@ tags client.config config/config.yaml +config/notifier-config.yaml config/docker-config.yaml IP2LOCATION-LITE-DB1.IPV6.BIN /cmd/test generateDDL.sh -dist/ +/dist/ /mytoken-migratedb /docker/docker-compose.yaml /docker/db.env diff --git a/.gitlab-ci-scripts/goreleaser.sh b/.gitlab-ci-scripts/goreleaser.sh index 3684e56d..1d86f65c 100755 --- a/.gitlab-ci-scripts/goreleaser.sh +++ b/.gitlab-ci-scripts/goreleaser.sh @@ -1,3 +1,5 @@ +#!/bin/bash + mkdir ../shared first=$(grep '^## ' -nm1 CHANGELOG.md | cut -d':' -f1); \ second=$(grep '^## ' -nm2 CHANGELOG.md | tail -n1 | cut -d':' -f1); \ @@ -6,5 +8,13 @@ GORELEASER_CONFIG=".goreleaser.yml" if [ -n "$CI_COMMIT_TAG" ] && echo "$CI_COMMIT_TAG" | grep -qv '~'; then GORELEASER_CONFIG=".goreleaser-release.yml" fi -goreleaser release -f $GORELEASER_CONFIG --release-notes ../shared/release.md +GORELEASER_OPTIONS="" +[[ "${CI_COMMIT_BRANCH}" != "${CI_DEFAULT_BRANCH}" ]] && { + [[ "${CI_COMMIT_BRANCH}" != "${PREREL_BRANCH_NAME}" ]] && { + # we're on devel + GORELEASER_OPTIONS="--skip docker" + } +} + +goreleaser release -f $GORELEASER_CONFIG --release-notes ../shared/release.md --verbose ${GORELEASER_OPTIONS} ls -l results \ No newline at end of file diff --git a/.gitlab-ci-scripts/set-prerel-version.sh b/.gitlab-ci-scripts/set-prerel-version.sh index c36cde4a..87bbbd72 100755 --- a/.gitlab-ci-scripts/set-prerel-version.sh +++ b/.gitlab-ci-scripts/set-prerel-version.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash DEVSTRING="pr" VERSION_FILE=internal/model/version/VERSION @@ -38,9 +38,17 @@ for R in $REMOTES; do MASTER_BRANCH="refs/remotes/${R}/${MASTER}" #echo "Master-branch: ${MASTER_BRANCH}" [ "x${R}" = "xorigin" ] && break + [ "x${R}" = "xcodebase" ] && break done -PREREL=$(git rev-list --count HEAD ^"$MASTER_BRANCH") +[[ "${DEVSTRING}" == "dev" ]] && { + [[ -z ${CI_JOB_ID} ]] || { + PREREL=${CI_JOB_ID} + } +} +[[ -z ${PREREL} ]] && { + PREREL=$(git rev-list --count HEAD ^"$MASTER_BRANCH") +} # use version file: VERSION=$(cat "$VERSION_FILE") diff --git a/.gitlab-ci-scripts/upload.sh b/.gitlab-ci-scripts/upload.sh index acd456bb..44be4c79 100755 --- a/.gitlab-ci-scripts/upload.sh +++ b/.gitlab-ci-scripts/upload.sh @@ -1,3 +1,4 @@ +#!/bin/bash REPO_TARGET="/prerel" if [ -n "$CI_COMMIT_TAG" ] && echo "$CI_COMMIT_TAG" | grep -qv '~'; then diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f7d72c73..1f83c68b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,15 +1,19 @@ include: - 'https://codebase.helmholtz.cloud/m-team/tools/ci-voodoo/-/raw/master/ci-include/github-status-sync.yml' + - "https://codebase.helmholtz.cloud/m-team/tools/ci-voodoo/raw/master/ci-include/pipeline-jobs-publish-to-repo.yml" + variables: + USE_GORELEASER: "true" UPSTREAM_PROJECT: oidc-mytoken/server -image: golang:1.19 +image: golang:1.22 stages: - build - test - lint - - release + - package + - publish - deploy default: @@ -44,7 +48,7 @@ staticcheck: vet: stage: lint script: - - go vet ./... + - go vet -structtag=false ./... build_server: stage: build @@ -61,19 +65,17 @@ build_migratedb: script: - go build github.com/oidc-mytoken/server/cmd/mytoken-server/mytoken-migratedb -prerelease: - stage: release +package: + stage: package image: name: goreleaser/goreleaser entrypoint: [ "" ] services: - docker:dind - only: - refs: - - tags - - prerel - tags: - - linux +# only: +# refs: +# - tags +# - prerel variables: GIT_STRATEGY: clone GIT_DEPTH: 0 @@ -83,13 +85,27 @@ prerelease: before_script: - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin "$DOCKER_REGISTRY" script: - - if [ -z "$CI_COMMIT_TAG" ]; then .gitlab-ci-scripts/set-prerel-version.sh; fi; - - .gitlab-ci-scripts/goreleaser.sh - - .gitlab-ci-scripts/upload.sh + - | + [[ "$CI_COMMIT_BRANCH" == "${PREREL_BRANCH_NAME}" ]] && { + echo "Running set-prerel-version" + .gitlab-ci-scripts/set-prerel-version.sh + } + [[ "$CI_COMMIT_BRANCH" != "${PREREL_BRANCH_NAME}" ]] && { + [[ "$CI_COMMIT_BRANCH" != "${CI_DEFAULT_BRANCH}" ]] && { + # we're on a devel branch + echo "Running set-devel-version" + .gitlab-ci-scripts/set-prerel-version.sh --devstring dev + } + } + .gitlab-ci-scripts/goreleaser.sh + + artifacts: + paths: + - "results/*" after_script: - curl -d "repo=github.com/oidc-mytoken/server" https://goreportcard.com/checks -deploy-dev: +deploy-prerel: stage: deploy only: refs: @@ -99,4 +115,4 @@ deploy-dev: - cp $KNOWN_HOSTS /root/.ssh/known_hosts - cp $DEPLOYMENT_SSH_KEY /root/.ssh/id_ed25519 && chmod 0600 /root/.ssh/id_ed25519 script: - - ssh mytoken-dev.vm.fedcloud.eu + - ssh -4 mytoken.data.kit.edu diff --git a/.goreleaser-release.yml b/.goreleaser-release.yml index d347cb05..01e0e267 100644 --- a/.goreleaser-release.yml +++ b/.goreleaser-release.yml @@ -34,6 +34,16 @@ builds: flags: - -trimpath mod_timestamp: '{{ .CommitTimestamp }}' + - id: notifier-server + main: ./cmd/mytoken-server/mytoken-notifier-server + binary: mytoken-notifier-server + env: + - CGO_ENABLED=0 + goos: + - linux + flags: + - -trimpath + mod_timestamp: '{{ .CommitTimestamp }}' archives: - name_template: >- {{ .ProjectName }}_{{ .Version }}_ @@ -121,6 +131,28 @@ nfpms: file_name_template: >- {{ .PackageName }}-{{ .Version }}. {{- if eq .Arch "386" }}i386{{- else if eq .Arch "amd64" }}x86_64{{- else }}{{ .Arch }}{{ end }} + - id: notifier-pkg + package_name: mytoken-notifier-server + file_name_template: >- + {{ .PackageName }}_{{ .Version }}_ + {{- if eq .Arch "386" }}i386{{- else }}{{ .Arch }}{{ end }} + builds: + - notifier-server + homepage: https://mytoken-docs.data.kit.edu/server + maintainer: Gabriel Zachmann + description: A standalone notifier server for mytoken notifications + license: MIT + formats: + - deb + - rpm + release: "1" + section: misc + bindir: /usr/bin + overrides: + rpm: + file_name_template: >- + {{ .PackageName }}-{{ .Version }}. + {{- if eq .Arch "386" }}i386{{- else if eq .Arch "amd64" }}x86_64{{- else }}{{ .Arch }}{{ end }} dockers: - goos: linux goarch: amd64 @@ -170,6 +202,22 @@ dockers: - "--label=org.opencontainers.image.title=mytoken-migratedb" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" + - goos: linux + goarch: amd64 + ids: + - notifier-server + image_templates: + - "oidcmytoken/mytoken-notifier-server:latest" + - "oidcmytoken/mytoken-notifier-server:{{ .Tag }}" + - "oidcmytoken/mytoken-notifier-server:v{{ .Major }}" + - "oidcmytoken/mytoken-notifier-server:v{{ .Major }}.{{ .Minor }}" + dockerfile: cmd/mytoken-server/mytoken-notifier-server/Dockerfile + build_flag_templates: + - "--pull" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title=mytoken-notifier-server" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" checksum: name_template: 'checksums.txt' snapshot: diff --git a/.goreleaser.yml b/.goreleaser.yml index 3fffae46..b9aefb95 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -34,6 +34,16 @@ builds: flags: - -trimpath mod_timestamp: '{{ .CommitTimestamp }}' + - id: notifier-server + main: ./cmd/mytoken-server/mytoken-notifier-server + binary: mytoken-notifier-server + env: + - CGO_ENABLED=0 + goos: + - linux + flags: + - -trimpath + mod_timestamp: '{{ .CommitTimestamp }}' archives: - name_template: >- {{ .ProjectName }}_{{ .Version }}_ @@ -121,6 +131,28 @@ nfpms: file_name_template: >- {{ .PackageName }}-{{ .Version }}. {{- if eq .Arch "386" }}i386{{- else if eq .Arch "amd64" }}x86_64{{- else }}{{ .Arch }}{{ end }} + - id: notifier-pkg + package_name: mytoken-notifier-server + file_name_template: >- + {{ .PackageName }}_{{ .Version }}_ + {{- if eq .Arch "386" }}i386{{- else }}{{ .Arch }}{{ end }} + builds: + - notifier-server + homepage: https://mytoken-docs.data.kit.edu/server + maintainer: Gabriel Zachmann + description: A standalone notifier server for mytoken notifications + license: MIT + formats: + - deb + - rpm + release: "1" + section: misc + bindir: /usr/bin + overrides: + rpm: + file_name_template: >- + {{ .PackageName }}-{{ .Version }}. + {{- if eq .Arch "386" }}i386{{- else if eq .Arch "amd64" }}x86_64{{- else }}{{ .Arch }}{{ end }} dockers: - goos: linux goarch: amd64 @@ -128,6 +160,7 @@ dockers: - server image_templates: - "oidcmytoken/mytoken-server:unstable" + - "oidcmytoken/mytoken-server:prerel" - "oidcmytoken/mytoken-server:{{ .Tag }}" dockerfile: cmd/mytoken-server/Dockerfile build_flag_templates: @@ -142,6 +175,7 @@ dockers: - setup image_templates: - "oidcmytoken/mytoken-setup:unstable" + - "oidcmytoken/mytoken-setup:prerel" - "oidcmytoken/mytoken-setup:{{ .Tag }}" dockerfile: cmd/mytoken-server/mytoken-setup/Dockerfile build_flag_templates: @@ -156,6 +190,7 @@ dockers: - migratedb image_templates: - "oidcmytoken/mytoken-migratedb:unstable" + - "oidcmytoken/mytoken-migratedb:prerel" - "oidcmytoken/mytoken-migratedb:{{ .Tag }}" dockerfile: cmd/mytoken-server/mytoken-migratedb/Dockerfile build_flag_templates: @@ -164,6 +199,21 @@ dockers: - "--label=org.opencontainers.image.title=mytoken-migratedb" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" + - goos: linux + goarch: amd64 + ids: + - notifier-server + image_templates: + - "oidcmytoken/mytoken-notifier-server:unstable" + - "oidcmytoken/mytoken-notifier-server:prerel" + - "oidcmytoken/mytoken-notifier-server:{{ .Tag }}" + dockerfile: cmd/mytoken-server/mytoken-notifier-server/Dockerfile + build_flag_templates: + - "--pull" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title=mytoken-notifier-server" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" checksum: name_template: 'checksums.txt' snapshot: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aee3ddb..0b88b120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,59 @@ + +## mytoken 0.10.0 + +### Features + +- Add support for notifications: + - Allows to create email notifications for various things + - Allows to calendar invites for token expirations + - Allows to create calendars and add token expirations to it; the ics feed can be subscribed to + - Allows to manage notifications on the web-interface +- Add "Enforceable Restrictions" + - Depending on a user attribute different restriction templates can be + enforced +- Add possibility to have an healthcheck endpoint + +### Enhancements + +- In the tokeninfo pane in the webinterface expired JWTs now get a more precise badge. +- Improved on returning json errors instead of html on api paths +- When not being logged in and no OP was selected now the 'Create new Mytoken' button in the webinterface is disabled. + +### Bugfixes + +- Fixed an issue with parallel access to refresh tokens if token rotation is used; this problem could for example + occur with EGI-checkin. +- Fixed unwanted behavior: If a profile was used and changes to the mytoken + spec would be made in the consent screen that would narrow it down, the + profile would still be applied. +- Fixed problems with the caching implementation. + +### Other + +- Changed CORP settings for `/api` and `/static` as this lead to problems with oidc-agent. + +### Dependencies + +- Bump go version from 1.19 to 1.22 +- Bump github.com/coreos/go-oidc/v3 from 3.9.0 to 3.11.0 +- Bump github.com/gliderlabs/ssh from 0.3.6 to 0.3.7 +- Bump github.com/go-resty/resty/v2 from 2.11.0 to 2.16.2 +- Bump github.com/go-sql-driver/mysql from 1.8.0 to 1.8.1 +- Bump github.com/gofiber/fiber/v2 from 2.52.2 to 2.52.5 +- Bump github.com/gofiber/template/mustache/v2 from 2.0.9 to 2.0.12 +- Bump github.com/jmoiron/sqlx from 1.3.5 to 1.4.0 +- Bump github.com/lestrrat-go/jwx from 1.2.29 to 1.2.30 +- Bump github.com/pires/go-proxyproto from 0.7.0 to 0.8.0 +- Bump github.com/redis/go-redis/v9 from 9.5.1 to 9.7.0 +- Bump github.com/valyala/fasthttp from 1.52.0 to 1.57.0 +- Bump golang.org/x/crypto from 0.21.0 to 0.30.0 +- Bump golang.org/x/mod from 0.16.0 to 0.22.0 +- Bump golang.org/x/oauth2 from 0.18.0 to 0.24.0 +- Bump golang.org/x/term from 0.18.0 to 0.27.0 + ## mytoken 0.9.2 ### Packaging @@ -28,12 +81,12 @@ - Bump golang.org/x/mod from 0.15.0 to 0.16.0 - Bump golang.org/x/oauth2 from 0.17.0 to 0.18.0 - Bump golang.org/x/term from 0.17.0 to 0.18.0 - +- ## mytoken 0.9.1 ### Enhancements -- Improfile includes handling in the webitnerface restrictions editor. +- Improve includes handling in the webinterface restrictions editor. ### Dependencies diff --git a/README.md b/README.md index 2350d7db..52af6218 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,9 @@ privileges. `Mytoken` focuses on integration with the command line through our [command line client](https://github.com/oidc-mytoken/client) -but also offers a web interface. A demo instance is available +but also offers a web interface. A demo instance (using the prerel branch) is available at [https://mytoken.data.kit.edu/](https://mytoken.data.kit.edu/). +For Production use [https://mytok.eu](https://mytok.eu) `Mytoken` is a central web service with the goal to easily obtain OpenID Connect access tokens across devices. Documentation is available at [https://mytoken-docs.data.kit.edu/](https://mytoken-docs.data.kit.edu/). diff --git a/cmd/mytoken-server/main.go b/cmd/mytoken-server/main.go index 698fe307..5388e799 100644 --- a/cmd/mytoken-server/main.go +++ b/cmd/mytoken-server/main.go @@ -16,9 +16,11 @@ import ( "github.com/oidc-mytoken/server/internal/endpoints/settings" "github.com/oidc-mytoken/server/internal/jws" "github.com/oidc-mytoken/server/internal/model/version" + notifier "github.com/oidc-mytoken/server/internal/notifier/client" "github.com/oidc-mytoken/server/internal/oidc/oidcfed" provider2 "github.com/oidc-mytoken/server/internal/oidc/provider" "github.com/oidc-mytoken/server/internal/server" + "github.com/oidc-mytoken/server/internal/server/healthcheck" "github.com/oidc-mytoken/server/internal/server/routes" "github.com/oidc-mytoken/server/internal/utils/cache" "github.com/oidc-mytoken/server/internal/utils/cookies" @@ -42,7 +44,8 @@ func main() { geoip.Init() settings.InitSettings() cookies.Init() - + notifier.Init() + healthcheck.Start() server.Start() } diff --git a/cmd/mytoken-server/mytoken-notifier-server/Dockerfile b/cmd/mytoken-server/mytoken-notifier-server/Dockerfile new file mode 100644 index 00000000..edf0484b --- /dev/null +++ b/cmd/mytoken-server/mytoken-notifier-server/Dockerfile @@ -0,0 +1,5 @@ +FROM debian:stable +WORKDIR /mytoken +COPY mytoken-notifier-server /usr/bin/mytoken-notifier-server +USER 1000:1000 +ENTRYPOINT ["mytoken-notifier-server"] diff --git a/cmd/mytoken-server/mytoken-notifier-server/config.go b/cmd/mytoken-server/mytoken-notifier-server/config.go new file mode 100644 index 00000000..54e8ec49 --- /dev/null +++ b/cmd/mytoken-server/mytoken-notifier-server/config.go @@ -0,0 +1,29 @@ +package main + +import ( + "github.com/oidc-mytoken/utils/utils/fileutil" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" + + "github.com/oidc-mytoken/server/internal/config" +) + +var possibleConfigLocations = []string{ + "config", + "/etc/mytoken", +} + +type conff struct { + Email config.MailNotificationConf `yaml:"email"` +} + +var conf conff + +func loadConfig() { + data, _ := fileutil.MustReadConfigFile("notifier-config.yaml", possibleConfigLocations) + err := yaml.Unmarshal(data, &conf) + if err != nil { + log.WithError(err).Fatal() + return + } +} diff --git a/cmd/mytoken-server/mytoken-notifier-server/main.go b/cmd/mytoken-server/mytoken-notifier-server/main.go new file mode 100644 index 00000000..9235f646 --- /dev/null +++ b/cmd/mytoken-server/mytoken-notifier-server/main.go @@ -0,0 +1,10 @@ +package main + +import ( + server "github.com/oidc-mytoken/server/internal/notifier/server" +) + +func main() { + loadConfig() + server.InitStandalone(conf.Email) +} diff --git a/config/example-config.yaml b/config/example-config.yaml index c6b3111e..ea1e9e77 100644 --- a/config/example-config.yaml +++ b/config/example-config.yaml @@ -6,7 +6,7 @@ service_operator: # The name of the service operator name: "Example Foundation" homepage: "https://example.com" - # An email address where users can contact the service operator. It is recommend to use a email list for this + # An email address where users can contact the service operator. It is recommended to use an email list for this mail_contact: "mytoken@example.com" # A dedicated email address for privacy related topics can be given. # If you do not have a dedicated mail for privacy, it can be omitted. @@ -25,9 +25,14 @@ server: cert: # The TLS certificate key file key: + # If behind a load balancer or reverse proxy and TLS is set on that and not on the mytoken server set this option + # to 'true' + ##secure: true # If behind a load balancer or reverse proxy, set this option. # Mytoken will read the client's ip address from this header. - # proxy_header: "X-FORWARDED-FOR" + ##proxy_header: "X-FORWARDED-FOR" + # If you run the mytoken server in a distributed setup with multiple instances set this option to 'true' + distributed_servers: false # Configure the request limits (these are per IP) request_limits: # Unless false request limits are enabled @@ -41,8 +46,11 @@ server: # hostnames including wildcards. always_allow: - "127.0.0.1" + healthcheck: + enabled: true + port: 9876 -# The database file for ip geo location. Will be installed by setup to this location. +# The database file for ip geolocation. Will be installed by setup to this location. geo_ip_db_file: "/IP2LOCATION-LITE-DB1.IPV6.BIN" # Configuration of the mytoken API @@ -120,7 +128,7 @@ logging: # The minimal log level that should be logged level: "error" # Configuration for 'smart' logging. If enabled internal logging is usually logged from the minimal log level as - # usual, but if an error occurs an special error log is logged to a dedicated file that contains all log entries + # usual, but if an error occurs a special error log is logged to a dedicated file that contains all log entries # (with all log levels, also levels that normally wouldn't be logged) for that particular request. smart: # Unless false smart logging is enabled @@ -230,6 +238,30 @@ features: groups: # _: admin + # If true a guest mode is enabled that allows to obtain mytokens without further auth, ATs are obviously dummy + guest_mode: false + + # Settings related to the notifications feature + notifications: + # If a standalone notifier server is used (required in a distributed setup), specify the base url here + notifier_server_url: + # Enables calendar support + ics: + enabled: true + # Email notifications + email: + enabled: true + # Settings for the used mailserver, only used if the integrated notifier is used + mail_server: + host: + port: 25 + user: + password: + from_address: + # Not yet implemented + ws: + enabled: true + # Configuration for usage of OpenID Federations federation: enabled: false @@ -265,4 +297,34 @@ providers: request_parameter: "resource" # Defines how multiple audience values in a request are handled; space_separate_auds: false + # Settings related to restrictions that should be enforced for different user groups depending on an OP attribute + enforced_restrictions: + # A mapping for claim sources: Mapping between an url and claim name; defines how the claim value is obtained + # on which the decision which restriction template is enforced is based on + # The key should be an url, that behaves in line with an OP's userinfo endpoint + # The special keys 'issuer', 'op', 'default', and 'userinfo' can be used; + # if one of these keys is used, the claim name is looked up in the + # id token, access token, and userinfo endpoint + claim_sources: + #issuer: "eduperson_entitlements" + # If true, indicates that access should be completely forbidden if no value from 'mapping' matches + #forbid_on_default: false + # The default restriction template that will be enforced for all users where no value from 'mapping' matches; + # only used if 'forbid_on_default=false'; the restriction template must be configured on the server or be + # provided as json (but as a string) + # The help_html content is displayed if forbid_on_default=true and a user tries to log in and does not have + # access. The html should contain information about why the user does not have access and what to do in order + # to get access. The html can also be given in the help_html_file. This should only be a html snippet, no + # complete html page. + #help_html: + #help_html_file: + #default_template: web-default + # A mapping between claim values and the enforced restriction template; the templates must be configured on the + # server or be provided as json (but as a string); an entry matches if the attribute is a string array and the + # mapping key is included in the array or the attribute is a single value and equals the mapping key; + # the mapping MUST be given in order such that the highest privileged template is listed first + mapping: + #admin: "" + #urn:geant:mytoken:advanced: "advanced" + #urn:geant:mytoken:medium: "medium" diff --git a/config/example-notifier-config.yaml b/config/example-notifier-config.yaml new file mode 100644 index 00000000..475a96b8 --- /dev/null +++ b/config/example-notifier-config.yaml @@ -0,0 +1,14 @@ +# This config file is used by a standalone notifier server +# It is not required for usage of the integrated notifier +# Obviously this has to be placed on the machine running the standalone notifier server + +email: + enabled: true + mail_server: + host: + port: 25 + user: + password: + from_address: + # Directory path to overwrite email templates + overwrite_dir: \ No newline at end of file diff --git a/config/server-profiles/rotation/AT b/config/server-profiles/rotation/AT deleted file mode 100644 index 95e8c1f9..00000000 --- a/config/server-profiles/rotation/AT +++ /dev/null @@ -1,3 +0,0 @@ -{ - "include": "at" -} \ No newline at end of file diff --git a/docker/example-docker-config.yaml b/docker/example-docker-config.yaml index 91168504..d48c88fd 100644 --- a/docker/example-docker-config.yaml +++ b/docker/example-docker-config.yaml @@ -6,7 +6,7 @@ service_operator: # The name of the service operator name: "Example Foundation" homepage: "https://example.com" - # An email address where users can contact the service operator. It is recommend to use a email list for this + # An email address where users can contact the service operator. It is recommended to use an email list for this mail_contact: "mytoken@example.com" # A dedicated email address for privacy related topics can be given. If you do not have a dedicated mail for privacy, it can be omitted. mail_privacy: "mytoken-privacy@example.com" @@ -57,7 +57,7 @@ database: # Enable / Disable cleanup of expired db entries once a day schedule_cleanup: true -# The database file for ip geo location. Will be installed by setup to this location. +# The database file for ip geolocation. Will be installed by setup to this location. geo_ip_db_file: "/mytoken/IP2LOCATION-LITE-DB1.IPV6.BIN" # Configuration for token signing @@ -86,7 +86,7 @@ logging: # The minimal log level that should be logged level: "error" # Configuration for 'smart' logging. If enabled internal logging is usually logged from the minimal log level as - # usual, but if an error occurs an special error log is logged to a dedicated file that contains all log entries + # usual, but if an error occurs a special error log is logged to a dedicated file that contains all log entries # (with all log levels, also levels that normally wouldn't be logged) for that particular request. smart: # Unless false smart logging is enabled @@ -161,7 +161,8 @@ features: expires_after: 300 # The time in seconds how long a polling code can be used polling_interval: 5 # The interval in seconds the native application should wait between two polling attempts - # Support for rotation mytokens; users can enable rotation/disable rotation for their mytokens; if enabled a new mytoken will be returned after each usages and old mytokens can no longer be used. + # Support for rotation mytokens; users can enable rotation/disable rotation for their mytokens; + # if enabled a new mytoken will be returned after each usage and old mytokens can no longer be used. token_rotation: enabled: true diff --git a/go.mod b/go.mod index 896ab33f..7e5feb89 100644 --- a/go.mod +++ b/go.mod @@ -1,59 +1,63 @@ module github.com/oidc-mytoken/server -go 1.19 +go 1.22.0 + +toolchain go1.22.5 require ( github.com/Songmu/prompter v0.5.1 - github.com/coreos/go-oidc/v3 v3.9.0 + github.com/TwiN/gocache/v2 v2.2.2 + github.com/arran4/golang-ical v0.3.1 + github.com/coreos/go-oidc/v3 v3.11.0 github.com/evanphx/json-patch/v5 v5.9.0 github.com/fatih/structs v1.1.0 - github.com/gliderlabs/ssh v0.3.6 - github.com/go-resty/resty/v2 v2.11.0 - github.com/go-sql-driver/mysql v1.8.0 - github.com/gofiber/fiber/v2 v2.52.2 - github.com/gofiber/helmet/v2 v2.2.26 - github.com/gofiber/template/mustache/v2 v2.0.9 + github.com/gliderlabs/ssh v0.3.7 + github.com/go-resty/resty/v2 v2.16.2 + github.com/go-sql-driver/mysql v1.8.1 + github.com/gofiber/fiber/v2 v2.52.5 + github.com/gofiber/template/mustache/v2 v2.0.12 github.com/gofrs/uuid v4.4.0+incompatible github.com/golang-jwt/jwt v3.2.2+incompatible github.com/ip2location/ip2location-go v8.3.0+incompatible github.com/jinzhu/copier v0.4.0 - github.com/jmoiron/sqlx v1.3.5 - github.com/lestrrat-go/jwx v1.2.29 - github.com/oidc-mytoken/api v0.11.2-0.20230810083726-bf164306e5b2 + github.com/jmoiron/sqlx v1.4.0 + github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible + github.com/lestrrat-go/jwx v1.2.30 + github.com/oidc-mytoken/api v0.11.2-0.20240426092102-fa4d583a79ad github.com/oidc-mytoken/lib v0.7.1 - github.com/oidc-mytoken/utils v0.1.3-0.20230731143919-ea5b78243e5d - github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/pires/go-proxyproto v0.7.0 + github.com/oidc-mytoken/utils v0.1.3-0.20240527155944-26103774a5aa + github.com/olekukonko/tablewriter v0.0.5 + github.com/pires/go-proxyproto v0.8.0 github.com/pkg/errors v0.9.1 - github.com/redis/go-redis/v9 v9.5.1 + github.com/redis/go-redis/v9 v9.7.0 + github.com/sethvargo/go-limiter v1.0.0 github.com/sirupsen/logrus v1.9.3 github.com/urfave/cli/v2 v2.3.1-0.20211205195634-e8d81738896c - github.com/valyala/fasthttp v1.52.0 - github.com/zachmann/go-oidcfed v0.1.0 - golang.org/x/crypto v0.21.0 - golang.org/x/mod v0.16.0 - golang.org/x/oauth2 v0.18.0 - golang.org/x/term v0.18.0 + github.com/valyala/fasthttp v1.57.0 + github.com/vmihailenco/msgpack/v5 v5.4.1 + github.com/zachmann/go-oidfed v0.1.1-0.20240830095406-169de417a975 + golang.org/x/crypto v0.30.0 + golang.org/x/mod v0.22.0 + golang.org/x/oauth2 v0.24.0 + golang.org/x/term v0.27.0 gopkg.in/yaml.v3 v3.0.1 ) require ( filippo.io/edwards25519 v1.1.0 // indirect - github.com/adam-hanna/arrayOperations v1.0.1 // indirect - github.com/andybalholm/brotli v1.1.0 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/cbroglie/mustache v1.4.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/go-jose/go-jose/v3 v3.0.3 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/go-jose/go-jose/v4 v4.0.2 // indirect + github.com/goccy/go-json v0.10.3 // indirect github.com/gofiber/template v1.8.3 // indirect github.com/gofiber/utils v1.1.0 // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/uuid v1.5.0 // indirect - github.com/klauspost/compress v1.17.6 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect @@ -68,10 +72,11 @@ require ( github.com/tinylib/msgp v1.1.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/net v0.22.0 // indirect - golang.org/x/sys v0.18.0 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.33.0 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.28.0 // indirect + tideland.dev/go/slices v0.2.0 // indirect ) replace github.com/urfave/cli/v2 => github.com/zachmann/cli/v2 v2.3.1-0.20211220102037-d619fd40a704 diff --git a/go.sum b/go.sum index 351213c7..3439d0ff 100644 --- a/go.sum +++ b/go.sum @@ -1,261 +1,83 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= -cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Songmu/prompter v0.5.1 h1:IAsttKsOZWSDw7bV1mtGn9TAmLFAjXbp9I/eYmUUogo= github.com/Songmu/prompter v0.5.1/go.mod h1:CS3jEPD6h9IaLaG6afrl1orTgII9+uDWuw95dr6xHSw= +github.com/TwiN/gocache/v2 v2.2.2 h1:4HToPfDV8FSbaYO5kkbhLpEllUYse5rAf+hVU/mSsuI= +github.com/TwiN/gocache/v2 v2.2.2/go.mod h1:WfIuwd7GR82/7EfQqEtmLFC3a2vqaKbs4Pe6neB7Gyc= github.com/adam-hanna/arrayOperations v1.0.1 h1:iAot3I2p4yKrFk8eRhEkuHj0ttOrfFJMWAo7Is/rHwk= github.com/adam-hanna/arrayOperations v1.0.1/go.mod h1:nScFkGwh89OyLY/cnXdx/S1maSqxhSXz38so1JxsChQ= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/arran4/golang-ical v0.3.1 h1:v13B3eQZ9VDHTAvT6M11vVzxYgcYmjyPBE2eAZl3VZk= +github.com/arran4/golang-ical v0.3.1/go.mod h1:LZWxF8ZIu/sjBVUCV0udiVPrQAgq3V0aa0RfbO99Qkk= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cbroglie/mustache v1.4.0 h1:Azg0dVhxTml5me+7PsZ7WPrQq1Gkf3WApcHMjMprYoU= github.com/cbroglie/mustache v1.4.0/go.mod h1:SS1FTIghy0sjse4DUVGV1k/40B1qE1XkD9DtDsHo9iM= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/coreos/go-oidc/v3 v3.4.0/go.mod h1:eHUXhZtXPQLgEaDrOVTgwbgmz1xGOkJNye6h3zkD2Pw= -github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= -github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= +github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= +github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.3.6 h1:ZzjlDa05TcFRICb3anf/dSPN3ewz1Zx6CMLPWgkm3b8= -github.com/gliderlabs/ssh v0.3.6/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= -github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= -github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= -github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4= -github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDaxEo= -github.com/gofiber/fiber/v2 v2.52.2/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= -github.com/gofiber/helmet/v2 v2.2.26 h1:KreQVUpCIGppPQ6Yt8qQMaIR4fVXMnvBdsda0dJSsO8= -github.com/gofiber/helmet/v2 v2.2.26/go.mod h1:XE0DF4cgf0M5xIt7qyAK5zOi8jJblhxfSDv9DAmEEQo= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg= +github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= +github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc= github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8= -github.com/gofiber/template/mustache/v2 v2.0.9 h1:KfVHg7S6WlEGv28ZH/FiO/0sI5hmcvFBwhjyKwqklkg= -github.com/gofiber/template/mustache/v2 v2.0.9/go.mod h1:Qd/xaSAVd0NHGEFrqOEwt+vuEEIW2II2+M8ElZXtEPc= +github.com/gofiber/template/mustache/v2 v2.0.12 h1:AUZmr5exKu3Efkef/l+TZjpP8e1o+dgqAtoONhcmE4w= +github.com/gofiber/template/mustache/v2 v2.0.12/go.mod h1:8NoF3AVoxvefK3kEH+0wcqM9k50YerDyccfnVMvoM5c= github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/ip2location/ip2location-go v8.3.0+incompatible h1:QwUE+FlSbo6bjOWZpv2Grb57vJhWYFNPyBj2KCvfWaM= github.com/ip2location/ip2location-go v8.3.0+incompatible/go.mod h1:3JUY1TBjTx1GdA7oRT7Zeqfc0bg3lMMuU5lXmzdpuME= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= -github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= -github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= @@ -264,591 +86,142 @@ github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZ github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx v1.2.29 h1:QT0utmUJ4/12rmsVQrJ3u55bycPkKqGYuGT4tyRhxSQ= -github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8= +github.com/lestrrat-go/jwx v1.2.30 h1:VKIFrmjYn0z2J51iLPadqoHIVLzvWNa1kCsTqNDHYPA= +github.com/lestrrat-go/jwx v1.2.30/go.mod h1:vMxrwFhunGZ3qddmfmEm2+uced8MSI6QFWGTKygjSzQ= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/luci/go-render v0.0.0-20160219211803-9a04cc21af0f h1:WVPqVsbUsrzAebTEgWRAZMdDOfkFx06iyhbIoyMgtkE= +github.com/luci/go-render v0.0.0-20160219211803-9a04cc21af0f/go.mod h1:aS446i8akEg0DAtNKTVYpNpLPMc0SzsZ0RtGhjl0uFM= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/oidc-mytoken/api v0.9.1/go.mod h1:DBIlUbaIgGlf607VZx8zFC97VR3WNN0kaMVO1AqyTdE= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oidc-mytoken/api v0.11.1/go.mod h1:bd7obYvztiIQW1PoRVBTOg8/clWlauNGwcZEu5mRbwg= -github.com/oidc-mytoken/api v0.11.2-0.20230810083726-bf164306e5b2 h1:GY8oBE+ZCEWUIfR2DdRqSjHdqTWWv38CsBqRO4EW+QA= -github.com/oidc-mytoken/api v0.11.2-0.20230810083726-bf164306e5b2/go.mod h1:E4nGP5D9575NtqQohVaC41rxRjoXvwtJi72aoIO2uUk= +github.com/oidc-mytoken/api v0.11.2-0.20240426092102-fa4d583a79ad h1:jFJqhdX7BqiL4y9yURp48roCVv3p8n18c1J188T6Jz8= +github.com/oidc-mytoken/api v0.11.2-0.20240426092102-fa4d583a79ad/go.mod h1:4QwDXesKKEzbTmH2vENYCVcmdb5/gbDPg3DOm9HQLdg= github.com/oidc-mytoken/lib v0.7.1 h1:inqnng/iVU42TU8Cuac7wP8I+JraTYmWMKSXdZvShfU= github.com/oidc-mytoken/lib v0.7.1/go.mod h1:LLDCpHCFzyodYLWSf69FDI4cvql/8B2sdKhVjmOBPgI= -github.com/oidc-mytoken/utils v0.1.3-0.20230731143919-ea5b78243e5d h1:6FH7MuDNSDWxhSzdzim7qeM4XASDb8TL4nytZAZLpBc= -github.com/oidc-mytoken/utils v0.1.3-0.20230731143919-ea5b78243e5d/go.mod h1:pAUlvF/mYz8CjinCx4iwoWzG514B9EhuCpu7O+afaD8= -github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= -github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/oidc-mytoken/utils v0.1.3-0.20240527155944-26103774a5aa h1:Ff2ebDPyHdxTIx95G5hD5PuS8o495JIwu4a2lsQ9sX8= +github.com/oidc-mytoken/utils v0.1.3-0.20240527155944-26103774a5aa/go.mod h1:PH/hvZP5HjEQFF/l/ooPj0MH0yhpqO4sEF9V33+cGL0= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= -github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= -github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0= +github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= -github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4= +github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.41.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY= -github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0= -github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= +github.com/valyala/fasthttp v1.57.0 h1:Xw8SjWGEP/+wAAgyy5XTvgrWlOD1+TxbbvNADYCm1Tg= +github.com/valyala/fasthttp v1.57.0/go.mod h1:h6ZBaPRlzpZ6O3H5t2gEk1Qi33+TmLvfwgLLp0t9CpE= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zachmann/cli/v2 v2.3.1-0.20211220102037-d619fd40a704 h1:dpR/K16zQgc/5xTQ66RevZikjkFzsCt6IeNDFyHltaM= github.com/zachmann/cli/v2 v2.3.1-0.20211220102037-d619fd40a704/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/zachmann/go-oidcfed v0.1.0 h1:dK31uH4msAcBwEgERcZaM85kuZQh3hncTmpQagBPEB0= -github.com/zachmann/go-oidcfed v0.1.0/go.mod h1:uBYe4JAsffqQmXL4tNNensujX9gIypIgj4OllxJdTVY= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +github.com/zachmann/go-oidfed v0.1.1-0.20240830095406-169de417a975 h1:0CXmuRhdTsEGVIRhzRY9UYHkDpbyh2SxKfh3r60GAV0= +github.com/zachmann/go-oidfed v0.1.1-0.20240830095406-169de417a975/go.mod h1:NZbSbNWnTk7kgc/8Bd5bVDMyWVh7lUAbWvpCHZV0bnQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= -golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= -golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= -google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= -google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= -google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +tideland.dev/go/audit v0.7.0 h1:lr4LkNu7i5qLJuqQ6lUfnt0J09anZNfrdXdB1I9JlTs= +tideland.dev/go/audit v0.7.0/go.mod h1:Jua+IB3KgAC7fbuZ1YHT7gKhwpiTOcn3Q7AOCQsrro8= +tideland.dev/go/slices v0.2.0 h1:OHOZCscL9R0KUqxezLkTmu+iEbQQ7ZN5ermFR4ElGhg= +tideland.dev/go/slices v0.2.0/go.mod h1:jgHyW6qZmZe0KPuX7JOpcb6D+pq63w6z2FCLqDkHP7U= diff --git a/internal/config/config.go b/internal/config/config.go index 198e58fb..2fa94b27 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,7 +12,7 @@ import ( "github.com/oidc-mytoken/utils/utils/fileutil" "github.com/pkg/errors" log "github.com/sirupsen/logrus" - oidcfed "github.com/zachmann/go-oidcfed/pkg" + oidfed "github.com/zachmann/go-oidfed/pkg" "golang.org/x/crypto/ssh" "gopkg.in/yaml.v3" @@ -109,6 +109,15 @@ var defaultConfig = Config{ Enabled: true, Groups: make(map[string]string), }, + Notifications: notificationConf{ + Mail: MailNotificationConf{ + Enabled: false, + MailServer: MailServerConf{ + Port: 587, + }, + }, + ICS: onlyEnable{true}, + }, Federation: federationConf{ Enabled: false, EntityConfigurationLifetime: 7 * 24 * 60 * 60, @@ -141,7 +150,7 @@ type Config struct { Logging loggingConf `yaml:"logging"` ServiceDocumentation string `yaml:"service_documentation"` Features featuresConf `yaml:"features"` - Providers []ProviderConf `yaml:"providers"` + Providers []*ProviderConf `yaml:"providers"` ServiceOperator ServiceOperatorConf `yaml:"service_operator"` Caching cacheConf `yaml:"cache"` } @@ -164,6 +173,7 @@ type featuresConf struct { ServerProfiles serverProfilesConf `yaml:"server_profiles"` Federation federationConf `yaml:"federation"` GuestMode onlyEnable `yaml:"guest_mode"` + Notifications notificationConf `yaml:"notifications"` } func (c *featuresConf) validate() error { @@ -176,6 +186,12 @@ func (c *featuresConf) validate() error { if err := c.Federation.validate(); err != nil { return err } + if err := c.Notifications.validate(); err != nil { + return err + } + if err := c.SSH.validate(); err != nil { + return err + } return nil } @@ -217,6 +233,27 @@ type sshConf struct { PrivateKeys []ssh.Signer `yaml:"-"` } +func (c *sshConf) validate() error { + if !c.Enabled { + return nil + } + if len(c.KeyFiles) == 0 { + return errors.New("invalid config: ssh feature enabled, but no ssh private key set") + } + for _, pkf := range c.KeyFiles { + pemBytes, err := os.ReadFile(pkf) + if err != nil { + return errors.Wrap(err, "reading ssh private key") + } + signer, err := ssh.ParsePrivateKey(pemBytes) + if err != nil { + return errors.Wrap(err, "parsing ssh private key") + } + c.PrivateKeys = append(c.PrivateKeys, signer) + } + return nil +} + type serverProfilesConf struct { Enabled bool `yaml:"enabled"` Groups profileGroupsCredentials `yaml:"groups"` @@ -241,6 +278,50 @@ func (g profileGroupsCredentials) validate() error { return nil } +type notificationConf struct { + AnyEnabled bool `yaml:"-"` + Mail MailNotificationConf `yaml:"email"` + Websocket onlyEnable `yaml:"ws"` + ICS onlyEnable `yaml:"ics"` + NotifierServer string `yaml:"notifier_server_url"` +} + +func (c *notificationConf) validate() error { + c.AnyEnabled = c.Mail.Enabled || c.Websocket.Enabled || c.ICS.Enabled + if !c.AnyEnabled { + return nil + } + if conf.Server.DistributedServers { + if c.NotifierServer == "" { + return errors.New("distributed deployment, but no notifier_server_url set") + } + } + if c.NotifierServer != "" { + if c.Mail.MailServer.Host != "" { + log.Warning( + "a standalone notifier server is used; however mail_server configuration is given here", + ) + } + } + return nil +} + +// MailNotificationConf holds the configuration for email notifications +type MailNotificationConf struct { + Enabled bool `yaml:"enabled"` + MailServer MailServerConf `yaml:"mail_server"` + OverwriteDir string `yaml:"overwrite_dir"` +} + +// MailServerConf holds the configuration for the email server +type MailServerConf struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + Username string `yaml:"user"` + Password string `yaml:"password"` + FromAddress string `yaml:"from_address"` +} + type tokeninfoConfig struct { Enabled bool `yaml:"-"` Introspect onlyEnable `yaml:"introspect"` @@ -333,8 +414,15 @@ type serverConf struct { TLS tlsConf `yaml:"tls"` Secure bool `yaml:"-"` // Secure indicates if the connection to the mytoken server is secure. This is // independent of TLS, e.g. a Proxy can be used. - ProxyHeader string `yaml:"proxy_header"` - Limiter limiterConf `yaml:"request_limits"` + ProxyHeader string `yaml:"proxy_header"` + Limiter limiterConf `yaml:"request_limits"` + DistributedServers bool `yaml:"distributed_servers"` + Healthcheck healtcheckConfig `yaml:"healthcheck"` +} + +type healtcheckConfig struct { + Enabled bool `yaml:"enabled"` + Port int `yaml:"port"` } type limiterConf struct { @@ -364,14 +452,43 @@ type signingConf struct { // ProviderConf holds information about a provider type ProviderConf struct { - Issuer string `yaml:"issuer"` - ClientID string `yaml:"client_id"` - ClientSecret string `yaml:"client_secret"` - Scopes []string `yaml:"scopes"` - MytokensMaxLifetime int64 `yaml:"mytokens_max_lifetime"` - Endpoints *oauth2x.Endpoints `yaml:"-"` - Name string `yaml:"name"` - Audience *model.AudienceConf `yaml:"audience"` + Issuer string `yaml:"issuer"` + ClientID string `yaml:"client_id"` + ClientSecret string `yaml:"client_secret"` + Scopes []string `yaml:"scopes"` + MytokensMaxLifetime int64 `yaml:"mytokens_max_lifetime"` + EnforcedRestrictions EnforcedRestrictionsConf `yaml:"enforced_restrictions"` + Endpoints *oauth2x.Endpoints `yaml:"-"` + Name string `yaml:"name"` + Audience *model.AudienceConf `yaml:"audience"` +} + +// EnforcedRestrictionsConf is a type for holding configuration for enforced restrictions +type EnforcedRestrictionsConf struct { + Enabled bool `yaml:"-"` + ClaimSources map[string]string `yaml:"claim_sources"` + DefaultTemplate string `yaml:"default_template"` + ForbidOnDefault bool `yaml:"forbid_on_default"` + HelpHTMLText string `yaml:"help_html"` + HelpHTMLFile string `yaml:"help_html_file"` + Mapping map[string]string `yaml:"mapping"` +} + +func (c *EnforcedRestrictionsConf) validate() error { + if len(c.ClaimSources) >= 1 { + c.Enabled = true + } + if c.HelpHTMLFile != "" { + content, err := os.ReadFile(c.HelpHTMLFile) + if err != nil { + return errors.Wrapf( + err, + "error reading enforced restrictions help html file '%s'", c.HelpHTMLFile, + ) + } + c.HelpHTMLText = string(content) + } + return nil } // ServiceOperatorConf is type holding the configuration for the service operator of this mytoken instance @@ -431,12 +548,12 @@ func (so *ServiceOperatorConf) validate() error { } type federationConf struct { - Enabled bool `yaml:"enabled"` - TrustAnchors oidcfed.TrustAnchors `yaml:"trust_anchors"` - AuthorityHints []string `yaml:"authority_hints"` - EntityConfigurationLifetime int64 `yaml:"entity_configuration_lifetime"` - Signing signingConf `yaml:"signing"` - Entity *oidcfed.FederationLeaf `yaml:"-"` + Enabled bool `yaml:"enabled"` + TrustAnchors oidfed.TrustAnchors `yaml:"trust_anchors"` + AuthorityHints []string `yaml:"authority_hints"` + EntityConfigurationLifetime int64 `yaml:"entity_configuration_lifetime"` + Signing signingConf `yaml:"signing"` + Entity *oidfed.FederationLeaf `yaml:"-"` } func (f *federationConf) validate() (err error) { @@ -483,6 +600,28 @@ func validate() error { if conf == nil { return errors.New("config not set") } + if err := validateIssuerURL(); err != nil { + return err + } + if err := configureServerTLS(); err != nil { + return err + } + if err := validateConfigSections(); err != nil { + return err + } + if err := validateProviders(); err != nil { + return err + } + if conf.Features.GuestMode.Enabled { + addGuestModeProvider() + } + if err := validateSigningConfig(); err != nil { + return err + } + return validateWebInterface() +} + +func validateIssuerURL() error { if conf.IssuerURL == "" { return errors.New("invalid config: issuer_url not set") } @@ -495,6 +634,10 @@ func validate() error { return errors.Wrap(err, "invalid config: issuer_url not valid") } conf.Host = u.Hostname() + return nil +} + +func configureServerTLS() error { if conf.Server.TLS.Enabled { if conf.Server.TLS.Key != "" && conf.Server.TLS.Cert != "" { conf.Server.Port = 443 @@ -502,87 +645,95 @@ func validate() error { conf.Server.TLS.Enabled = false } } - if err = conf.Logging.validate(); err != nil { - return err - } - if err = conf.ServiceOperator.validate(); err != nil { + return nil +} + +func validateConfigSections() error { + if err := conf.Logging.validate(); err != nil { return err } - if err = conf.Features.validate(); err != nil { + + if err := conf.ServiceOperator.validate(); err != nil { return err } - if len(conf.Providers) <= 0 { + + return conf.Features.validate() +} + +func validateProviders() error { + if len(conf.Providers) == 0 { return errors.New("invalid config: providers must have at least one entry") } for i, p := range conf.Providers { - if p.Issuer == "" { - return errors.Errorf("invalid config: provider.issuer not set (Index %d)", i) - } - oc, err := oauth2x.NewConfig(context.Get(), p.Issuer) - if err != nil { - return errors.Errorf("error '%s' for provider.issuer '%s' (Index %d)", err, p.Issuer, i) - } - // Endpoints only returns an error if it does discovery but this was already done in NewConfig, so we can ignore - // the error value - p.Endpoints, _ = oc.Endpoints() - if p.ClientID == "" { - return errors.Errorf("invalid config: provider.clientid not set (Index %d)", i) - } - if p.ClientSecret == "" { - return errors.Errorf("invalid config: provider.clientsecret not set (Index %d)", i) - } - if len(p.Scopes) <= 0 { - return errors.Errorf("invalid config: provider.scopes not set (Index %d)", i) - } - if p.Audience == nil { - p.Audience = &model.AudienceConf{RFC8707: true} - } - if p.Audience.RFC8707 { - p.Audience.RequestParameter = model.AudienceParameterResource - p.Audience.SpaceSeparateAuds = false - } else if p.Audience.RequestParameter == "" { - p.Audience.RequestParameter = model.AudienceParameterResource + if err := validateProvider(p, i); err != nil { + return err } conf.Providers[i] = p } - if conf.Features.GuestMode.Enabled { - iss := utils2.CombineURLPath(conf.IssuerURL, paths.GetCurrentAPIPaths().GuestModeOP) - p := ProviderConf{ - Issuer: iss, - Name: "Guest Mode", - Scopes: []string{"openid"}, - Endpoints: &oauth2x.Endpoints{ - Authorization: utils2.CombineURLPath(iss, "auth"), - Token: utils2.CombineURLPath(iss, "token"), - }, - } - conf.Providers = append(conf.Providers, p) + return nil +} + +func validateProvider(p *ProviderConf, i int) error { + if p.Issuer == "" { + return errors.Errorf("invalid config: provider.issuer not set (Index %d)", i) } - if conf.IssuerURL == "" { - return errors.New("invalid config: issuer_url not set") + if err := p.EnforcedRestrictions.validate(); err != nil { + return err + } + oc, err := oauth2x.NewConfig(context.Get(), p.Issuer) + if err != nil { + return errors.Errorf("error '%s' for provider.issuer '%s' (Index %d)", err, p.Issuer, i) + } + p.Endpoints, err = oc.Endpoints() + if err != nil { + return errors.Errorf("error '%s' for provider.issuer '%s' (Index %d)", err, p.Issuer, i) + } + if p.ClientID == "" { + return errors.Errorf("invalid config: provider.clientid not set (Index %d)", i) + } + if p.ClientSecret == "" { + return errors.Errorf("invalid config: provider.clientsecret not set (Index %d)", i) + } + if len(p.Scopes) == 0 { + return errors.Errorf("invalid config: provider.scopes not set (Index %d)", i) + } + if p.Audience == nil { + p.Audience = &model.AudienceConf{RFC8707: true} + } + if p.Audience.RFC8707 { + p.Audience.RequestParameter = model.AudienceParameterResource + p.Audience.SpaceSeparateAuds = false + } else if p.Audience.RequestParameter == "" { + p.Audience.RequestParameter = model.AudienceParameterResource } + return nil +} + +func addGuestModeProvider() { + iss := utils2.CombineURLPath(conf.IssuerURL, paths.GetCurrentAPIPaths().GuestModeOP) + p := &ProviderConf{ + Issuer: iss, + Name: "Guest Mode", + Scopes: []string{"openid"}, + Endpoints: &oauth2x.Endpoints{ + Authorization: utils2.CombineURLPath(iss, "auth"), + Token: utils2.CombineURLPath(iss, "token"), + }, + } + conf.Providers = append(conf.Providers, p) +} + +func validateSigningConfig() error { if conf.Signing.Mytoken.KeyFile == "" { return errors.New("invalid config: signing keyfile not set") } if conf.Signing.Mytoken.Alg == "" { return errors.New("invalid config: token signing alg not set") } - if conf.Features.SSH.Enabled { - if len(conf.Features.SSH.KeyFiles) == 0 { - return errors.New("invalid config: ssh feature enabled, but no ssh private key set") - } - for _, pkf := range conf.Features.SSH.KeyFiles { - pemBytes, err := os.ReadFile(pkf) - if err != nil { - return errors.Wrap(err, "reading ssh private key") - } - signer, err := ssh.ParsePrivateKey(pemBytes) - if err != nil { - return errors.Wrap(err, "parsing ssh private key") - } - conf.Features.SSH.PrivateKeys = append(conf.Features.SSH.PrivateKeys, signer) - } - } + return nil +} + +func validateWebInterface() error { if !conf.Features.TokenInfo.Introspect.Enabled && conf.Features.WebInterface.Enabled { return errors.New("web interface requires tokeninfo.introspect to be enabled") } diff --git a/internal/db/cluster/cluster.go b/internal/db/cluster/cluster.go index 5e8cb042..57eecaef 100644 --- a/internal/db/cluster/cluster.go +++ b/internal/db/cluster/cluster.go @@ -67,7 +67,7 @@ func (c *Cluster) AddNodes() { } } -// AddNode adds the passed host a a db node to the cluster +// AddNode adds the passed host to a db node to the cluster func (c *Cluster) AddNode(host string) error { log.WithField("host", host).Debug("Adding node to db cluster") return c.addNode( @@ -179,6 +179,9 @@ func (n *node) transact(rlog log.Ext1FieldLogger, fn func(*sqlx.Tx) error) (bool if err != nil { e := errorfmt.Error(err) switch { + case e == "Error 1047 (08S01): WSREP has not yet prepared node for application use": + rlog.WithField("host", n.host).Error("WSREP error on node") + return true, err case e == "sql: database is closed", strings.HasPrefix(e, "dial tcp"), strings.HasSuffix(e, "closing bad idle connection: EOF"): @@ -204,12 +207,12 @@ func (n *node) trans(rlog log.Ext1FieldLogger, fn func(*sqlx.Tx) error) error { } func (c *Cluster) next(rlog log.Ext1FieldLogger) *node { - rlog.Trace("Selecting a node") + // rlog.Trace("Selecting a node") select { case n := <-c.active: if n.active { c.active <- n - rlog.WithField("host", n.host).Trace("Selected active node") + // rlog.WithField("host", n.host).Trace("Selected active node") return n } rlog.WithField("host", n.host).Trace("Found inactive node") diff --git a/internal/db/db.go b/internal/db/db.go index 7dc34d2d..0140732f 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -131,12 +131,13 @@ func RunWithinTransaction(rlog log.Ext1FieldLogger, tx *sqlx.Tx, fn func(*sqlx.T } // ParseError parses the passed error for a sql.ErrNoRows -func ParseError(err error) (bool, error) { - if err == nil { - return true, nil +func ParseError(e error) (found bool, err error) { + if e == nil { + found = true + return } - if errors.Is(err, sql.ErrNoRows) { - err = nil + if !errors.Is(e, sql.ErrNoRows) { + err = e } - return false, err + return } diff --git a/internal/db/dbmigrate/scripts/v0.10.0.pre.sql b/internal/db/dbmigrate/scripts/v0.10.0.pre.sql new file mode 100644 index 00000000..c2ddca44 --- /dev/null +++ b/internal/db/dbmigrate/scripts/v0.10.0.pre.sql @@ -0,0 +1,748 @@ +### Tables + +CREATE TABLE IF NOT EXISTS Actions +( + id INT UNSIGNED AUTO_INCREMENT + PRIMARY KEY, action VARCHAR(128) NOT NULL, CONSTRAINT Actions_UN + UNIQUE (action) +); + +CREATE TABLE IF NOT EXISTS ActionCodes +( + id BIGINT UNSIGNED AUTO_INCREMENT + PRIMARY KEY, + action INT UNSIGNED NOT NULL, + code VARCHAR(128) NOT NULL, + expires_at DATETIME NULL, + CONSTRAINT ActionCodes_UN + UNIQUE (code), + CONSTRAINT ActionCodes_FK + FOREIGN KEY (action) REFERENCES Actions (id) +); + +CREATE INDEX IF NOT EXISTS AuthInfo_FK + ON AuthInfo (polling_code); + +ALTER TABLE Users + ADD IF NOT EXISTS email TEXT NULL; +ALTER TABLE Users + ADD IF NOT EXISTS email_verified BOOL DEFAULT 0 NOT NULL; +ALTER TABLE Users + ADD IF NOT EXISTS prefer_html_mail BOOL DEFAULT 1 NOT NULL; + +ALTER TABLE AccessTokens + MODIFY COLUMN ip_created VARCHAR(42) NOT NULL; +ALTER TABLE MTokens + MODIFY COLUMN ip_created VARCHAR(42) NOT NULL; +ALTER TABLE MT_Events + MODIFY COLUMN ip VARCHAR(42) NOT NULL; + +CREATE TABLE IF NOT EXISTS ActionReferencesUser +( + action_id BIGINT UNSIGNED NOT NULL, uid BIGINT UNSIGNED NOT NULL, CONSTRAINT ActionReferencesUser_FK + FOREIGN KEY (uid) REFERENCES Users (id) + ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT ActionReferencesUser_FK_1 + FOREIGN KEY (action_id) REFERENCES ActionCodes (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS Calendars +( + id VARCHAR(128) NOT NULL + PRIMARY KEY, name VARCHAR(128) NOT NULL, + uid BIGINT UNSIGNED NOT NULL, ics_path VARCHAR(128) NOT NULL, ics LONGTEXT NOT NULL, + CONSTRAINT Calendars_UN + UNIQUE (ics_path), + CONSTRAINT Calendars_UN_1 + UNIQUE (name, uid), + CONSTRAINT Calendars_FK + FOREIGN KEY (uid) REFERENCES Users (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +ALTER TABLE MTokens + ADD IF NOT EXISTS capabilities JSON NULL; +ALTER TABLE MTokens + ADD IF NOT EXISTS rotation JSON NULL; +ALTER TABLE MTokens + ADD IF NOT EXISTS restrictions JSON NULL; + +CREATE TABLE IF NOT EXISTS ActionReferencesMytokens +( + action_id BIGINT UNSIGNED NOT NULL, MT_id VARCHAR(128) NOT NULL, CONSTRAINT ActionReferencesMytokens_FK + FOREIGN KEY (action_id) REFERENCES ActionCodes (id) + ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT ActionReferencesMytokens_FK_1 + FOREIGN KEY (MT_id) REFERENCES MTokens (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS CalendarMapping +( + calendar_id VARCHAR(128) NOT NULL, MT_id VARCHAR(128) NOT NULL, mapping_id BIGINT UNSIGNED AUTO_INCREMENT + PRIMARY KEY, CONSTRAINT CalendarMapping_FK + FOREIGN KEY (MT_id) REFERENCES MTokens (id) + ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT CalendarMapping_FK_1 + FOREIGN KEY (calendar_id) REFERENCES Calendars (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS ActionReferencesCalendarEntries +( + action_id BIGINT UNSIGNED NOT NULL, + calendar_mapping_id BIGINT UNSIGNED NOT NULL, + CONSTRAINT ActionReferencesCalendarEntries_FK + FOREIGN KEY (action_id) REFERENCES ActionCodes (id) + ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT ActionReferencesCalendarEntries_FK_1 + FOREIGN KEY (calendar_mapping_id) REFERENCES CalendarMapping (mapping_id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS Notifications +( + id BIGINT UNSIGNED AUTO_INCREMENT + PRIMARY KEY, + type VARCHAR(32) NOT NULL, + management_code VARCHAR(128) NOT NULL, + ws VARCHAR(128) NULL, + user_wide TINYINT(1) DEFAULT 0 NOT NULL, + uid BIGINT UNSIGNED NOT NULL, + CONSTRAINT Notifications_pk2 + UNIQUE (management_code), + CONSTRAINT Notifications_FK + FOREIGN KEY (uid) REFERENCES Users (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS ActionReferencesNotificationSchedule +( + action_id BIGINT UNSIGNED NOT NULL, + notification_id BIGINT UNSIGNED NOT NULL, + MT_id VARCHAR(128) NOT NULL, + CONSTRAINT ActionReferencesNotificationSchedule_UN + UNIQUE (notification_id, MT_id), + CONSTRAINT ActionReferencesNotificationSchedule_FK + FOREIGN KEY (MT_id) REFERENCES MTokens (id) + ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT ActionReferencesNotificationSchedule_FK_1 + FOREIGN KEY (notification_id) REFERENCES Notifications (id) + ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT ActionReferencesNotificationSchedule_FK_2 + FOREIGN KEY (action_id) REFERENCES ActionCodes (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS MTNotificationsMapping +( + MT_id VARCHAR(128) NOT NULL, + notification_id BIGINT UNSIGNED NOT NULL, + include_children TINYINT(1) DEFAULT 1 NOT NULL, + CONSTRAINT MTNotificationsMapping_pk + UNIQUE (notification_id, MT_id), + CONSTRAINT MTNotificationsMapping_MTokens_id_fk + FOREIGN KEY (MT_id) REFERENCES MTokens (id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT MTNotificationsMapping_Notifications_id_fk + FOREIGN KEY (notification_id) REFERENCES Notifications (id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS SubscribedNotificationClasses +( + notificaton_id BIGINT UNSIGNED NOT NULL, class VARCHAR(128) NOT NULL, CONSTRAINT SubscribedNotificationClasses_pk + UNIQUE (notificaton_id, class), CONSTRAINT SubscribedNotificationClasses_Notifications_id_fk + FOREIGN KEY (notificaton_id) REFERENCES Notifications (id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE OR REPLACE TABLE NotificationSchedule +( + id BIGINT UNSIGNED AUTO_INCREMENT + PRIMARY KEY, + due_time DATETIME NOT NULL, + notification_id BIGINT UNSIGNED NOT NULL, + MT_id VARCHAR(128) NOT NULL, + class VARCHAR(128) NOT NULL, + additional_info LONGTEXT COLLATE utf8mb4_bin NULL + CHECK (JSON_VALID(`additional_info`)), + CONSTRAINT NotificationSchedule_FK + FOREIGN KEY (MT_id) REFERENCES MTokens (id) + ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT NotificationSchedule_FK_1 + FOREIGN KEY (notification_id) REFERENCES Notifications (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + + +### Views + +CREATE OR REPLACE VIEW CalendarRemoveCodes AS +SELECT `fa`.`id` AS `id`, + `fa`.`action` AS `action`, + `fa`.`code` AS `code`, + `fa`.`expires_at` AS `expires_at`, + `arce`.`calendar_mapping_id` AS `calendar_mapping_id`, + `cm`.`MT_id` AS `MT_id`, + `c`.`id` AS `calendar_id`, + `c`.`ics` AS `ics` + FROM ((((SELECT `ac`.`id` AS `id`, + `ac`.`action` AS `action`, + `ac`.`code` AS `code`, + `ac`.`expires_at` AS `expires_at` + FROM `ActionCodes` `ac` + WHERE `ac`.`action` = (SELECT `a`.`id` + FROM `Actions` `a` + WHERE `a`.`action` = 'remove_from_calendar')) `fa` JOIN `ActionReferencesCalendarEntries` `arce` + ON (`arce`.`action_id` = `fa`.`id`)) JOIN `CalendarMapping` `cm` + ON (`arce`.`calendar_mapping_id` = `cm`.`mapping_id`)) JOIN `Calendars` `c` + ON (`cm`.`calendar_id` = `c`.`id`)); + +CREATE OR REPLACE VIEW EventHistory AS +SELECT `me`.`time` AS `time`, + `me`.`MT_id` AS `MT_id`, + `e`.`event` AS `event`, + `me`.`comment` AS `comment`, + `me`.`ip` AS `ip`, + `me`.`user_agent` AS `user_agent` + FROM (`Events` `e` JOIN `MT_Events` `me` ON (`e`.`id` = `me`.`event_id`)) + ORDER BY `me`.`time` DESC; + +CREATE OR REPLACE VIEW MailVerificationCodes AS +SELECT `fa`.`id` AS `id`, + `fa`.`action` AS `action`, + `fa`.`code` AS `code`, + `fa`.`expires_at` AS `expires_at`, + `aru`.`uid` AS `uid` + FROM ((SELECT `ac`.`id` AS `id`, `ac`.`action` AS `action`, `ac`.`code` AS `code`, `ac`.`expires_at` AS `expires_at` + FROM `ActionCodes` `ac` + WHERE `ac`.`action` = (SELECT `a`.`id` + FROM `Actions` `a` + WHERE `a`.`action` = 'verify_email')) `fa` JOIN `ActionReferencesUser` `aru` + ON (`aru`.`action_id` = `fa`.`id`)); + +CREATE OR REPLACE VIEW MytokenRecreateCodes AS +SELECT `fa`.`id` AS `id`, + `fa`.`action` AS `action`, + `fa`.`code` AS `code`, + `fa`.`expires_at` AS `expires_at`, + `arm`.`MT_id` AS `MT_id`, + `mt`.`name` AS `name`, + `mt`.`capabilities` AS `capabilities`, + `mt`.`rotation` AS `rotation`, + `mt`.`restrictions` AS `restrictions`, + `mt`.`created` AS `token_created`, + `u`.`iss` AS `issuer` + FROM ((((SELECT `ac`.`id` AS `id`, + `ac`.`action` AS `action`, + `ac`.`code` AS `code`, + `ac`.`expires_at` AS `expires_at` + FROM `ActionCodes` `ac` + WHERE `ac`.`action` = (SELECT `a`.`id` + FROM `Actions` `a` + WHERE `a`.`action` = 'recreate_token')) `fa` JOIN `ActionReferencesMytokens` `arm` + ON (`arm`.`action_id` = `fa`.`id`)) JOIN `MTokens` `mt` + ON (`mt`.`id` = `arm`.`MT_id`)) JOIN `Users` `u` ON (`mt`.`user_id` = `u`.`id`)); + + +### Procedures + +DELIMITER ;; + +CREATE OR REPLACE PROCEDURE EncryptionKeys_GetRTKeyForMT(IN MTID VARCHAR(128)) +BEGIN + DECLARE rtid BIGINT UNSIGNED; + DECLARE keyid BIGINT UNSIGNED; + SELECT rt_id FROM MTokens WHERE id = MTID INTO rtid; + SELECT key_id FROM RT_EncryptionKeys WHERE MT_id = MTID AND rt_id = rtid INTO keyid; + SELECT * + FROM (SELECT ek.encryption_key, ek.id AS key_id FROM EncryptionKeys ek WHERE ek.id = keyid) encr + JOIN + (SELECT crypt AS refresh_token, id AS rt_id FROM CryptStore WHERE id = rtid FOR UPDATE) rt; +END;; + +CREATE OR REPLACE PROCEDURE ActionCodes_AddRecreateToken(IN MTID VARCHAR(128), IN CODE_ VARCHAR(128)) +BEGIN + DECLARE aid BIGINT UNSIGNED; + DECLARE id BIGINT UNSIGNED; + SET TIME_ZONE = "+0:00"; + SELECT a.id FROM Actions a WHERE a.`action` = 'recreate_token' INTO aid; + INSERT INTO ActionCodes (action, code) VALUES (aid, CODE_); + SELECT LAST_INSERT_ID() INTO id; + INSERT INTO ActionReferencesMytokens (action_id, MT_id) VALUES (id, MTID); +END;; + +CREATE OR REPLACE PROCEDURE ActionCodes_AddRemoveFromCalendar(IN MTID VARCHAR(128), IN CALENDAR VARCHAR(128), + IN CODE_ VARCHAR(128)) +BEGIN + DECLARE aid BIGINT UNSIGNED; + DECLARE id BIGINT UNSIGNED; + SET TIME_ZONE = "+0:00"; + SELECT a.id FROM Actions a WHERE a.`action` = 'remove_from_calendar' INTO aid; + INSERT INTO ActionCodes (action, code) VALUES (aid, CODE_); + SELECT LAST_INSERT_ID() INTO id; + INSERT INTO ActionReferencesCalendarEntries (action_id, calendar_mapping_id) + VALUES (id, (SELECT cm.mapping_id + FROM CalendarMapping cm + WHERE cm.MT_id = MTID + AND cm.calendar_id = (SELECT c.id + FROM Calendars c + WHERE c.name = CALENDAR + AND c.uid = (SELECT m.user_id + FROM MTokens m + WHERE m.id = MTID)))); +END;; + +CREATE OR REPLACE PROCEDURE ActionCodes_AddScheduleNotificationCode(IN CODE_ varchar(128), IN NID bigint unsigned, IN + MTID varchar(128)) +BEGIN + DECLARE id bigint unsigned; + SET TIME_ZONE = "+0:00"; + INSERT INTO ActionCodes (action, code) VALUES((SELECT a.id FROM Actions a WHERE a.`action` ='unsubscribe_scheduled'), CODE_); + SELECT LAST_INSERT_ID() INTO id; + INSERT INTO ActionReferencesNotificationSchedule (action_id,notification_id,MT_id) VALUES(id, NID, MTID); +END;; + +CREATE OR REPLACE PROCEDURE ActionCodes_AddVerifyMail(IN MTID VARCHAR(128), IN CODE_ VARCHAR(128), IN EXPIRES_IN INT) +BEGIN + DECLARE aid BIGINT UNSIGNED; + DECLARE userid BIGINT UNSIGNED; + DECLARE id BIGINT UNSIGNED; + SET TIME_ZONE = "+0:00"; + SELECT a.id FROM Actions a WHERE a.`action` = 'verify_email' INTO aid; + SELECT m.user_id FROM MTokens m WHERE m.id = MTID INTO userid; + DELETE + FROM ActionCodes + WHERE `action` = aid + AND id IN (SELECT mvc.id FROM MailVerificationCodes mvc WHERE mvc.uid = userid); + INSERT INTO ActionCodes (action, code, expires_at) + VALUES (aid, CODE_, (UTC_TIMESTAMP() + INTERVAL EXPIRES_IN SECOND)); + SELECT LAST_INSERT_ID() INTO id; + INSERT INTO ActionReferencesUser (action_id, uid) VALUES (id, userid); +END;; + +CREATE OR REPLACE PROCEDURE ActionCodes_Delete(IN CODE_ VARCHAR(128)) +BEGIN + DELETE FROM ActionCodes WHERE code = CODE_; +END; + +CREATE OR REPLACE PROCEDURE ActionCodes_GetRecreateData(IN CODE_ VARCHAR(128)) +BEGIN + SELECT name, capabilities, restrictions, rotation, token_created AS created, issuer + FROM MytokenRecreateCodes + WHERE code = CODE_; +END;; + +CREATE OR REPLACE PROCEDURE ActionCodes_RemoveFromCalendar(IN CODE_ VARCHAR(128)) +BEGIN + SET TIME_ZONE = "+0:00"; + DELETE FROM CalendarMapping WHERE mapping_id = (SELECT calendar_mapping_id FROM CalendarRemoveCodes WHERE code = + CODE_); +END;; + +CREATE OR REPLACE PROCEDURE ActionCodes_UnsubscribeFurtherScheduled(IN CODE_ VARCHAR(128)) +BEGIN + DECLARE MTID VARCHAR(128); + DECLARE NID BIGINT UNSIGNED; + DECLARE AID BIGINT UNSIGNED; + SELECT ac.id INTO AID FROM ActionCodes ac WHERE ac.code=CODE_ AND ac.action=(SELECT id FROM + Actions WHERE action= + 'unsubscribe_scheduled'); + SELECT ans.MT_id, ans.notification_id INTO MTID, NID FROM ActionReferencesNotificationSchedule ans WHERE ans.action_id = AID; + DELETE FROM NotificationSchedule WHERE MT_id = MTID AND notification_id = NID; + DELETE FROM ActionCodes WHERE id=AID; +END;; + +CREATE OR REPLACE PROCEDURE ActionCodes_UseRemoveFromCalendar(IN CODE_ VARCHAR(128)) +BEGIN + CALL ActionCodes_RemoveFromCalendar(CODE_); + CALL ActionCodes_Delete(CODE_); +END;; + +CREATE OR REPLACE PROCEDURE ActionCodes_VerifyMail(IN CODE_ VARCHAR(128)) +BEGIN + SET TIME_ZONE = "+0:00"; + UPDATE Users u + SET u.email_verified=1 + WHERE u.id = + (SELECT v.uid FROM MailVerificationCodes v WHERE v.code = CODE_ AND v.expires_at > CURRENT_TIMESTAMP()); +END;; + +CREATE OR REPLACE PROCEDURE Calendar_AddMytoken(IN MTID VARCHAR(128), IN CALENDARID VARCHAR(128)) +BEGIN + INSERT IGNORE INTO CalendarMapping (calendar_id, MT_id) VALUES (CALENDARID, MTID); +END;; + +CREATE OR REPLACE PROCEDURE Calendar_Delete(IN MTID VARCHAR(128), IN NAME_ VARCHAR(128)) +BEGIN + DELETE FROM Calendars WHERE uid = (SELECT m.user_id FROM MTokens m WHERE m.id = MTID) AND name = NAME_; +END;; + +CREATE OR REPLACE PROCEDURE Calendar_Get(IN MTID VARCHAR(128), IN NAME_ VARCHAR(128)) +BEGIN + SELECT id, name, ics_path, ics + FROM Calendars + WHERE name = NAME_ + AND uid = (SELECT m.user_id FROM MTokens m WHERE m.id = MTID); +END;; + +CREATE OR REPLACE PROCEDURE Calendar_GetByID(IN CID VARCHAR(128)) +BEGIN + SELECT id, name, ics_path, ics FROM Calendars WHERE id = CID; +END;; + +CREATE OR REPLACE PROCEDURE Calendar_GetMTsInCalendar(IN CALID VARCHAR(128)) +BEGIN + SELECT MT_id FROM CalendarMapping WHERE calendar_id = CALID; +END;; + +CREATE OR REPLACE PROCEDURE Calendar_Insert(IN MTID VARCHAR(128), IN CID VARCHAR(128), IN NAME_ VARCHAR(128), + IN PATH_ TEXT, + IN ICS_ LONGTEXT) +BEGIN + SET TIME_ZONE = "+0:00"; + INSERT INTO Calendars (id, name, uid, ics_path, ics) + VALUES (CID, NAME_, (SELECT m.user_id FROM MTokens m WHERE m.id = MTID), PATH_, ICS_); +END;; + +CREATE OR REPLACE PROCEDURE Calendar_List(IN MTID VARCHAR(128)) +BEGIN + SELECT id, name, ics_path, ics FROM Calendars WHERE uid = (SELECT m.user_id FROM MTokens m WHERE m.id = MTID); +END;; + +CREATE OR REPLACE PROCEDURE Calendar_ListForMT(IN MTID VARCHAR(128)) +BEGIN + SELECT id, name, ics_path, ics + FROM Calendars + WHERE id IN (SELECT calendar_id FROM CalendarMapping WHERE MT_id = MTID); +END;; + +CREATE OR REPLACE PROCEDURE Calendar_Update(IN MTID VARCHAR(128), IN CID VARCHAR(128), IN NAME_ VARCHAR(128), + IN ICS_ LONGTEXT) +BEGIN + SET TIME_ZONE = "+0:00"; + UPDATE Calendars + SET name=NAME_, ics=ICS_ + WHERE uid = (SELECT m.user_id FROM MTokens m WHERE m.id = MTID) AND id = CID; +END;; + +CREATE OR REPLACE PROCEDURE Calendar_UpdateInternal(IN CID VARCHAR(128), IN NAME_ VARCHAR(128), IN ICS_ LONGTEXT) +BEGIN + SET TIME_ZONE = "+0:00"; + UPDATE Calendars SET name=NAME_, ics=ICS_ WHERE id = CID; +END;; + +CREATE OR REPLACE PROCEDURE Cleanup() +BEGIN + CALL Cleanup_MTokens(); + CALL Cleanup_AuthInfo(); + CALL Cleanup_ProxyTokens(); + CALL Cleanup_ActionCodes(); +END;; + +CREATE OR REPLACE PROCEDURE Cleanup_ActionCodes() +BEGIN + SET TIME_ZONE = "+0:00"; + DELETE FROM ActionCodes WHERE expires_at < CURRENT_TIMESTAMP(); +END;; + +CREATE OR REPLACE PROCEDURE MTokens_Insert(IN SUB TEXT, IN ISS TEXT, IN MTID VARCHAR(128), IN SEQNO_ BIGINT UNSIGNED, + IN PARENT VARCHAR(128), IN RTID BIGINT UNSIGNED, IN NAME_ TEXT, IN IP TEXT, + IN EXPIRES_AT_ DATETIME, IN CAPABILITIES_ TEXT, IN ROTATION_ TEXT, + IN RESTR TEXT) +BEGIN + SET TIME_ZONE = "+0:00"; + CALL Users_GetID(SUB, ISS, @UID); + INSERT INTO MTokens (id, seqno, parent_id, rt_id, name, ip_created, user_id, expires_at, capabilities, rotation, + restrictions) + VALUES (MTID, SEQNO_, PARENT, RTID, NAME_, IP, @UID, EXPIRES_AT_, CAPABILITIES_, ROTATION_, RESTR); +END;; + + +CREATE OR REPLACE PROCEDURE MTokens_SetMetadata(IN MTID VARCHAR(128), IN CAPABILITIES_ TEXT, IN ROTATION_ TEXT, + IN RESTR TEXT) +BEGIN + UPDATE MTokens m SET m.capabilities=CAPABILITIES_ WHERE m.id = MTID AND m.capabilities IS NULL; + UPDATE MTokens m SET m.rotation=ROTATION_ WHERE m.id = MTID AND m.rotation IS NULL; + UPDATE MTokens m SET m.restrictions=RESTR WHERE m.id = MTID AND m.restrictions IS NULL; +END;; + +CREATE OR REPLACE PROCEDURE Mtokens_GetInfo(IN MTID VARCHAR(128)) +BEGIN + SELECT id, parent_id, id AS mom_id, name, created, expires_at, ip_created AS ip + FROM MTokens + WHERE id = MTID; +END;; + +CREATE OR REPLACE PROCEDURE NotificationScheduleAdd(IN DUETIME TIMESTAMP, IN NID BIGINT UNSIGNED, IN MTID VARCHAR(128), + IN CLASS_ VARCHAR(128), IN ADDITIONALINFO LONGTEXT) +BEGIN + INSERT IGNORE INTO NotificationSchedule (due_time, notification_id, MT_id, class, additional_info) VALUES(DUETIME, NID, MTID, CLASS_,ADDITIONALINFO); +END;; + +CREATE OR REPLACE PROCEDURE NotificationSchedule_DeleteExpirations(IN NID BIGINT UNSIGNED) +BEGIN + DELETE FROM NotificationSchedule WHERE notification_id = NID AND class = 'exp'; + DELETE FROM ActionCodes WHERE id IN (SELECT action_id FROM ActionReferencesNotificationSchedule WHERE + notification_id=NID); +END;; + +CREATE OR REPLACE PROCEDURE NotificationSchedule_DeleteExpirationsForMT(IN NID BIGINT UNSIGNED, IN MTID VARCHAR(128)) +BEGIN + DELETE FROM NotificationSchedule WHERE notification_id = NID AND MT_id = MTID AND class = 'exp'; + DELETE FROM ActionCodes WHERE action IN (SELECT action_id FROM ActionReferencesNotificationSchedule WHERE + notification_id=NID AND MT_id=MTID); +END;; + +CREATE OR REPLACE PROCEDURE Notifications_ClearNotificationClasses(IN NID BIGINT UNSIGNED) +BEGIN + DELETE FROM SubscribedNotificationClasses WHERE notificaton_id = NID; +END;; + +CREATE OR REPLACE PROCEDURE Notifications_Create(IN MTID VARCHAR(128), IN TYPE_ VARCHAR(32), IN MNGCODE VARCHAR(128), + IN WS_ VARCHAR(128), IN USERWIDE TINYINT(1), OUT ID BIGINT UNSIGNED) +BEGIN + INSERT INTO Notifications (type, management_code, ws, user_wide, uid) + VALUES (TYPE_, MNGCODE, WS_, USERWIDE, (SELECT m.user_id FROM MTokens m WHERE m.id = MTID)); + SET ID = LAST_INSERT_ID(); + +END;; + +CREATE OR REPLACE PROCEDURE Notifications_CreateForMT(IN MTID VARCHAR(128), IN INCLUDECHILDS TINYINT(1), + IN TYPE_ VARCHAR(32), + IN MNGCODE VARCHAR(128), IN WS_ VARCHAR(128)) +BEGIN + CALL Notifications_Create(MTID, TYPE_, MNGCODE, WS_, 0, @ID); + IF INCLUDECHILDS = 0 THEN + CALL Notifications_LinkMT(MTID, @ID, INCLUDECHILDS); + ELSE + CALL Notifications_LinkMTWithChildren(MTID, @ID); + END IF; + SELECT @ID AS notification_id; +END;; + +CREATE OR REPLACE PROCEDURE Notifications_CreateUserWide(IN MTID VARCHAR(128), IN TYPE_ VARCHAR(32), + IN MNGCODE VARCHAR(128), + IN WS_ VARCHAR(128)) +BEGIN + CALL Notifications_Create(MTID, TYPE_, MNGCODE, WS_, 1, @ID); + SELECT @ID AS notification_id; +END;; + +CREATE OR REPLACE PROCEDURE Notifications_DeleteByManagementCode(IN CODE VARCHAR(128)) +BEGIN + DECLARE nid bigint unsigned; + SELECT id FROM Notifications WHERE management_code=CODE INTO nid; + DELETE FROM ActionCodes WHERE id IN (SELECT action_id FROM ActionReferencesNotificationSchedule WHERE notification_id=nid); + DELETE FROM Notifications WHERE id=nid; +END;; + + +CREATE OR REPLACE PROCEDURE Notifications_ExpandToChildren(IN PARENT VARCHAR(128), IN CHILD VARCHAR(128)) +BEGIN + DECLARE nid BIGINT UNSIGNED; + + SELECT notification_id FROM MTNotificationsMapping WHERE MT_id = PARENT AND include_children = 1 INTO nid; + IF (nid IS NOT NULL) THEN + INSERT IGNORE INTO MTNotificationsMapping (MT_id, notification_id, include_children) VALUES (CHILD, nid, 1); + END IF; +END;; + +CREATE OR REPLACE PROCEDURE Notifications_GetForMT(IN MTID VARCHAR(128)) +BEGIN + SELECT n.id, n.type, n.management_code, n.ws, n.user_wide, snc.class, n.uid + FROM ((SELECT * + FROM Notifications + WHERE id IN ( + (SELECT notification_id FROM MTNotificationsMapping WHERE MT_id = MTID)) + OR (user_wide = 1 AND uid = (SELECT user_id FROM MTokens m WHERE m.id = MTID))) n JOIN SubscribedNotificationClasses snc + ON n.id = snc.notificaton_id + ) + ORDER BY n.id DESC; +END;; + +CREATE OR REPLACE PROCEDURE Notifications_GetForMTAndClass(IN MTID VARCHAR(128), IN _CLASS VARCHAR(128)) +BEGIN + SELECT n.id, n.type, n.management_code, n.ws, n.user_wide, n.uid + FROM Notifications n + WHERE id IN (((SELECT notification_id FROM MTNotificationsMapping WHERE MT_id = MTID) + UNION + (SELECT id + FROM Notifications + WHERE user_wide = 1 AND uid = (SELECT user_id FROM MTokens WHERE id = MTID))) + INTERSECT + (SELECT notificaton_id FROM SubscribedNotificationClasses WHERE class = _CLASS)); + +END;; + +CREATE OR REPLACE PROCEDURE Notifications_GetForManagementCode(IN CODE VARCHAR(128)) +BEGIN + SELECT n.id, n.type, n.management_code, n.ws, n.user_wide, snc.class, n.uid + FROM ((SELECT * + FROM Notifications + WHERE management_code = CODE) n JOIN SubscribedNotificationClasses snc ON n.id = snc.notificaton_id + ); + +END;; + +CREATE OR REPLACE PROCEDURE Notifications_GetForUser(IN MTID VARCHAR(128)) +BEGIN + SELECT n.id, n.type, n.management_code, n.ws, n.user_wide, snc.class, n.uid + FROM ((SELECT * + FROM Notifications + WHERE uid = (SELECT user_id FROM MTokens WHERE id = MTID)) n JOIN SubscribedNotificationClasses snc + ON n.id = snc.notificaton_id + ) + ORDER BY n.id DESC; + +END;; + +CREATE OR REPLACE PROCEDURE Notifications_GetMTsForNotification(IN NID BIGINT UNSIGNED) +BEGIN + SELECT MT_id FROM MTNotificationsMapping WHERE notification_id = nid; + +END;; + +CREATE OR REPLACE PROCEDURE Notifications_LinkClass(IN NID BIGINT UNSIGNED, IN NCLASS VARCHAR(128)) +BEGIN + INSERT IGNORE INTO SubscribedNotificationClasses (notificaton_id, class) VALUES (NID, NCLASS); +END;; + +CREATE OR REPLACE PROCEDURE Notifications_LinkMT(IN MTID VARCHAR(128), IN NID BIGINT UNSIGNED, + IN INCLUDECHILDS TINYINT(1)) +BEGIN + INSERT IGNORE INTO MTNotificationsMapping (MT_id, notification_id, include_children) + VALUES (MTID, NID, INCLUDECHILDS); +END;; + +CREATE OR REPLACE PROCEDURE Notifications_LinkMTWithChildren(IN MTID VARCHAR(128), IN NID BIGINT UNSIGNED) +BEGIN + INSERT IGNORE INTO MTNotificationsMapping(MT_id, notification_id, include_children) + SELECT c.id, NID, 1 + FROM (WITH RECURSIVE childs AS (SELECT id, parent_id + FROM MTokens + WHERE id = MTID + UNION ALL + SELECT mt.id, mt.parent_id + FROM MTokens mt + INNER JOIN childs c + WHERE mt.parent_id = c.id) + SELECT id + FROM childs) c; +END;; + +CREATE OR REPLACE PROCEDURE Notifications_UnlinkMT(IN MTID VARCHAR(128), IN NID BIGINT UNSIGNED) +BEGIN + IF (SELECT include_children FROM MTNotificationsMapping WHERE notification_id = NID AND MT_id = MTID) = 0 THEN + DELETE FROM MTNotificationsMapping WHERE notification_id = NID AND MT_id = MTID; + ELSE + DELETE + FROM MTNotificationsMapping + WHERE notification_id = NID + AND MT_id IN (WITH RECURSIVE childs AS (SELECT id, parent_id + FROM MTokens + WHERE id = MTID + UNION ALL + SELECT mt.id, mt.parent_id + FROM MTokens mt + INNER JOIN childs c + WHERE mt.parent_id = c.id) + SELECT id + FROM childs); + END IF; +END;; + + +CREATE OR REPLACE PROCEDURE ScheduledNotification_GetActionCode(IN NID BIGINT UNSIGNED, IN MTID VARCHAR(128)) +BEGIN + SELECT ac.code FROM ActionCodes ac WHERE ac.id =(SELECT arns.action_id FROM ActionReferencesNotificationSchedule arns WHERE arns.MT_id = MTID AND arns.notification_id = NID); +END;; + +CREATE OR REPLACE PROCEDURE Users_ChangeMail(IN MTID VARCHAR(128), IN MAIL TEXT) +BEGIN + UPDATE Users SET email=MAIL, email_verified=0 WHERE id = (SELECT m.user_id FROM MTokens m WHERE m.id = MTID); +END;; + +CREATE OR REPLACE PROCEDURE Users_ChangePreferredMailType(IN MTID VARCHAR(128), IN PREFER_HTML TINYINT(1)) +BEGIN + UPDATE Users SET prefer_html_mail=PREFER_HTML WHERE id = (SELECT m.user_id FROM MTokens m WHERE m.id = MTID); +END;; + +CREATE OR REPLACE PROCEDURE Users_GetMail(IN MTID VARCHAR(128)) +BEGIN + SELECT u.email, u.email_verified, u.prefer_html_mail + FROM Users u + WHERE u.id = (SELECT m.user_id FROM MTokens m WHERE m.id = MTID); +END;; + +CREATE OR REPLACE PROCEDURE Users_SetMail(IN MTID VARCHAR(128), IN MAIL TEXT, IN VERIFIED BIT) +BEGIN + UPDATE Users u + SET u.email=MAIL, + u.email_verified=VERIFIED + WHERE u.id = (SELECT m.user_id FROM MTokens m WHERE m.id = MTID); +END;; + +CREATE OR REPLACE PROCEDURE getOIDCIssForManagementCode(IN CODE VARCHAR(128)) +BEGIN + SELECT u.iss FROM Users u WHERE u.id = (SELECT n.uid FROM Notifications n WHERE n.management_code = CODE); +END;; + +CREATE OR REPLACE PROCEDURE Events_GetIPs(IN MTID VARCHAR(128)) +BEGIN + SELECT UNIQUE ip FROM MT_Events me WHERE me.MT_id = MTID; +END;; + +CREATE OR REPLACE PROCEDURE PopOneDueScheduledNotification() +BEGIN + DECLARE sid BIGINT UNSIGNED; + SET TIME_ZONE = "+0:00"; + SELECT ns.id INTO sid FROM NotificationSchedule ns WHERE ns.due_time <= CURRENT_TIMESTAMP() LIMIT 1; + SELECT nss.*, n.`type` ,n.management_code, n.ws , n.user_wide ,n.uid FROM (( SELECT * FROM NotificationSchedule ns WHERE ns.id=sid ) nss JOIN Notifications n ON nss.notification_id = n.id); + DELETE FROM NotificationSchedule WHERE id=sid; +END;; + +DELIMITER ; + +# Values +INSERT IGNORE INTO Events (event) + VALUES ('expired'); +INSERT IGNORE INTO Events (event) + VALUES ('revoked'); +INSERT IGNORE INTO Events (event) + VALUES ('notification_subscribed'); +INSERT IGNORE INTO Events (event) + VALUES ('notification_listed'); +INSERT IGNORE INTO Events (event) + VALUES ('notification_unsubscribed'); +INSERT IGNORE INTO Events (event) + VALUES ('notification_subscribed_other'); +INSERT IGNORE INTO Events (event) + VALUES ('notification_unsubscribed_other'); +INSERT IGNORE INTO Events (event) + VALUES ('calendar_created'); +INSERT IGNORE INTO Events (event) + VALUES ('calendar_listed'); +INSERT IGNORE INTO Events (event) + VALUES ('calendar_deleted'); +INSERT IGNORE INTO Events (event) + VALUES ('email_settings_listed'); +INSERT IGNORE INTO Events (event) + VALUES ('email_changed'); +INSERT IGNORE INTO Events (event) + VALUES ('email_mimetype_changed'); +INSERT IGNORE INTO Events (event) + VALUES ('tokeninfo_notifications'); +INSERT IGNORE INTO Events (event) + VALUES ('tokeninfo_notifications_other_token'); +INSERT IGNORE INTO Events (event) + VALUES ('notification_created'); +INSERT IGNORE INTO Events (event) + VALUES ('notification_created_other'); + +INSERT IGNORE INTO Actions (action) + VALUES ('verify_email'); +INSERT IGNORE INTO Actions (action) + VALUES ('unsubscribe_notification'); +INSERT IGNORE INTO Actions (action) + VALUES ('recreate_token'); +INSERT IGNORE INTO Actions (action) + VALUES ('remove_from_calendar'); +INSERT IGNORE INTO Actions (action) + VALUES ('unsubscribe_scheduled'); diff --git a/internal/db/dbmigrate/scripts/v0.4.0.pre.sql b/internal/db/dbmigrate/scripts/v0.4.0.pre.sql index 320ff069..32012474 100644 --- a/internal/db/dbmigrate/scripts/v0.4.0.pre.sql +++ b/internal/db/dbmigrate/scripts/v0.4.0.pre.sql @@ -57,7 +57,7 @@ INSERT IGNORE INTO CryptPayloadTypes (payload_type) RENAME TABLE IF EXISTS RefreshTokens TO CryptStore; ALTER TABLE CryptStore - CHANGE rt crypt TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL; + CHANGE rt crypt TEXT NOT NULL; ALTER TABLE CryptStore ADD COLUMN IF NOT EXISTS payload_type INT UNSIGNED NULL; ALTER TABLE CryptStore diff --git a/internal/db/dbrepo/actionrepo/actions.go b/internal/db/dbrepo/actionrepo/actions.go new file mode 100644 index 00000000..d8a22dd1 --- /dev/null +++ b/internal/db/dbrepo/actionrepo/actions.go @@ -0,0 +1,163 @@ +package actionrepo + +import ( + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + "github.com/oidc-mytoken/utils/unixtime" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" + "github.com/oidc-mytoken/server/internal/mytoken/restrictions" +) + +// VerifyMail verifies a mail address +func VerifyMail(rlog log.Ext1FieldLogger, tx *sqlx.Tx, code string) (verified bool, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + result, err := tx.Exec(`CALL ActionCodes_VerifyMail(?)`, code) + if err != nil { + return errors.WithStack(err) + } + rows, err := result.RowsAffected() + if err != nil { + return errors.WithStack(err) + } + verified = rows == 1 + return deleteCode(rlog, tx, code) + }, + ) + return +} + +// deleteCode deletes a code +func deleteCode(rlog log.Ext1FieldLogger, tx *sqlx.Tx, code string) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL ActionCodes_Delete(?)`, code) + return errors.WithStack(err) + }, + ) +} + +// AddVerifyEmailCode adds a code for email verification to the database +func AddVerifyEmailCode( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, code string, + expiresIn int, +) (err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err = tx.Exec(`CALL ActionCodes_AddVerifyMail(?,?,?)`, mtID, code, expiresIn) + if err != nil { + return errors.WithStack(err) + } + return err + }, + ) + return +} + +// AddRecreateTokenCode adds a code for token recreation to the database +func AddRecreateTokenCode( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, code string, +) (err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err = tx.Exec(`CALL ActionCodes_AddRecreateToken(?,?)`, mtID, code) + if err != nil { + return errors.WithStack(err) + } + return err + }, + ) + return +} + +// AddRemoveFromCalendarCode adds a code for removing a token from a calendar to the database +func AddRemoveFromCalendarCode( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, code, calendarName string, +) (err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err = tx.Exec(`CALL ActionCodes_AddRemoveFromCalendar(?,?,?)`, mtID, calendarName, code) + if err != nil { + return errors.WithStack(err) + } + return err + }, + ) + return +} + +// AddScheduleNotificationCode adds a code for un-scheduling scheduled notifications +func AddScheduleNotificationCode( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, notificationID uint64, code string, +) (err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err = tx.Exec(`CALL ActionCodes_AddScheduleNotificationCode(?,?,?)`, code, notificationID, mtID) + if err != nil { + return errors.WithStack(err) + } + return err + }, + ) + return +} + +// UseRemoveCalendarCode uses a calendar remove ActionCode to remove a token from a calendar and then deletes the +// code from the database +func UseRemoveCalendarCode(rlog log.Ext1FieldLogger, tx *sqlx.Tx, code string) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL ActionCodes_UseRemoveFromCalendar(?)`, code) + return errors.WithStack(err) + }, + ) +} + +// UseUnsubscribeFurtherNotificationsCode uses the ActionCode to unsubscribe from further scheduled notifications and +// deletes the code +func UseUnsubscribeFurtherNotificationsCode(rlog log.Ext1FieldLogger, tx *sqlx.Tx, code string) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL ActionCodes_UnsubscribeFurtherScheduled(?)`, code) + return errors.WithStack(err) + }, + ) +} + +// RecreateData holds data stored in the database to enable re-creation of mytokens +type RecreateData struct { + Name db.NullString `db:"name"` + Issuer string `db:"issuer"` + Restrictions restrictions.Restrictions `db:"restrictions"` + Capabilities api.Capabilities `db:"capabilities"` + Rotation *api.Rotation `db:"rotation"` + Created unixtime.UnixTime `db:"created"` +} + +// GetRecreateData returns the stored token recreation data linked to the passed code +func GetRecreateData(rlog log.Ext1FieldLogger, tx *sqlx.Tx, code string) (data RecreateData, found bool, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + return errors.WithStack(tx.Get(&data, `CALL ActionCodes_GetRecreateData(?)`, code)) + }, + ) + found, err = db.ParseError(err) + return +} + +// GetScheduledNotificationActionCode returns the action code for a scheduled notification +func GetScheduledNotificationActionCode(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, nid uint64) ( + code string, + err error, +) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + return errors.WithStack(tx.Get(&code, `CALL ScheduledNotification_GetActionCode(?,?)`, nid, mtID)) + }, + ) + return +} diff --git a/internal/db/dbrepo/authcodeinforepo/authcodeInfo.go b/internal/db/dbrepo/authcodeinforepo/authcodeInfo.go index 58cdec30..6a3aab52 100644 --- a/internal/db/dbrepo/authcodeinforepo/authcodeInfo.go +++ b/internal/db/dbrepo/authcodeinforepo/authcodeInfo.go @@ -75,10 +75,10 @@ func (i *AuthFlowInfo) Store(rlog log.Ext1FieldLogger, tx *sqlx.Tx) error { } // GetAuthFlowInfoByState returns AuthFlowInfoIn by state -func GetAuthFlowInfoByState(rlog log.Ext1FieldLogger, state *state.State) (*AuthFlowInfoOut, error) { +func GetAuthFlowInfoByState(rlog log.Ext1FieldLogger, tx *sqlx.Tx, state *state.State) (*AuthFlowInfoOut, error) { info := authFlowInfo{} - if err := db.Transact( - rlog, func(tx *sqlx.Tx) error { + if err := db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { row := tx.QueryRowx(`CALL AuthInfo_Get(?)`, state) if err := row.Err(); err != nil { return errors.WithStack(err) @@ -112,7 +112,7 @@ func UpdateTokenInfoByState( ) error { return db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { - info, err := GetAuthFlowInfoByState(rlog, state) + info, err := GetAuthFlowInfoByState(rlog, tx, state) if err != nil { return err } diff --git a/internal/db/dbrepo/eventrepo/event.go b/internal/db/dbrepo/eventrepo/event.go index c91aec67..057cfd3a 100644 --- a/internal/db/dbrepo/eventrepo/event.go +++ b/internal/db/dbrepo/eventrepo/event.go @@ -8,14 +8,14 @@ import ( "github.com/oidc-mytoken/api/v0" "github.com/oidc-mytoken/server/internal/db" - event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" ) // EventDBObject holds information needed for storing an event in the database type EventDBObject struct { - *event.Event - MTID mtid.MTID + api.Event + Comment string + MTID mtid.MTID api.ClientMetaData } @@ -25,7 +25,7 @@ func (e *EventDBObject) Store(rlog log.Ext1FieldLogger, tx *sqlx.Tx) error { rlog, tx, func(tx *sqlx.Tx) error { _, err := tx.Exec( `CALL Event_Insert(?, ?, ?, ?, ?)`, - e.MTID, e.Event.String(), e.Event.Comment, e.ClientMetaData.IP, e.ClientMetaData.UserAgent, + e.MTID, e.Event, e.Comment, e.ClientMetaData.IP, e.ClientMetaData.UserAgent, ) return errors.WithStack(err) }, diff --git a/internal/db/dbrepo/eventrepo/eventHistory.go b/internal/db/dbrepo/eventrepo/eventHistory.go index 62f9f6a5..31db0800 100644 --- a/internal/db/dbrepo/eventrepo/eventHistory.go +++ b/internal/db/dbrepo/eventrepo/eventHistory.go @@ -72,3 +72,14 @@ func GetEventHistoryChildren( } return } + +// GetPreviouslyUsedIPs returns a list of the ips that were previously used with a mytoken +func GetPreviouslyUsedIPs(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID) (ips []string, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err = db.ParseError(errors.WithStack(tx.Select(&ips, `CALL Events_GetIPs(?)`, mtID))) + return err + }, + ) + return +} diff --git a/internal/db/dbrepo/mytokenrepo/mytoken.go b/internal/db/dbrepo/mytokenrepo/mytoken.go index 0a289145..8fd54944 100644 --- a/internal/db/dbrepo/mytokenrepo/mytoken.go +++ b/internal/db/dbrepo/mytokenrepo/mytoken.go @@ -11,8 +11,9 @@ import ( log "github.com/sirupsen/logrus" "github.com/oidc-mytoken/server/internal/db" + helper "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/mytokenrepohelper" eventService "github.com/oidc-mytoken/server/internal/mytoken/event" - event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" "github.com/oidc-mytoken/server/internal/utils/cryptutils" @@ -37,9 +38,12 @@ type MytokenEntry struct { } // InitRefreshToken links a refresh token to this MytokenEntry -func (mte *MytokenEntry) InitRefreshToken(rt string) error { +func (mte *MytokenEntry) InitRefreshToken(rt string) (err error) { mte.refreshToken = rt - mte.encryptionKey = cryptutils.RandomBytes(32) + mte.encryptionKey, err = cryptutils.RandomBytes(32) + if err != nil { + return + } tmp, err := cryptutils.AESEncrypt(mte.refreshToken, mte.encryptionKey) if err != nil { return err @@ -103,30 +107,38 @@ func (mte *MytokenEntry) Store(rlog log.Ext1FieldLogger, tx *sqlx.Tx, comment st Sub: mte.Token.OIDCSubject, ExpiresAt: db.NewNullTime(mte.expiresAt.Time()), } + meta, err := mte.Token.DBMetadata() + if err != nil { + return err + } + steStore.MytokenDBMetadata = meta + return db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { if mte.rtID == nil { - if _, err := tx.Exec(`CALL CryptStoreRT_Insert(?,@ID)`, mte.rtEncrypted); err != nil { + if _, err = tx.Exec(`CALL CryptStoreRT_Insert(?,@ID)`, mte.rtEncrypted); err != nil { return errors.WithStack(err) } var rtID uint64 - if err := tx.Get(&rtID, `SELECT @ID`); err != nil { + if err = tx.Get(&rtID, `SELECT @ID`); err != nil { return errors.WithStack(err) } mte.rtID = &rtID } steStore.RefreshTokenID = *mte.rtID - if err := steStore.Store(rlog, tx); err != nil { + if err = steStore.Store(rlog, tx); err != nil { return err } - if err := storeEncryptionKey(tx, mte.encryptionKeyEncrypted, steStore.RefreshTokenID, mte.ID); err != nil { + if err = storeEncryptionKey(tx, mte.encryptionKeyEncrypted, steStore.RefreshTokenID, mte.ID); err != nil { return err } return eventService.LogEvent( - rlog, tx, eventService.MTEvent{ - Event: event.FromNumber(event.MTCreated, comment), - MTID: mte.ID, - }, mte.networkData, + rlog, tx, pkg.MTEvent{ + Event: api.EventMTCreated, + Comment: comment, + MTID: mte.ID, + ClientMetaData: mte.networkData, + }, ) }, ) @@ -148,6 +160,7 @@ type mytokenEntryStore struct { Iss string Sub string ExpiresAt sql.NullTime + helper.MytokenDBMetadata } // Store stores the mytokenEntryStore in the database; if this is the first token for this user, the user is also added @@ -156,8 +169,9 @@ func (e *mytokenEntryStore) Store(rlog log.Ext1FieldLogger, tx *sqlx.Tx) error { return db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { _, err := tx.Exec( - `CALL MTokens_Insert(?,?,?,?,?,?,?,?,?)`, - e.Sub, e.Iss, e.ID, e.SeqNo, e.ParentID, e.RefreshTokenID, e.Name, e.IP, e.ExpiresAt, + `CALL MTokens_Insert(?,?,?,?,?,?,?,?,?,?,?,?)`, + e.Sub, e.Iss, e.ID, e.SeqNo, e.ParentID, e.RefreshTokenID, e.Name, e.IP, e.ExpiresAt, e.Capabilities, + e.Rotation, e.Restrictions, ) return errors.WithStack(err) }, diff --git a/internal/db/dbrepo/mytokenrepo/mytokenrepohelper/helpers.go b/internal/db/dbrepo/mytokenrepo/mytokenrepohelper/helpers.go index 0db6a6b3..6dd616cd 100644 --- a/internal/db/dbrepo/mytokenrepo/mytokenrepohelper/helpers.go +++ b/internal/db/dbrepo/mytokenrepo/mytokenrepohelper/helpers.go @@ -1,8 +1,6 @@ package mytokenrepohelper import ( - "database/sql" - "github.com/jmoiron/sqlx" "github.com/oidc-mytoken/api/v0" "github.com/pkg/errors" @@ -112,6 +110,29 @@ func UpdateSeqNo(rlog log.Ext1FieldLogger, tx *sqlx.Tx, id mtid.MTID, seqno uint ) } +// MytokenDBMetadata is a type for metadata about a mytoken +type MytokenDBMetadata struct { + Capabilities db.NullString + Rotation db.NullString + Restrictions db.NullString +} + +// SetMetadata adds a mytoken's metadata (capabilities, rotation, +// restrictions) to the database. This is needed for legacy mytokens where the metadata was not yet stored on +// creation. token version <0.7 +func SetMetadata( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, id mtid.MTID, meta MytokenDBMetadata, +) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec( + `CALL MTokens_SetMetadata(?,?,?,?)`, id, meta.Capabilities, meta.Rotation, meta.Restrictions, + ) + return errors.WithStack(err) + }, + ) +} + // revokeMT revokes the passed mytoken but no children func revokeMT(rlog log.Ext1FieldLogger, tx *sqlx.Tx, id interface{}) error { return db.RunWithinTransaction( @@ -157,11 +178,8 @@ func GetTokenUsagesAT(rlog log.Ext1FieldLogger, tx *sqlx.Tx, myID mtid.MTID, res return errors.WithStack(tx.Get(&usageCount, `CALL TokenUsages_GetAT(?,?)`, myID, restrictionHash)) }, ); err != nil { - if errors.Is(err, sql.ErrNoRows) { - // No usage entry -> was not used before -> usages=nil - err = nil // This is fine - return - } + _, err = db.ParseError(err) + // No usage entry -> was not used before -> usages=nil return } usages = &usageCount @@ -179,11 +197,8 @@ func GetTokenUsagesOther(rlog log.Ext1FieldLogger, tx *sqlx.Tx, myID mtid.MTID, return errors.WithStack(tx.Get(&usageCount, `CALL TokenUsages_GetOther(?,?)`, myID, restrictionHash)) }, ); err != nil { - if errors.Is(err, sql.ErrNoRows) { - // No usage entry -> was not used before -> usages=nil - err = nil // This is fine - return - } + _, err = db.ParseError(err) + // No usage entry -> was not used before -> usages=nil return } usages = &usageCount diff --git a/internal/db/dbrepo/mytokenrepo/transfercoderepo/proxytoken.go b/internal/db/dbrepo/mytokenrepo/shorttokenrepo/proxytoken.go similarity index 68% rename from internal/db/dbrepo/mytokenrepo/transfercoderepo/proxytoken.go rename to internal/db/dbrepo/mytokenrepo/shorttokenrepo/proxytoken.go index 542383f6..9f81b2e2 100644 --- a/internal/db/dbrepo/mytokenrepo/transfercoderepo/proxytoken.go +++ b/internal/db/dbrepo/mytokenrepo/shorttokenrepo/proxytoken.go @@ -1,8 +1,6 @@ -package transfercoderepo +package shorttokenrepo import ( - "database/sql" - "github.com/jmoiron/sqlx" "github.com/oidc-mytoken/utils/utils" "github.com/pkg/errors" @@ -14,8 +12,8 @@ import ( "github.com/oidc-mytoken/server/internal/utils/hashutils" ) -// proxyToken holds information for proxy tokens, i.e. tokens that proxy another token, e.g. a short token -type proxyToken struct { +// ProxyToken holds information for proxy tokens, i.e. tokens that proxy another token, e.g. a short token +type ProxyToken struct { id string token string mtID mtid.MTID @@ -24,43 +22,49 @@ type proxyToken struct { decryptedJWT string } -// newProxyToken creates a new proxyToken of the given length -func newProxyToken(size int) *proxyToken { +// NewProxyToken creates a new ProxyToken of the given length +func NewProxyToken(size int) *ProxyToken { token := utils.RandReadableAlphaString(size) - return createProxyToken(token) + return CreateProxyToken(token) } -func createProxyToken(token string) *proxyToken { +// CreateProxyToken creates a new *ProxyToken for a token +func CreateProxyToken(token string) *ProxyToken { id := hashutils.SHA512Str([]byte(token)) - return &proxyToken{ + return &ProxyToken{ id: id, token: token, } } -// parseProxyToken parses the proxy token string into a proxyToken -func parseProxyToken(token string) *proxyToken { +// ParseProxyToken parses the proxy token string into a proxyToken +func ParseProxyToken(token string) *ProxyToken { var id string if token != "" { id = hashutils.SHA512Str([]byte(token)) } - return &proxyToken{ + return &ProxyToken{ id: id, token: token, } } -func (pt proxyToken) String() string { +func (pt ProxyToken) String() string { return pt.Token() } -// Token returns the token of this proxyToken -func (pt proxyToken) Token() string { +// Token returns the token of this ProxyToken +func (pt ProxyToken) Token() string { return pt.token } +// MTID returns the mtid.MTID of this ProxyToken +func (pt ProxyToken) MTID() mtid.MTID { + return pt.mtID +} + // ID returns the id of this token -func (pt *proxyToken) ID() string { +func (pt *ProxyToken) ID() string { if pt.id == "" { pt.id = hashutils.SHA512Str([]byte(pt.token)) } @@ -68,7 +72,7 @@ func (pt *proxyToken) ID() string { } // SetJWT sets the jwt for this proxyToken -func (pt *proxyToken) SetJWT(jwt string, mID mtid.MTID) (err error) { +func (pt *ProxyToken) SetJWT(jwt string, mID mtid.MTID) (err error) { pt.mtID = mID pt.decryptedJWT = jwt pt.encryptedJWT, err = cryptutils.AES256Encrypt(jwt, pt.token) @@ -76,7 +80,7 @@ func (pt *proxyToken) SetJWT(jwt string, mID mtid.MTID) (err error) { } // JWT returns the decrypted jwt that is linked to this proxyToken -func (pt *proxyToken) JWT(rlog log.Ext1FieldLogger, tx *sqlx.Tx) (jwt string, valid bool, err error) { +func (pt *ProxyToken) JWT(rlog log.Ext1FieldLogger, tx *sqlx.Tx) (jwt string, valid bool, err error) { jwt = pt.decryptedJWT if jwt != "" { valid = true @@ -89,7 +93,7 @@ func (pt *proxyToken) JWT(rlog log.Ext1FieldLogger, tx *sqlx.Tx) (jwt string, va JWT string `db:"jwt"` MTID mtid.MTID `db:"MT_id"` } - if err = tx.Get(&res, `CALL ProxyTokens_GetMT(?)`, pt.id); err != nil { + if _, err = db.ParseError(tx.Get(&res, `CALL ProxyTokens_GetMT(?)`, pt.id)); err != nil { return errors.WithStack(err) } pt.encryptedJWT = res.JWT @@ -97,10 +101,6 @@ func (pt *proxyToken) JWT(rlog log.Ext1FieldLogger, tx *sqlx.Tx) (jwt string, va return nil }, ); err != nil { - if errors.Is(err, sql.ErrNoRows) { - err = nil - return - } return } } @@ -114,7 +114,7 @@ func (pt *proxyToken) JWT(rlog log.Ext1FieldLogger, tx *sqlx.Tx) (jwt string, va } // Store stores the proxyToken -func (pt proxyToken) Store(rlog log.Ext1FieldLogger, tx *sqlx.Tx) error { +func (pt ProxyToken) Store(rlog log.Ext1FieldLogger, tx *sqlx.Tx) error { return db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { _, err := tx.Exec(`CALL ProxyTokens_Insert(?,?,?)`, pt.id, pt.encryptedJWT, pt.mtID) @@ -124,7 +124,7 @@ func (pt proxyToken) Store(rlog log.Ext1FieldLogger, tx *sqlx.Tx) error { } // Update updates the jwt of the proxyToken -func (pt proxyToken) Update(rlog log.Ext1FieldLogger, tx *sqlx.Tx) error { +func (pt ProxyToken) Update(rlog log.Ext1FieldLogger, tx *sqlx.Tx) error { return db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { _, err := tx.Exec(`CALL ProxyTokens_Update(?,?,?)`, pt.ID(), pt.encryptedJWT, pt.mtID) @@ -135,7 +135,7 @@ func (pt proxyToken) Update(rlog log.Ext1FieldLogger, tx *sqlx.Tx) error { // Delete deletes the proxyToken from the database, it does not delete the linked Mytoken, the jwt should have been // retrieved earlier and the Mytoken if desired be revoked separately -func (pt proxyToken) Delete(rlog log.Ext1FieldLogger, tx *sqlx.Tx) error { +func (pt ProxyToken) Delete(rlog log.Ext1FieldLogger, tx *sqlx.Tx) error { return db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { _, err := tx.Exec(`CALL ProxyTokens_Delete(?)`, pt.id) diff --git a/internal/db/dbrepo/mytokenrepo/transfercoderepo/shorttoken.go b/internal/db/dbrepo/mytokenrepo/shorttokenrepo/shorttoken.go similarity index 77% rename from internal/db/dbrepo/mytokenrepo/transfercoderepo/shorttoken.go rename to internal/db/dbrepo/mytokenrepo/shorttokenrepo/shorttoken.go index b0401a0c..95b9fc5f 100644 --- a/internal/db/dbrepo/mytokenrepo/transfercoderepo/shorttoken.go +++ b/internal/db/dbrepo/mytokenrepo/shorttokenrepo/shorttoken.go @@ -1,4 +1,4 @@ -package transfercoderepo +package shorttokenrepo import ( "github.com/oidc-mytoken/server/internal/config" @@ -7,22 +7,22 @@ import ( // ShortToken holds database information of a short token type ShortToken struct { - proxyToken + ProxyToken } // NewShortToken creates a new short token from the given jwt of a normal Mytoken func NewShortToken(jwt string, mID mtid.MTID) (*ShortToken, error) { - pt := newProxyToken(config.Get().Features.ShortTokens.Len) + pt := NewProxyToken(config.Get().Features.ShortTokens.Len) if err := pt.SetJWT(jwt, mID); err != nil { return nil, err } shortToken := &ShortToken{ - proxyToken: *pt, + ProxyToken: *pt, } return shortToken, nil } // ParseShortToken creates a new short token from a short token string func ParseShortToken(token string) *ShortToken { - return &ShortToken{proxyToken: *parseProxyToken(token)} + return &ShortToken{ProxyToken: *ParseProxyToken(token)} } diff --git a/internal/db/dbrepo/mytokenrepo/transfercoderepo/pollingCodeTmpMT.go b/internal/db/dbrepo/mytokenrepo/transfercoderepo/pollingCodeTmpMT.go index 83eede8f..476d31a5 100644 --- a/internal/db/dbrepo/mytokenrepo/transfercoderepo/pollingCodeTmpMT.go +++ b/internal/db/dbrepo/mytokenrepo/transfercoderepo/pollingCodeTmpMT.go @@ -11,9 +11,10 @@ import ( "github.com/oidc-mytoken/server/internal/db" "github.com/oidc-mytoken/server/internal/db/dbrepo/authcodeinforepo/state" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/shorttokenrepo" "github.com/oidc-mytoken/server/internal/model" eventService "github.com/oidc-mytoken/server/internal/mytoken/event" - event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" ) @@ -29,7 +30,7 @@ type TransferCodeStatus struct { // CheckTransferCode checks the passed polling code in the database func CheckTransferCode(rlog log.Ext1FieldLogger, tx *sqlx.Tx, pollingCode string) (TransferCodeStatus, error) { - pt := createProxyToken(pollingCode) + pt := shorttokenrepo.CreateProxyToken(pollingCode) var p TransferCodeStatus err := db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { @@ -52,7 +53,7 @@ func PopTokenForTransferCode( ) ( jwt string, err error, ) { - pt := createProxyToken(pollingCode) + pt := shorttokenrepo.CreateProxyToken(pollingCode) var valid bool err = db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { @@ -67,10 +68,11 @@ func PopTokenForTransferCode( return err } return eventService.LogEvent( - rlog, tx, eventService.MTEvent{ - Event: event.FromNumber(event.TransferCodeUsed, ""), - MTID: pt.mtID, - }, clientMetadata, + rlog, tx, pkg.MTEvent{ + Event: api.EventTransferCodeUsed, + MTID: pt.MTID(), + ClientMetaData: clientMetadata, + }, ) }, ) @@ -79,7 +81,7 @@ func PopTokenForTransferCode( // LinkPollingCodeToMT links a pollingCode to a Mytoken func LinkPollingCodeToMT(rlog log.Ext1FieldLogger, tx *sqlx.Tx, pollingCode, jwt string, mID mtid.MTID) error { - pc := createProxyToken(pollingCode) + pc := shorttokenrepo.CreateProxyToken(pollingCode) if err := pc.SetJWT(jwt, mID); err != nil { return err } @@ -88,7 +90,7 @@ func LinkPollingCodeToMT(rlog log.Ext1FieldLogger, tx *sqlx.Tx, pollingCode, jwt // LinkPollingCodeToSSHKey links a pollingCode to an ssh public key func LinkPollingCodeToSSHKey(rlog log.Ext1FieldLogger, tx *sqlx.Tx, pollingCode, sshKeyHash string) error { - pc := createProxyToken(pollingCode) + pc := shorttokenrepo.CreateProxyToken(pollingCode) return db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { _, err := tx.Exec(`CALL TransferCodeAttributes_UpdateSSHKey(?,?)`, pc.ID(), sshKeyHash) @@ -99,7 +101,7 @@ func LinkPollingCodeToSSHKey(rlog log.Ext1FieldLogger, tx *sqlx.Tx, pollingCode, // DeleteTransferCodeByState deletes a polling code func DeleteTransferCodeByState(rlog log.Ext1FieldLogger, tx *sqlx.Tx, state *state.State) error { - pc := createProxyToken(state.PollingCode(rlog)) + pc := shorttokenrepo.CreateProxyToken(state.PollingCode(rlog)) return db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { _, err := tx.Exec(`CALL ProxyTokens_Delete(?)`, pc.ID()) @@ -110,7 +112,7 @@ func DeleteTransferCodeByState(rlog log.Ext1FieldLogger, tx *sqlx.Tx, state *sta // DeclineConsentByState updates the polling code attribute after the consent has been declined func DeclineConsentByState(rlog log.Ext1FieldLogger, tx *sqlx.Tx, state *state.State) error { - pc := createProxyToken(state.PollingCode(rlog)) + pc := shorttokenrepo.CreateProxyToken(state.PollingCode(rlog)) return db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { _, err := tx.Exec(`CALL TransferCodeAttributes_DeclineConsent(?)`, pc.ID()) diff --git a/internal/db/dbrepo/mytokenrepo/transfercoderepo/transfercode.go b/internal/db/dbrepo/mytokenrepo/transfercoderepo/transfercode.go index e6eb137d..aac2e9ef 100644 --- a/internal/db/dbrepo/mytokenrepo/transfercoderepo/transfercode.go +++ b/internal/db/dbrepo/mytokenrepo/transfercoderepo/transfercode.go @@ -10,13 +10,14 @@ import ( "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/shorttokenrepo" "github.com/oidc-mytoken/server/internal/model" "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" ) // TransferCode is a type used to transfer a token type TransferCode struct { - proxyToken + shorttokenrepo.ProxyToken Attributes transferCodeAttributes } @@ -29,12 +30,12 @@ type transferCodeAttributes struct { // NewTransferCode creates a new TransferCode for the passed jwt func NewTransferCode(jwt string, mID mtid.MTID, newMT bool, responseType model.ResponseType) (*TransferCode, error) { - pt := newProxyToken(config.Get().Features.Polling.Len) + pt := shorttokenrepo.NewProxyToken(config.Get().Features.Polling.Len) if err := pt.SetJWT(jwt, mID); err != nil { return nil, err } transferCode := &TransferCode{ - proxyToken: *pt, + ProxyToken: *pt, Attributes: transferCodeAttributes{ NewMT: db.BitBool(newMT), ResponseType: responseType, @@ -45,14 +46,14 @@ func NewTransferCode(jwt string, mID mtid.MTID, newMT bool, responseType model.R // ParseTransferCode creates a new transfer code from a transfer code string func ParseTransferCode(token string) *TransferCode { - return &TransferCode{proxyToken: *parseProxyToken(token)} + return &TransferCode{ProxyToken: *shorttokenrepo.ParseProxyToken(token)} } // CreatePollingCode creates a polling code func CreatePollingCode(pollingCode string, responseType model.ResponseType, maxTokenLen int) *TransferCode { - pt := createProxyToken(pollingCode) + pt := shorttokenrepo.CreateProxyToken(pollingCode) return &TransferCode{ - proxyToken: *pt, + ProxyToken: *pt, Attributes: transferCodeAttributes{ NewMT: true, ResponseType: responseType, @@ -66,12 +67,12 @@ func (tc TransferCode) Store(rlog log.Ext1FieldLogger, tx *sqlx.Tx) error { rlog.Debug("Storing transfer code") return db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { - if err := tc.proxyToken.Store(rlog, tx); err != nil { + if err := tc.ProxyToken.Store(rlog, tx); err != nil { return err } _, err := tx.Exec( `CALL TransferCodeAttributes_Insert(?,?,?,?,?)`, - tc.id, config.Get().Features.Polling.PollingCodeExpiresAfter, tc.Attributes.NewMT, + tc.ID(), config.Get().Features.Polling.PollingCodeExpiresAfter, tc.Attributes.NewMT, tc.Attributes.ResponseType, tc.Attributes.MaxTokenLen, ) return errors.WithStack(err) @@ -85,7 +86,7 @@ func (tc TransferCode) GetRevokeJWT(rlog log.Ext1FieldLogger, tx *sqlx.Tx) (bool var revokeMT db.BitBool err := db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { - if err := tx.Get(&revokeMT, `CALL TransferCodeAttributes_GetRevokeJWT(?)`, tc.id); err != nil { + if err := tx.Get(&revokeMT, `CALL TransferCodeAttributes_GetRevokeJWT(?)`, tc.ID()); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil } diff --git a/internal/db/dbrepo/mytokenrepo/tree/tree.go b/internal/db/dbrepo/mytokenrepo/tree/tree.go index de3ce279..dc6c6446 100644 --- a/internal/db/dbrepo/mytokenrepo/tree/tree.go +++ b/internal/db/dbrepo/mytokenrepo/tree/tree.go @@ -35,6 +35,17 @@ func (ste *MytokenEntry) Root() bool { return !ste.ParentID.HashValid() } +// SingleTokenEntry obtains the MytokenEntry for a single mytoken +func SingleTokenEntry(rlog log.Ext1FieldLogger, tx *sqlx.Tx, tokenID mtid.MTID) (m MytokenEntry, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + return errors.WithStack(tx.Get(&m, `CALL MTokens_GetInfo(?)`, tokenID)) + }, + ) + return + +} + // AllTokens returns information about all mytokens for the user linked to the passed mytoken func AllTokens(rlog log.Ext1FieldLogger, tx *sqlx.Tx, tokenID mtid.MTID) ([]*MytokenEntryTree, error) { var tokens []*MytokenEntry @@ -48,6 +59,16 @@ func AllTokens(rlog log.Ext1FieldLogger, tx *sqlx.Tx, tokenID mtid.MTID) ([]*Myt return tokensToTrees(tokens), nil } +// AllTokensByUID returns information about all mytokens for a user +func AllTokensByUID(rlog log.Ext1FieldLogger, tx *sqlx.Tx, uid uint64) (tokens []*MytokenEntry, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + return errors.WithStack(tx.Select(&tokens, `CALL MTokens_GetForUser(?)`, uid)) + }, + ) + return +} + // TokenSubTree returns information about all subtokens for the passed mytoken func TokenSubTree(rlog log.Ext1FieldLogger, tx *sqlx.Tx, tokenID mtid.MTID) (MytokenEntryTree, error) { var tokens []*MytokenEntry diff --git a/internal/db/dbrepo/userrepo/users.go b/internal/db/dbrepo/userrepo/users.go new file mode 100644 index 00000000..b4ca1dc0 --- /dev/null +++ b/internal/db/dbrepo/userrepo/users.go @@ -0,0 +1,91 @@ +package userrepo + +import ( + "github.com/gofiber/fiber/v2" + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/model" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" +) + +// MailInfo holds information about a user's mail settings +type MailInfo struct { + Mail db.NullString `db:"email"` + MailVerified bool `db:"email_verified"` + PreferHTMLMail bool `db:"prefer_html_mail"` +} + +// GetMail returns the mail address and verification status for a user linked to a mytoken +func GetMail(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID) (data MailInfo, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + return errors.WithStack(tx.Get(&data, `CALL Users_GetMail(?)`, mtID)) + }, + ) + return +} + +// GetAndCheckMail gets the MailInfo for a mytoken and already checks that it can be used +func GetAndCheckMail(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID) ( + data MailInfo, errRes *model.Response, + err error, +) { + data, err = GetMail(rlog, tx, mtID) + var found bool + found, err = db.ParseError(err) + if err != nil { + errRes = model.ErrorToInternalServerErrorResponse(err) + return + } + if !found || !data.Mail.Valid { + errRes = &model.Response{ + Status: fiber.StatusUnprocessableEntity, + Response: api.ErrorMailRequired, + } + err = errors.New("rollback") + return + } + if !data.MailVerified { + errRes = &model.Response{ + Status: fiber.StatusUnprocessableEntity, + Response: api.ErrorMailNotVerified, + } + err = errors.New("rollback") + return + } + return +} + +// ChangeEmail changes the user's email address +func ChangeEmail(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, newMail string) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL Users_ChangeMail(?,?)`, mtID, newMail) + return errors.WithStack(err) + }, + ) +} + +// ChangePreferredMailType changes the user's preferred email mimetype +func ChangePreferredMailType(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, preferHTML bool) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL Users_ChangePreferredMailType(?,?)`, mtID, preferHTML) + return errors.WithStack(err) + }, + ) +} + +// SetEmail sets a user's email address +func SetEmail(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, mail string, mailVerified bool) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL Users_SetMail(?,?,?)`, mtID, mail, mailVerified) + return errors.WithStack(err) + }, + ) +} diff --git a/internal/db/notificationsrepo/calendarrepo/calendar.go b/internal/db/notificationsrepo/calendarrepo/calendar.go new file mode 100644 index 00000000..d0c2a6f9 --- /dev/null +++ b/internal/db/notificationsrepo/calendarrepo/calendar.go @@ -0,0 +1,181 @@ +package calendarrepo + +import ( + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/endpoints/notification/calendar/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" +) + +// CalendarInfo is a type holding the information stored in the database related to a calendar +type CalendarInfo struct { + ID string `db:"id" json:"id"` + Name string `db:"name" json:"name"` + ICSPath string `db:"ics_path" json:"ics_path"` + ICS string `db:"ics" json:"-"` +} + +// Insert inserts a calendar for the given user (given by the mytoken) into the database +func Insert(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, info CalendarInfo) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL Calendar_Insert(?,?,?,?,?)`, mtID, info.ID, info.Name, info.ICSPath, info.ICS) + return errors.WithStack(err) + }, + ) +} + +// Delete deletes a calendar for the given user (given by the mytoken) from the database +func Delete(rlog log.Ext1FieldLogger, tx *sqlx.Tx, myid mtid.MTID, name string) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL Calendar_Delete(?,?)`, myid, name) + return errors.WithStack(err) + }, + ) +} + +// Update updates a calendar entry in the database +func Update(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, info CalendarInfo) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL Calendar_Update(?,?,?,?)`, mtID, info.ID, info.Name, info.ICS) + return errors.WithStack(err) + }, + ) +} + +// UpdateInternal updates a calendar entry in the database and does not require a mtid.MTID +func UpdateInternal(rlog log.Ext1FieldLogger, tx *sqlx.Tx, info CalendarInfo) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL Calendar_UpdateInternal(?,?,?)`, info.ID, info.Name, info.ICS) + return errors.WithStack(err) + }, + ) +} + +// GetMTsInCalendar returns a list of mytoken ids that are in a certain calendar +func GetMTsInCalendar(rlog log.Ext1FieldLogger, tx *sqlx.Tx, calendarID string) (mtids []string, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + return tx.Select(&mtids, `CALL Calendar_getMTsInCalendar(?)`, calendarID) + }, + ) + return +} + +// Get returns a calendar entry for a user and name +func Get(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID any, name string) (info CalendarInfo, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + return errors.WithStack(tx.Get(&info, `CALL Calendar_Get(?,?)`, mtID, name)) + }, + ) + return +} + +// GetByID returns a calendar entry for a certain calendar id +func GetByID(rlog log.Ext1FieldLogger, tx *sqlx.Tx, id string) (info CalendarInfo, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + return errors.WithStack(tx.Get(&info, `CALL Calendar_GetByID(?)`, id)) + }, + ) + return +} + +func calendarInfosToAPICalendarInfos(rlog log.Ext1FieldLogger, tx *sqlx.Tx, in []CalendarInfo) ( + out []api.CalendarInfo, err error, +) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + for _, i := range in { + info, err := i.toAPICalendarInfo(rlog, tx) + if err != nil { + return err + } + out = append(out, info) + } + return nil + }, + ) + return +} + +// toAPICalendarInfo transforms a CalendarInfo into an api.CalendarInfo +func (i CalendarInfo) toAPICalendarInfo(rlog log.Ext1FieldLogger, tx *sqlx.Tx) ( + out api.CalendarInfo, err error, +) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + out = api.CalendarInfo{ + NotificationCalendar: api.NotificationCalendar{ + Name: i.Name, + ICSPath: i.ICSPath, + }, + } + out.SubscribedTokens, err = GetMTsInCalendar(rlog, tx, i.ID) + return err + }, + ) + return +} + +// ToCalendarInfoResponse transforms a CalendarInfo into an pkg.CalendarInfoResponse +func (i CalendarInfo) ToCalendarInfoResponse(rlog log.Ext1FieldLogger, tx *sqlx.Tx) ( + *pkg.CalendarInfoResponse, error, +) { + info, err := i.toAPICalendarInfo(rlog, tx) + if err != nil { + return nil, err + } + return &pkg.CalendarInfoResponse{ + CalendarInfo: info, + }, nil + +} + +// List returns a list of all calendar entries for a user +func List(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID) (cals []api.CalendarInfo, err error) { + var infos []CalendarInfo + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + if err = errors.WithStack(tx.Select(&infos, `CALL Calendar_List(?)`, mtID)); err != nil { + return err + } + cals, err = calendarInfosToAPICalendarInfos(rlog, tx, infos) + return err + }, + ) + return +} + +// ListCalendarsForMT returns a list of calendars where the passed token is used in +func ListCalendarsForMT(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID any) (cals []api.CalendarInfo, err error) { + var infos []CalendarInfo + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + if err = errors.WithStack(tx.Select(&infos, `CALL Calendar_ListForMT(?)`, mtID)); err != nil { + return err + } + cals, err = calendarInfosToAPICalendarInfos(rlog, tx, infos) + return err + }, + ) + return +} + +// AddMytokenToCalendar associates a mytoken with a calendar in the database; you still have to update the ics +func AddMytokenToCalendar(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, calendarID string) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL Calendar_AddMytoken(?, ?)`, mtID, calendarID) + return errors.WithStack(err) + }, + ) +} diff --git a/internal/db/notificationsrepo/notifications.go b/internal/db/notificationsrepo/notifications.go new file mode 100644 index 00000000..0b44ec9b --- /dev/null +++ b/internal/db/notificationsrepo/notifications.go @@ -0,0 +1,342 @@ +package notificationsrepo + +import ( + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/db/notificationsrepo/calendarrepo" + "github.com/oidc-mytoken/server/internal/endpoints/notification/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" +) + +// ExpandNotificationsToChildrenIfApplicable checks if there is a notification subscription for the parent token that +// should include its children, and if so expands the subscription to also the just created child +func ExpandNotificationsToChildrenIfApplicable(rlog log.Ext1FieldLogger, tx *sqlx.Tx, parent, child mtid.MTID) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL Notifications_ExpandToChildren(?,?)`, parent, child) + return errors.WithStack(err) + }, + ) +} + +// NotificationInfoBaseWithClass is a type for holding information about a notification including class +type NotificationInfoBaseWithClass struct { + NotificationInfoBase + Class string `db:"class"` +} + +// NotificationInfoBase is a type for holding information about a notification +type NotificationInfoBase struct { + api.NotificationInfoBase + WebSocketPath db.NullString `db:"ws" json:"ws,omitempty"` + UID uint64 `db:"uid" json:"-"` +} + +// ManagementCodeNotificationInfoResponse extens api.ManagementCodeNotificationInfoResponse with an uid (not for json) +type ManagementCodeNotificationInfoResponse struct { + api.ManagementCodeNotificationInfoResponse + UID uint64 `db:"uid" json:"-"` +} + +// GetNotificationsForMTAndClass checks for and returns the found notifications for a certain mytoken and +// notification class +func GetNotificationsForMTAndClass( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, + class *api.NotificationClass, +) (notifications []api.NotificationInfoBase, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + var dbNotifications []NotificationInfoBase + _, err = db.ParseError( + tx.Select( + &dbNotifications, `CALL Notifications_GetForMTAndClass(?,?)`, mtID, class.Name, + ), + ) + if err != nil { + return errors.WithStack(err) + } + for _, n := range dbNotifications { + n.NotificationInfoBase.WebSocketPath = n.WebSocketPath.String + notifications = append(notifications, n.NotificationInfoBase) + } + return nil + }, + ) + return +} + +// GetNotificationsForMT checks for and returns the found notifications for a certain mytoken +func GetNotificationsForMT( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID any, +) (notifications []NotificationInfoBaseWithClass, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err = db.ParseError(tx.Select(¬ifications, `CALL Notifications_GetForMT(?)`, mtID)) + return errors.WithStack(err) + }, + ) + return +} + +// GetNotificationsAndCalendarsForMT obtains all api.NotificationInfo and api. +// CalendarInfo for a mytoken from the database +func GetNotificationsAndCalendarsForMT( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID any, +) (notifications []api.NotificationInfo, calendars []api.CalendarInfo, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + calendars, err = calendarrepo.ListCalendarsForMT(rlog, tx, mtID) + if err != nil { + return err + } + ns, err := GetNotificationsForMT(rlog, tx, mtID) + if err != nil { + return err + } + notifications, err = notificationInfoBaseWithClassToNotificationInfo(rlog, tx, ns) + return err + }, + ) + return +} + +func notificationInfoBaseWithClassToNotificationInfo( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, + in []NotificationInfoBaseWithClass, +) ( + out []api.NotificationInfo, + err error, +) { + notificationMap := make(map[uint64]api.NotificationInfo) + var ids []uint64 + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + for _, n := range in { + nie, ok := notificationMap[n.NotificationID] + if ok { + nie.Classes = append(nie.Classes, api.NewNotificationClass(n.Class)) + } else { + ids = append(ids, n.NotificationID) + n.NotificationInfoBase.NotificationInfoBase.WebSocketPath = n.WebSocketPath.String + nie = api.NotificationInfo{ + NotificationInfoBase: n.NotificationInfoBase.NotificationInfoBase, + Classes: api.NotificationClasses{api.NewNotificationClass(n.Class)}, + } + if !n.UserWide { + if err = tx.Select( + &nie.SubscribedTokens, `CALL Notifications_GetMTsForNotification(?)`, + n.NotificationID, + ); err != nil { + return err + } + } + } + notificationMap[nie.NotificationID] = nie + } + return nil + }, + ) + if err != nil { + return + } + for _, id := range ids { + out = append(out, notificationMap[id]) + } + return +} + +// GetNotificationsForUser returns all found notifications for a user +func GetNotificationsForUser( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, +) (notifications []api.NotificationInfo, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + var withClass []NotificationInfoBaseWithClass + _, err = db.ParseError(tx.Select(&withClass, `CALL Notifications_GetForUser(?)`, mtID)) + if err != nil { + return errors.WithStack(err) + } + notifications, err = notificationInfoBaseWithClassToNotificationInfo(rlog, tx, withClass) + return err + }, + ) + return +} + +// GetNotificationForManagementCode returns the notification for a management code +func GetNotificationForManagementCode( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, managementCode string, +) (info *ManagementCodeNotificationInfoResponse, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + var withClass []NotificationInfoBaseWithClass + found, err := db.ParseError( + tx.Select( + &withClass, `CALL Notifications_GetForManagementCode(?)`, + managementCode, + ), + ) + if err != nil { + return errors.WithStack(err) + } + if !found || len(withClass) == 0 { + info = nil + return nil + } + // we can have multiple entries for the different classes, but they will all be for the same notification id + info = &ManagementCodeNotificationInfoResponse{ + ManagementCodeNotificationInfoResponse: api.ManagementCodeNotificationInfoResponse{ + NotificationInfo: api.NotificationInfo{ + NotificationInfoBase: withClass[0].NotificationInfoBase.NotificationInfoBase, + }, + }, + UID: withClass[0].UID, + } + for _, n := range withClass { + info.Classes = append(info.Classes, api.NewNotificationClass(n.Class)) + } + if !info.UserWide { + if err = errors.WithStack( + tx.Select( + &info.SubscribedTokens, `CALL Notifications_GetMTsForNotification(?)`, + info.NotificationID, + ), + ); err != nil { + if _, err = db.ParseError(err); err != nil { + return err + } + } + } + return errors.WithStack(tx.Get(&info.OIDCIssuer, `CALL GetOIDCIssForManagementCode(?)`, managementCode)) + }, + ) + return +} + +// NewNotification stores a new notification in the database +func NewNotification( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req pkg.SubscribeNotificationRequest, + mtID mtid.MOMID, managementCode, ws string, +) error { + if req.UserWide { + return newUserWideNotification(rlog, tx, req, mtID, managementCode, ws) + } + return newMTNotification(rlog, tx, req, mtID, managementCode, ws) +} + +func newUserWideNotification( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req pkg.SubscribeNotificationRequest, + mtID mtid.MOMID, managementCode, ws string, +) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + var nid uint64 + if err := errors.WithStack( + tx.Get( + &nid, `CALL Notifications_CreateUserWide(?,?,?,?)`, mtID, req.NotificationType, + managementCode, db.NewNullString(ws), + ), + ); err != nil { + return err + } + return linkNotificationClasses(rlog, tx, nid, req.NotificationClasses) + }, + ) +} + +func newMTNotification( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req pkg.SubscribeNotificationRequest, + mtID mtid.MOMID, managementCode, ws string, +) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + var nid uint64 + if err := errors.WithStack( + tx.Get( + &nid, `CALL Notifications_CreateForMT(?,?,?,?,?)`, mtID, req.IncludeChildren, req.NotificationType, + managementCode, db.NewNullString(ws), + ), + ); err != nil { + return err + } + return linkNotificationClasses(rlog, tx, nid, req.NotificationClasses) + }, + ) +} + +func linkNotificationClasses( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, nid uint64, classes api.NotificationClasses, +) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + for _, nc := range classes { + _, err := tx.Exec(`CALL Notifications_LinkClass(?,?)`, nid, nc.Name) + if err != nil { + return errors.WithStack(err) + } + } + return nil + }, + ) +} + +// AddTokenToNotification subscribes an additional token (and possibly its children) to a notification +func AddTokenToNotification( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, notificationID uint64, mtID mtid.MOMID, includeChildren bool, +) (err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + if includeChildren { + _, err = tx.Exec(`CALL Notifications_LinkMTWithChildren(?,?)`, mtID, notificationID) + } else { + _, err = tx.Exec(`CALL Notifications_LinkMT(?,?,?)`, mtID, notificationID, 0) + } + return errors.WithStack(err) + + }, + ) + return +} + +// RemoveTokenFromNotification unsubscribes a token (and possibly its children) from a notification +func RemoveTokenFromNotification( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, notificationID uint64, mtID mtid.MOMID, +) (err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err = tx.Exec(`CALL Notifications_UnlinkMT(?,?)`, mtID, notificationID) + return errors.WithStack(err) + }, + ) + return +} + +// UpdateNotificationClasses updates the notification classes for a notification +func UpdateNotificationClasses( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, notificationID uint64, newClasses api.NotificationClasses, +) (err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err = tx.Exec(`CALL Notifications_ClearNotificationClasses(?)`, notificationID) + if err != nil { + return errors.WithStack(err) + } + return linkNotificationClasses(rlog, tx, notificationID, newClasses) + }, + ) + return +} + +// Delete deletes the notification for a managementCode +func Delete(rlog log.Ext1FieldLogger, tx *sqlx.Tx, managementCode string) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL Notifications_DeleteByManagementCode(?)`, managementCode) + return errors.WithStack(err) + }, + ) +} diff --git a/internal/db/notificationsrepo/schedule.go b/internal/db/notificationsrepo/schedule.go new file mode 100644 index 00000000..ebfaace6 --- /dev/null +++ b/internal/db/notificationsrepo/schedule.go @@ -0,0 +1,237 @@ +package notificationsrepo + +import ( + "database/sql/driver" + "encoding/json" + "time" + + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + "github.com/oidc-mytoken/utils/unixtime" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/db/dbrepo/actionrepo" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/tree" + "github.com/oidc-mytoken/server/internal/endpoints/actions/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" +) + +// Constants for scheduling classes +const ( + ScheduleClassExp = "exp" +) + +// Constants for AdditionalInfo keys +const ( + AdditionalInfoKeyExpiresAt = "expires_at" +) + +type jsonMap map[string]any + +// Scan implements the sql.Scanner interface. +func (m *jsonMap) Scan(src interface{}) error { + if src == nil { + return nil + } + val := src.([]uint8) + err := json.Unmarshal(val, m) + return err +} + +// Value implements the driver.Valuer interface +func (m jsonMap) Value() (driver.Value, error) { + if len(m) == 0 { + return nil, nil + } + return json.Marshal(m) +} + +// ScheduledNotification holds information about a notification that is scheduled +type ScheduledNotification struct { + ScheduleID uint64 `db:"id"` + DueTime time.Time `db:"due_time"` + MTID mtid.MTID `db:"MT_id"` + Class string `db:"class"` + AdditionalInfo jsonMap `db:"additional_info"` + NotificationID uint64 `db:"notification_id"` + NotificationInfoBase +} + +// PopOneScheduledNotification pops one ScheduledNotification from the database that is due +func PopOneScheduledNotification(rlog log.Ext1FieldLogger, tx *sqlx.Tx) (*ScheduledNotification, error) { + n := &ScheduledNotification{} + err := db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + err := errors.WithStack(tx.Get(n, `CALL PopOneDueScheduledNotification()`)) + if err != nil { + _, err = db.ParseError(err) + if err == nil { + n = nil + } + } + return err + }, + ) + return n, err +} + +var notificationIntervalsBeforeExpiration = []uint64{ + 30 * 24 * 60 * 60, + 14 * 24 * 60 * 60, + 7 * 24 * 60 * 60, + 3 * 24 * 60 * 60, + 1 * 24 * 60 * 60, + 3 * 60 * 60, + 0, +} + +// ScheduleExpirationNotificationsIfNeeded schedules all the needed expiration notifications if an exp notification +// is enabled for this mytoken +func ScheduleExpirationNotificationsIfNeeded( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, expiresAt unixtime.UnixTime, createdAt unixtime.UnixTime, +) error { + notifications, err := GetNotificationsForMTAndClass(rlog, tx, mtID, api.NotificationClassExpiration) + if err != nil { + return err + } + if len(notifications) == 0 { + return nil + } + for _, n := range notifications { + if err = ScheduleExpirationNotifications(rlog, tx, n.NotificationID, mtID, expiresAt, createdAt); err != nil { + return err + } + } + return nil +} + +// ScheduleExpirationNotifications schedules all the needed expiration notifications for this mytoken +func ScheduleExpirationNotifications( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, nid uint64, mtID mtid.MTID, + expiresAt unixtime.UnixTime, createdAt unixtime.UnixTime, +) error { + now := unixtime.Now() + if expiresAt == 0 || expiresAt < now { + return nil + } + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + + tokenLifetime := uint64(expiresAt - createdAt) + halfTokenLifetime := tokenLifetime / 2 + var numberScheduledNotifications uint + for _, secondsBeforeExpiration := range notificationIntervalsBeforeExpiration { + if secondsBeforeExpiration > halfTokenLifetime { + continue + } + notifyAt := expiresAt - unixtime.UnixTime(secondsBeforeExpiration) + if notifyAt < now { + continue + } + data := ScheduledNotification{ + DueTime: notifyAt.Time(), + MTID: mtID, + Class: ScheduleClassExp, + AdditionalInfo: jsonMap{AdditionalInfoKeyExpiresAt: expiresAt}, + NotificationID: nid, + } + _, err := tx.NamedExec( + `CALL NotificationScheduleAdd(:due_time,:notification_id,:MT_id,:class,:additional_info)`, data, + ) + if err = errors.WithStack(err); err != nil { + return err + } + numberScheduledNotifications++ + } + if numberScheduledNotifications > 0 { + _, err := actionrepo.GetScheduledNotificationActionCode(rlog, tx, mtID, nid) + var found bool + found, err = db.ParseError(err) + if err != nil { + return err + } + if !found { + if err = actionrepo.AddScheduleNotificationCode(rlog, tx, mtID, nid, pkg.NewCode()); err != nil { + return err + } + } + } + return nil + }, + ) +} + +// DeleteScheduledExpirationNotifications deletes all scheduled notifications for a Notification +func DeleteScheduledExpirationNotifications(rlog log.Ext1FieldLogger, tx *sqlx.Tx, nid uint64) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL NotificationSchedule_DeleteExpirations(?)`, nid) + return errors.WithStack(err) + }, + ) +} + +// DeleteScheduledExpirationNotificationsForMT deletes all scheduled notifications for a Notification and a certain +// mytoken +func DeleteScheduledExpirationNotificationsForMT( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, nid uint64, mtID mtid.MTID, +) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL NotificationSchedule_DeleteExpirationsForMT(?,?)`, nid, mtID) + return errors.WithStack(err) + }, + ) +} + +// AddScheduledExpirationNotifications adds all the needed expiration notifications for all the mytokens linked to a +// Notification +func AddScheduledExpirationNotifications(rlog log.Ext1FieldLogger, tx *sqlx.Tx, info NotificationInfoBase) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + var err error + if info.UserWide { + err = scheduleUserWideNotifications(rlog, tx, info) + } else { + err = scheduleTokenWideNotifications(rlog, tx, info) + } + return err + }, + ) +} + +func scheduleUserWideNotifications(rlog log.Ext1FieldLogger, tx *sqlx.Tx, info NotificationInfoBase) error { + tokens, err := tree.AllTokensByUID(rlog, tx, info.UID) + if err != nil { + return err + } + for _, mt := range tokens { + if err = ScheduleExpirationNotifications( + rlog, tx, info.NotificationID, mt.ID, mt.ExpiresAt, mt.CreatedAt, + ); err != nil { + return err + } + } + return nil +} + +func scheduleTokenWideNotifications(rlog log.Ext1FieldLogger, tx *sqlx.Tx, info NotificationInfoBase) error { + var mtids []mtid.MTID + if err := tx.Select(&mtids, `CALL Notifications_GetMTsForNotification(?)`, info.NotificationID); err != nil { + return err + } + for _, mtID := range mtids { + tokenEntry, err := tree.SingleTokenEntry(rlog, tx, mtID) + if err != nil { + return err + } + if err = ScheduleExpirationNotifications( + rlog, tx, info.NotificationID, mtID, tokenEntry.ExpiresAt, tokenEntry.CreatedAt, + ); err != nil { + return err + } + } + return nil +} diff --git a/internal/db/profilerepo/get.go b/internal/db/profilerepo/get.go index dbe109b7..1b97d00f 100644 --- a/internal/db/profilerepo/get.go +++ b/internal/db/profilerepo/get.go @@ -36,7 +36,7 @@ func GetProfiles(rlog log.Ext1FieldLogger, tx *sqlx.Tx, group string) (profiles rlog, tx, group, func(group string, dbReader *dbProfileReader) (map[string]profileData, error) { return dbReader.readAllProfile(group) - }, func(content []byte, parser *profile.ProfileParser) (interface{}, error) { + }, func(content []byte, parser *profile.Parser) (interface{}, error) { return parser.ParseProfile(content) }, ) @@ -48,7 +48,7 @@ func GetRestrictionsTemplates(rlog log.Ext1FieldLogger, tx *sqlx.Tx, group strin rlog, tx, group, func(group string, dbReader *dbProfileReader) (map[string]profileData, error) { return dbReader.readAllRestrictionsTemplate(group) - }, func(content []byte, parser *profile.ProfileParser) (interface{}, error) { + }, func(content []byte, parser *profile.Parser) (interface{}, error) { return parser.ParseRestrictionsTemplate(content) }, ) @@ -60,7 +60,7 @@ func GetCapabilitiesTemplates(rlog log.Ext1FieldLogger, tx *sqlx.Tx, group strin rlog, tx, group, func(group string, dbReader *dbProfileReader) (map[string]profileData, error) { return dbReader.readAllCapabilityTemplate(group) - }, func(content []byte, parser *profile.ProfileParser) (interface{}, error) { + }, func(content []byte, parser *profile.Parser) (interface{}, error) { return parser.ParseCapabilityTemplate(content) }, ) @@ -72,7 +72,7 @@ func GetRotationTemplates(rlog log.Ext1FieldLogger, tx *sqlx.Tx, group string) ( rlog, tx, group, func(group string, dbReader *dbProfileReader) (map[string]profileData, error) { return dbReader.readAllRotationTemplate(group) - }, func(content []byte, parser *profile.ProfileParser) (interface{}, error) { + }, func(content []byte, parser *profile.Parser) (interface{}, error) { return parser.ParseRotationTemplate(content) }, ) @@ -112,38 +112,34 @@ func getRestrictionsTemplates(rlog log.Ext1FieldLogger, tx *sqlx.Tx, group strin } func profileGetAndParse( - rlog log.Ext1FieldLogger, tx *sqlx.Tx, group string, + rlog log.Ext1FieldLogger, _ *sqlx.Tx, group string, readAllFnc func(string, *dbProfileReader) (map[string]profileData, error), - parseFnc func([]byte, *profile.ProfileParser) (interface{}, error), -) (profiles []api.Profile, err error) { - err = db.RunWithinTransaction( - rlog, tx, func(tx *sqlx.Tx) error { - dbReader := newDBProfileReader(rlog) - p := profile.NewProfileParser(dbReader) - groupData, err := readAllFnc(group, dbReader) - if err != nil { - return err - } - for _, d := range groupData { - rot, err := parseFnc([]byte(d.Payload), p) - if err != nil { - return err - } - parsedPayload, err := json.Marshal(rot) - if err != nil { - return errors.WithStack(err) - } - d.Payload = string(parsedPayload) - profiles = append( - profiles, api.Profile{ - ID: d.ID.String(), - Name: d.Name, - Payload: json.RawMessage(d.Payload), - }, - ) - } - return nil - }, - ) - return + parseFnc func([]byte, *profile.Parser) (interface{}, error), +) ([]api.Profile, error) { + dbReader := newDBProfileReader(rlog) + p := profile.NewParser(dbReader) + groupData, err := readAllFnc(group, dbReader) + if err != nil { + return nil, err + } + var profiles []api.Profile + for _, d := range groupData { + rot, err := parseFnc([]byte(d.Payload), p) + if err != nil { + return nil, err + } + parsedPayload, err := json.Marshal(rot) + if err != nil { + return nil, errors.WithStack(err) + } + d.Payload = string(parsedPayload) + profiles = append( + profiles, api.Profile{ + ID: d.ID.String(), + Name: d.Name, + Payload: json.RawMessage(d.Payload), + }, + ) + } + return profiles, nil } diff --git a/internal/db/profilerepo/parser.go b/internal/db/profilerepo/parser.go index e832fc1d..024ef160 100644 --- a/internal/db/profilerepo/parser.go +++ b/internal/db/profilerepo/parser.go @@ -65,8 +65,8 @@ func newDBProfileReader(rlog log.Ext1FieldLogger) *dbProfileReader { } // NewDBProfileParser creates a new profile.ProfileParser that can read profiles from the db -func NewDBProfileParser(rlog log.Ext1FieldLogger) *profile.ProfileParser { - return profile.NewProfileParser(newDBProfileReader(rlog)) +func NewDBProfileParser(rlog log.Ext1FieldLogger) *profile.Parser { + return profile.NewParser(newDBProfileReader(rlog)) } func read( diff --git a/internal/endpoints/actions/actions.go b/internal/endpoints/actions/actions.go new file mode 100644 index 00000000..c1b71d2a --- /dev/null +++ b/internal/endpoints/actions/actions.go @@ -0,0 +1,187 @@ +package actions + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + "github.com/oidc-mytoken/utils/unixtime" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/db/dbrepo/actionrepo" + "github.com/oidc-mytoken/server/internal/endpoints/actions/pkg" + "github.com/oidc-mytoken/server/internal/model" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" + "github.com/oidc-mytoken/server/internal/server/routes" + "github.com/oidc-mytoken/server/internal/utils/ctxutils" + "github.com/oidc-mytoken/server/internal/utils/logger" +) + +// HandleActions is the main entry function to handle the different actions of the action endpoint +func HandleActions(ctx *fiber.Ctx) error { + actionInfo := pkg.CtxGetActionInfo(ctx) + switch actionInfo.Action { + case pkg.ActionRecreate: + return handleRecreate(ctx, actionInfo.Code) + case pkg.ActionVerifyEmail: + return handleVerifyEmail(ctx, actionInfo.Code) + case pkg.ActionRemoveFromCalendar: + return handleRemoveFromCalendar(ctx, actionInfo.Code) + case pkg.ActionUnsubscribeScheduled: + return handleUnsubscribeScheduled(ctx, actionInfo.Code) + } + return ctxutils.RenderErrorPage( + ctx, fiber.StatusBadRequest, model.BadRequestError("unknown action"). + CombinedMessage(), + ) +} + +func handleRecreate(ctx *fiber.Ctx, code string) (err error) { + rlog := logger.GetRequestLogger(ctx) + var found bool + var baseRequest string + err = db.Transact( + rlog, func(tx *sqlx.Tx) error { + var data actionrepo.RecreateData + data, found, err = actionrepo.GetRecreateData(rlog, tx, code) + if err != nil || !found { + return err + } + var req api.GeneralMytokenRequest + req.Issuer = data.Issuer + if data.Name.Valid { + req.Name = data.Name.String + } + req.Rotation = data.Rotation + req.Capabilities = data.Capabilities + if data.Restrictions != nil { + restr := make(api.Restrictions, len(data.Restrictions)) + created := data.Created + now := unixtime.Now() + diff := now - created + for i, r := range data.Restrictions { + apiR := r.Restriction + if r.NotBefore != 0 { + apiR.NotBefore = int64(r.NotBefore + diff) + } + if r.ExpiresAt != 0 { + apiR.ExpiresAt = int64(r.ExpiresAt + diff) + } + restr[i] = &apiR + } + req.Restrictions = restr + } + j, err := json.Marshal(req) + if err != nil { + return err + } + baseRequest = base64.RawURLEncoding.EncodeToString(j) + return nil + }, + ) + if err != nil { + return ctxutils.RenderInternalServerErrorPage(ctx, err) + } + if !found { + return ctxutils.RenderErrorPage(ctx, fiber.StatusNotFound, "recreation code not found") + } + return ctx.Redirect(fmt.Sprintf("/?r=%s#mt", baseRequest), fiber.StatusSeeOther) +} + +func handleVerifyEmail(ctx *fiber.Ctx, code string) error { + rlog := logger.GetRequestLogger(ctx) + verified, err := actionrepo.VerifyMail(rlog, nil, code) + if err != nil { + return ctxutils.RenderInternalServerErrorPage(ctx, err) + } + if !verified { + return ctxutils.RenderErrorPage(ctx, http.StatusBadRequest, "code not valid or expired") + } + return ctxutils.RenderErrorPage( + ctx, http.StatusOK, "The email address was successfully verified.", "Email Verified", + ) +} + +func handleRemoveFromCalendar(ctx *fiber.Ctx, code string) error { + rlog := logger.GetRequestLogger(ctx) + err := actionrepo.UseRemoveCalendarCode(rlog, nil, code) + if err != nil { + return ctxutils.RenderInternalServerErrorPage(ctx, err) + } + return ctxutils.RenderErrorPage( + ctx, http.StatusOK, "The token was successfully removed from the calendar.", "Token Removed from Calendar", + ) +} + +func handleUnsubscribeScheduled(ctx *fiber.Ctx, code string) error { + rlog := logger.GetRequestLogger(ctx) + err := actionrepo.UseUnsubscribeFurtherNotificationsCode(rlog, nil, code) + if err != nil { + return ctxutils.RenderInternalServerErrorPage(ctx, err) + } + return ctxutils.RenderErrorPage( + ctx, http.StatusOK, "You have successfully unsubscribed from further notifications of this kind.", + "Unsubscribed", + ) +} + +// CreateVerifyEmail creates an action url for verifying a mail address +func CreateVerifyEmail(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID) (string, error) { + code := pkg.ActionInfo{ + Action: pkg.ActionVerifyEmail, + Code: pkg.NewCode(), + } + if err := actionrepo.AddVerifyEmailCode(rlog, tx, mtID, code.Code, pkg.CodeLifetimes[code.Action]); err != nil { + return "", err + } + return routes.ActionsURL(code), nil +} + +// CreateRecreateToken creates an action url for recreating a mytoken +func CreateRecreateToken(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID) (string, error) { + code := pkg.ActionInfo{ + Action: pkg.ActionRecreate, + Code: pkg.NewCode(), + } + if err := actionrepo.AddRecreateTokenCode(rlog, tx, mtID, code.Code); err != nil { + return "", err + } + return routes.ActionsURL(code), nil +} + +// CreateRemoveFromCalendar creates an action url for removing a token from a calendar +func CreateRemoveFromCalendar(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, calendarName string) ( + string, + error, +) { + code := pkg.ActionInfo{ + Action: pkg.ActionRemoveFromCalendar, + Code: pkg.NewCode(), + } + if err := actionrepo.AddRemoveFromCalendarCode(rlog, tx, mtID, code.Code, calendarName); err != nil { + return "", err + } + return routes.ActionsURL(code), nil +} + +// GetUnsubscribeScheduled obtains the action code stored in the database for a scheduled notification and returns +// the action url +func GetUnsubscribeScheduled(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, nid uint64) ( + string, + error, +) { + code, err := actionrepo.GetScheduledNotificationActionCode(rlog, tx, mtID, nid) + if err != nil { + return "", err + } + ac := pkg.ActionInfo{ + Action: pkg.ActionUnsubscribeScheduled, + Code: code, + } + return routes.ActionsURL(ac), nil +} diff --git a/internal/endpoints/actions/pkg/actions.go b/internal/endpoints/actions/pkg/actions.go new file mode 100644 index 00000000..e81b1f0a --- /dev/null +++ b/internal/endpoints/actions/pkg/actions.go @@ -0,0 +1,41 @@ +package pkg + +import ( + "github.com/gofiber/fiber/v2" + "github.com/oidc-mytoken/utils/utils" +) + +// Actions +const ( + ActionRecreate = "recreate_token" + ActionVerifyEmail = "verify_email" + ActionRemoveFromCalendar = "remove_from_calendar" + ActionUnsubscribeScheduled = "unsubscribe_scheduled" +) + +// CodeLifetimes holds the default lifetime of the different action codes +var CodeLifetimes = map[string]int{ + ActionVerifyEmail: 3600, + ActionRecreate: 0, + ActionRemoveFromCalendar: 0, + ActionUnsubscribeScheduled: 0, +} + +// ActionInfo is type for associating an Action with a Code +type ActionInfo struct { + Action string + Code string +} + +// CtxGetActionInfo obtains the ActionInfo from a fiber.Ctx +func CtxGetActionInfo(ctx *fiber.Ctx) ActionInfo { + return ActionInfo{ + Action: ctx.Query("action"), + Code: ctx.Query("code"), + } +} + +// NewCode creates a new code +func NewCode() string { + return utils.RandASCIIString(32) +} diff --git a/internal/endpoints/configuration/configurationEndpoint.go b/internal/endpoints/configuration/configurationEndpoint.go index 5ed79d6c..3ac2f2ee 100644 --- a/internal/endpoints/configuration/configurationEndpoint.go +++ b/internal/endpoints/configuration/configurationEndpoint.go @@ -14,6 +14,8 @@ import ( "github.com/oidc-mytoken/server/internal/server/paths" ) +// SupportedProviders returns a list of all the api. +// SupportedProviderConfig including providers specified in the config file as well as possible oidc fed providers func SupportedProviders() []api.SupportedProviderConfig { if config.Get().Features.Federation.Enabled { mytokenConfig.ProvidersSupported = append(getProvidersFromConfig(), oidcfed.SupportedProviders()...) @@ -24,15 +26,14 @@ func SupportedProviders() []api.SupportedProviderConfig { } // HandleConfiguration handles calls to the configuration endpoint -func HandleConfiguration(ctx *fiber.Ctx) error { +func HandleConfiguration(*fiber.Ctx) *model.Response { if config.Get().Features.Federation.Enabled { mytokenConfig.ProvidersSupported = append(getProvidersFromConfig(), oidcfed.SupportedProviders()...) } - res := model.Response{ + return &model.Response{ Status: fiber.StatusOK, Response: mytokenConfig, } - return res.Send(ctx) } var mytokenConfig *pkg.MytokenConfiguration @@ -71,17 +72,18 @@ func basicConfiguration() *pkg.MytokenConfiguration { otherPaths := paths.GetGeneralPaths() return &pkg.MytokenConfiguration{ MytokenConfiguration: api.MytokenConfiguration{ - Issuer: config.Get().IssuerURL, - AccessTokenEndpoint: utils.CombineURLPath(config.Get().IssuerURL, apiPaths.AccessTokenEndpoint), - MytokenEndpoint: utils.CombineURLPath(config.Get().IssuerURL, apiPaths.MytokenEndpoint), - TokeninfoEndpoint: utils.CombineURLPath(config.Get().IssuerURL, apiPaths.TokenInfoEndpoint), - UserSettingsEndpoint: utils.CombineURLPath(config.Get().IssuerURL, apiPaths.UserSettingEndpoint), - ProfilesEndpoint: utils.CombineURLPath(config.Get().IssuerURL, apiPaths.ProfilesEndpoint), - JWKSURI: utils.CombineURLPath(config.Get().IssuerURL, otherPaths.JWKSEndpoint), - ProvidersSupported: getProvidersFromConfig(), - TokenSigningAlgValue: config.Get().Signing.Mytoken.Alg.String(), - ServiceDocumentation: config.Get().ServiceDocumentation, - Version: version.VERSION, + Issuer: config.Get().IssuerURL, + AccessTokenEndpoint: utils.CombineURLPath(config.Get().IssuerURL, apiPaths.AccessTokenEndpoint), + MytokenEndpoint: utils.CombineURLPath(config.Get().IssuerURL, apiPaths.MytokenEndpoint), + TokeninfoEndpoint: utils.CombineURLPath(config.Get().IssuerURL, apiPaths.TokenInfoEndpoint), + UserSettingsEndpoint: utils.CombineURLPath(config.Get().IssuerURL, apiPaths.UserSettingEndpoint), + NotificationsEndpoint: utils.CombineURLPath(config.Get().IssuerURL, apiPaths.NotificationEndpoint), + ProfilesEndpoint: utils.CombineURLPath(config.Get().IssuerURL, apiPaths.ProfilesEndpoint), + JWKSURI: utils.CombineURLPath(config.Get().IssuerURL, otherPaths.JWKSEndpoint), + ProvidersSupported: getProvidersFromConfig(), + TokenSigningAlgValue: config.Get().Signing.Mytoken.Alg.String(), + ServiceDocumentation: config.Get().ServiceDocumentation, + Version: version.VERSION, }, AccessTokenEndpointGrantTypesSupported: []model.GrantType{model.GrantTypeMytoken}, MytokenEndpointGrantTypesSupported: []model.GrantType{ diff --git a/internal/endpoints/consent/consent.go b/internal/endpoints/consent/consent.go index 91d9b358..1e74296f 100644 --- a/internal/endpoints/consent/consent.go +++ b/internal/endpoints/consent/consent.go @@ -16,6 +16,7 @@ import ( "github.com/oidc-mytoken/server/internal/db" pkg2 "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/endpoints/webentities" "github.com/oidc-mytoken/server/internal/model/profiled" "github.com/oidc-mytoken/server/internal/mytoken/restrictions" "github.com/oidc-mytoken/server/internal/oidc/oidcfed" @@ -45,8 +46,8 @@ func handleConsent(ctx *fiber.Ctx, info *pkg2.OIDCFlowRequest, includeConsentCal templating.MustacheKeyEmptyNavbar: true, templating.MustacheKeyRestrictionsGUI: true, templating.MustacheKeyCollapse: templating.Collapsable{All: true}, - templating.MustacheKeyRestrictions: pkg.WebRestrictions{Restrictions: info.Restrictions.Restrictions}, - templating.MustacheKeyCapabilities: pkg.AllWebCapabilities(), + templating.MustacheKeyRestrictions: webentities.WebRestrictions{Restrictions: info.Restrictions.Restrictions}, + templating.MustacheKeyCapabilities: webentities.AllWebCapabilities(), templating.MustacheKeyCheckedCapabilities: c.Strings(), templating.MustacheKeyIss: info.Issuer, @@ -74,7 +75,7 @@ func getAuthInfoFromConsentCodeStr(rlog log.Ext1FieldLogger, code string) ( ) { consentCode := state.ConsentCodeFromStr(code) oState := state.NewState(consentCode.GetState()) - authInfo, err := authcodeinforepo.GetAuthFlowInfoByState(rlog, oState) + authInfo, err := authcodeinforepo.GetAuthFlowInfoByState(rlog, nil, oState) if err != nil { if errors.Is(err, sql.ErrNoRows) { err = fiber.ErrNotFound @@ -92,10 +93,7 @@ func HandleCreateConsent(ctx *fiber.Ctx) error { return model.ErrorToBadRequestErrorResponse(err).Send(ctx) } if req.Issuer == "" { - return model.Response{ - Status: fiber.StatusBadRequest, - Response: model.BadRequestError("required parameter 'oidc_issuer' missing"), - }.Send(ctx) + return model.BadRequestErrorResponse("required parameter 'oidc_issuer' missing").Send(ctx) } rlog := logger.GetRequestLogger(ctx) mt, _ := auth.RequireValidMytoken(rlog, nil, &req.Mytoken, ctx) @@ -123,7 +121,10 @@ func HandleConsent(ctx *fiber.Ctx) error { return handleConsent(ctx, &(authInfo.AuthCodeFlowRequest.OIDCFlowRequest), true) } -func handleConsentDecline(ctx *fiber.Ctx, authInfo *authcodeinforepo.AuthFlowInfoOut, oState *state.State) error { +func handleConsentDecline( + ctx *fiber.Ctx, authInfo *authcodeinforepo.AuthFlowInfoOut, + oState *state.State, +) *model.Response { rlog := logger.GetRequestLogger(ctx) url := "/" if authInfo.PollingCode { @@ -138,19 +139,19 @@ func handleConsentDecline(ctx *fiber.Ctx, authInfo *authcodeinforepo.AuthFlowInf m := utils.StructToStringMapUsingJSONTags(res.Response) m["url"] = url res.Response = m - return res.Send(ctx) + return res } if authInfo.PollingCode { if err := transfercoderepo.DeclineConsentByState(rlog, nil, oState); err != nil { rlog.Errorf("%s", errorfmt.Full(err)) } } - return model.Response{ + return &model.Response{ Status: httpstatus.StatusOKForward, Response: map[string]string{ "url": url, }, - }.Send(ctx) + } } // handleConsentAccept handles the acceptance of a consent code @@ -160,10 +161,7 @@ func handleConsentAccept( ) *model.Response { for _, c := range req.Capabilities { if !api.AllCapabilities.Has(c) { - return &model.Response{ - Status: fiber.StatusBadRequest, - Response: model.BadRequestError(fmt.Sprintf("unknown capability '%s'", c)), - } + return model.BadRequestErrorResponse(fmt.Sprintf("unknown capability '%s'", c)) } } p := provider2.GetProvider(req.Issuer) @@ -200,19 +198,19 @@ func handleConsentAccept( } // HandleConsentPost handles consent confirmation requests -func HandleConsentPost(ctx *fiber.Ctx) error { +func HandleConsentPost(ctx *fiber.Ctx) *model.Response { rlog := logger.GetRequestLogger(ctx) authInfo, oState, err := getAuthInfoFromConsentCodeStr(rlog, ctx.Params("consent_code")) if err != nil { // Don't log error here, it was already logged - return err + return model.ErrorToInternalServerErrorResponse(err) } if len(ctx.Body()) == 0 { return handleConsentDecline(ctx, authInfo, oState) } req := pkg.ConsentApprovalRequest{} if err = json.Unmarshal(ctx.Body(), &req); err != nil { - return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + return model.ErrorToBadRequestErrorResponse(err) } - return handleConsentAccept(rlog, &req, oState).Send(ctx) + return handleConsentAccept(rlog, &req, oState) } diff --git a/internal/endpoints/federation/entityStatement.go b/internal/endpoints/federation/entityStatement.go index cfb62669..840c47cf 100644 --- a/internal/endpoints/federation/entityStatement.go +++ b/internal/endpoints/federation/entityStatement.go @@ -6,7 +6,7 @@ import ( "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/valyala/fasthttp" - oidcfed "github.com/zachmann/go-oidcfed/pkg" + oidfed "github.com/zachmann/go-oidfed/pkg" "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/jws" @@ -15,6 +15,7 @@ import ( "github.com/oidc-mytoken/server/internal/server/paths" ) +// InitEntityConfiguration initializes the entity configuration if enabled func InitEntityConfiguration() { if config.Get().Features.Federation.Entity != nil { return @@ -22,12 +23,13 @@ func InitEntityConfiguration() { otherPaths := paths.GetGeneralPaths() privacyURI := utils.CombineURLPath(config.Get().IssuerURL, otherPaths.Privacy) var err error - config.Get().Features.Federation.Entity, err = oidcfed.NewFederationLeaf( + jwks := jws.GetJWKS(jws.KeyUsageOIDCSigning) + config.Get().Features.Federation.Entity, err = oidfed.NewFederationLeaf( config.Get().IssuerURL, config.Get().Features.Federation.AuthorityHints, config.Get().Features.Federation.TrustAnchors, - &oidcfed.Metadata{ - RelyingParty: &oidcfed.OpenIDRelyingPartyMetadata{ + &oidfed.Metadata{ + RelyingParty: &oidfed.OpenIDRelyingPartyMetadata{ RedirectURIS: []string{ utils.CombineURLPath( config.Get().IssuerURL, otherPaths.OIDCRedirectEndpoint, @@ -44,13 +46,13 @@ func InitEntityConfiguration() { ClientURI: config.Get().IssuerURL, PolicyURI: privacyURI, TOSURI: privacyURI, - JWKS: jws.GetJWKS(jws.KeyUsageOIDCSigning), + JWKS: &jwks, SoftwareID: version.SOFTWAREID, SoftwareVersion: version.VERSION, OrganizationName: config.Get().ServiceOperator.Name, - ClientRegistrationTypes: []string{oidcfed.ClientRegistrationTypeAutomatic}, + ClientRegistrationTypes: []string{oidfed.ClientRegistrationTypeAutomatic}, }, - FederationEntity: &oidcfed.FederationEntityMetadata{ + FederationEntity: &oidfed.FederationEntityMetadata{ OrganizationName: config.Get().ServiceOperator.Name, Contacts: []string{config.Get().ServiceOperator.Contact}, LogoURI: utils.CombineURLPath(config.Get().IssuerURL, "static/img/mytoken.png"), @@ -58,8 +60,10 @@ func InitEntityConfiguration() { HomepageURI: "https://mytoken-docs.data.kit.edu", }, }, - jws.GetSigningKey(jws.KeyUsageFederation), - config.Get().Features.Federation.Signing.Alg, + oidfed.NewEntityStatementSigner( + jws.GetSigningKey(jws.KeyUsageFederation), + config.Get().Features.Federation.Signing.Alg, + ), config.Get().Features.Federation.EntityConfigurationLifetime, jws.GetSigningKey(jws.KeyUsageOIDCSigning), config.Get().Signing.OIDC.Alg, @@ -79,7 +83,7 @@ func (r entityStatementResponse) Send(ctx *fiber.Ctx) error { // HandleEntityConfiguration handles calls to the oidc federation entity configuration endpoint func HandleEntityConfiguration(ctx *fiber.Ctx) error { - entityConfigurationJWT, err := config.Get().Features.Federation.Entity.EntityConfiguration().JWT() + entityConfigurationJWT, err := config.Get().Features.Federation.Entity.EntityConfigurationJWT() if err != nil { err = errors.Wrap(err, "could not create entity configuration JWT") return model.ErrorToInternalServerErrorResponse(err).Send(ctx) diff --git a/internal/endpoints/guestmode/guestmode.go b/internal/endpoints/guestmode/guestmode.go index 57e1e608..8efc058c 100644 --- a/internal/endpoints/guestmode/guestmode.go +++ b/internal/endpoints/guestmode/guestmode.go @@ -13,6 +13,7 @@ import ( "github.com/oidc-mytoken/server/internal/utils/hashutils" ) +// Init initializes the guest mode func Init(s fiber.Router) { if !config.Get().Features.GuestMode.Enabled { return diff --git a/internal/endpoints/notification/calendar/calendar.go b/internal/endpoints/notification/calendar/calendar.go new file mode 100644 index 00000000..5f94a3f4 --- /dev/null +++ b/internal/endpoints/notification/calendar/calendar.go @@ -0,0 +1,556 @@ +package calendar + +import ( + "fmt" + "net/http" + "strings" + "time" + + ics "github.com/arran4/golang-ical" + "github.com/gofiber/fiber/v2" + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + "github.com/oidc-mytoken/utils/utils" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/tree" + "github.com/oidc-mytoken/server/internal/db/dbrepo/userrepo" + "github.com/oidc-mytoken/server/internal/db/notificationsrepo/calendarrepo" + "github.com/oidc-mytoken/server/internal/endpoints/actions" + "github.com/oidc-mytoken/server/internal/endpoints/notification/calendar/pkg" + pkg4 "github.com/oidc-mytoken/server/internal/endpoints/notification/pkg" + "github.com/oidc-mytoken/server/internal/model" + mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" + "github.com/oidc-mytoken/server/internal/mytoken/universalmytoken" + notifier "github.com/oidc-mytoken/server/internal/notifier/client" + "github.com/oidc-mytoken/server/internal/notifier/server/mailing" + "github.com/oidc-mytoken/server/internal/server/routes" + "github.com/oidc-mytoken/server/internal/utils/auth" + "github.com/oidc-mytoken/server/internal/utils/ctxutils" + "github.com/oidc-mytoken/server/internal/utils/logger" + "github.com/oidc-mytoken/server/internal/utils/mytokenutils" +) + +var calendarNotFoundError = model.NotFoundErrorResponse("calendar not found") + +// HandleGetICS returns a calendar ics by its id +func HandleGetICS(ctx *fiber.Ctx) error { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle get ics calendar request") + cid := ctx.Params("id") + var info calendarrepo.CalendarInfo + if err := db.Transact( + rlog, func(tx *sqlx.Tx) error { + var err error + info, err = calendarrepo.GetByID(rlog, tx, cid) + if err != nil { + return err + } + + cal, err := ics.ParseCalendar(strings.NewReader(info.ICS)) + if err != nil { + return err + } + mtids, err := calendarrepo.GetMTsInCalendar(rlog, tx, info.ID) + if err != nil { + return err + } + for _, e := range cal.Events() { + id := e.Id() + if !utils.StringInSlice(id, mtids) { + cal.RemoveEvent(id) + cal.SetLastModified(time.Now()) + } + } + newICS := cal.Serialize() + if newICS != info.ICS { + info.ICS = newICS + return calendarrepo.UpdateInternal(rlog, tx, info) + } + return nil + }, + ); err != nil { + _, e := db.ParseError(err) + if e != nil { + return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + } + return calendarNotFoundError.Send(ctx) + } + ctx.Set(fiber.HeaderContentType, "text/calendar") + ctx.Set(fiber.HeaderContentDisposition, fmt.Sprintf(`attachment; filename=%q`, info.Name)) + return ctx.SendString(info.ICS) +} + +// HandleAdd handles a request to create a new calendar +func HandleAdd(ctx *fiber.Ctx) *model.Response { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle add calendar request") + var umt universalmytoken.UniversalMytoken + mt, errRes := auth.RequireValidMytoken(rlog, nil, &umt, ctx) + if errRes != nil { + return errRes + } + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, nil, mt, + ctxutils.ClientMetaData(ctx), api.CapabilityNotifyAnyToken, + ) + if errRes != nil { + return errRes + } + var calendarInfo api.NotificationCalendar + if err := errors.WithStack(ctx.BodyParser(&calendarInfo)); err != nil { + return model.ErrorToBadRequestErrorResponse(err) + } + if calendarInfo.Name == "" { + return model.BadRequestErrorResponse("required parameter 'name' is missing") + } + + id := utils.RandASCIIString(32) + cal := ics.NewCalendar() + cal.SetMethod(ics.MethodPublish) + cal.SetName(calendarInfo.Name) + cal.SetDescription( + fmt.Sprintf( + "This calendar contains events and reminders for expiring mytokens issued from '%s'", + config.Get().IssuerURL, + ), + ) + icsPath := utils.CombineURLPath(routes.CalendarDownloadEndpoint, id) + cal.SetUrl(icsPath) + calendarInfo.ICSPath = icsPath + dbInfo := calendarrepo.CalendarInfo{ + ID: id, + Name: calendarInfo.Name, + ICSPath: icsPath, + ICS: cal.Serialize(), + } + res := &model.Response{ + Status: http.StatusCreated, + Response: &pkg.CreateCalendarResponse{NotificationCalendar: calendarInfo}, + } + if err := db.Transact( + rlog, func(tx *sqlx.Tx) error { + if err := calendarrepo.Insert(rlog, tx, mt.ID, dbInfo); err != nil { + return err + } + var rollback bool + res, rollback = mytokenutils.DoAfterRequestThingsOther( + rlog, tx, res, mt, *ctxutils.ClientMetaData(ctx), + api.EventCalendarCreated, calendarInfo.Name, usedRestriction, umt.JWT, umt.OriginalTokenType, + ) + if rollback { + return errors.New("rollback") + } + return nil + }, + ); err != nil && res == nil { + return model.ErrorToInternalServerErrorResponse(err) + } + return res +} + +// HandleDelete deletes a calendar +func HandleDelete(ctx *fiber.Ctx) *model.Response { + rlog := logger.GetRequestLogger(ctx) + name := ctxutils.Params(ctx, "name") + rlog.WithField("calendar", name).Debug("Handle delete calendar request") + var umt universalmytoken.UniversalMytoken + mt, errRes := auth.RequireValidMytoken(rlog, nil, &umt, ctx) + if errRes != nil { + return errRes + } + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, nil, mt, + ctxutils.ClientMetaData(ctx), api.CapabilityNotifyAnyToken, + ) + if errRes != nil { + return errRes + } + + var res *model.Response + if err := db.Transact( + rlog, func(tx *sqlx.Tx) error { + if err := calendarrepo.Delete(rlog, tx, mt.ID, name); err != nil { + return err + } + var rollback bool + res, rollback = mytokenutils.DoAfterRequestThingsOther( + rlog, tx, nil, mt, *ctxutils.ClientMetaData(ctx), + api.EventCalendarDeleted, name, usedRestriction, umt.JWT, umt.OriginalTokenType, + ) + if rollback { + return errors.New("rollback") + } + return nil + }, + ); err != nil && res == nil { + res = model.ErrorToInternalServerErrorResponse(err) + } + if res == nil { + res = &model.Response{ + Status: http.StatusNoContent, + } + } + return res +} + +// HandleGet looks up the id for a calendar name for the given user (by mytoken) and redirects to the ics endpoint +func HandleGet(ctx *fiber.Ctx) error { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle get calendar request") + calendarName := ctxutils.Params(ctx, "name") + var umt universalmytoken.UniversalMytoken + mt, errRes := auth.RequireValidMytoken(rlog, nil, &umt, ctx) + if errRes != nil { + return errRes.Send(ctx) + } + info, err := calendarrepo.Get(rlog, nil, mt.ID, calendarName) + if err != nil { + _, e := db.ParseError(err) + if e != nil { + return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + } + return calendarNotFoundError.Send(ctx) + } + return ctx.Redirect(info.ICSPath) +} + +// HandleList lists all calendars for a user +func HandleList(ctx *fiber.Ctx) *model.Response { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle list calendar request") + var umt universalmytoken.UniversalMytoken + mt, errRes := auth.RequireValidMytoken(rlog, nil, &umt, ctx) + if errRes != nil { + return errRes + } + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, nil, mt, + ctxutils.ClientMetaData(ctx), api.CapabilityNotifyAnyTokenRead, + ) + if errRes != nil { + return errRes + } + var res *model.Response + if err := db.Transact( + rlog, func(tx *sqlx.Tx) error { + infos, err := calendarrepo.List(rlog, tx, mt.ID) + if err != nil { + return err + } + res = &model.Response{ + Status: fiber.StatusOK, + Response: &pkg.CalendarListResponse{CalendarListResponse: api.CalendarListResponse{Calendars: infos}}, + } + var rollback bool + res, rollback = mytokenutils.DoAfterRequestThingsOther( + rlog, tx, res, mt, *ctxutils.ClientMetaData(ctx), + api.EventCalendarListed, "", usedRestriction, umt.JWT, umt.OriginalTokenType, + ) + if rollback { + return errors.New("rollback") + } + return nil + }, + ); err != nil && res == nil { + res = model.ErrorToInternalServerErrorResponse(err) + } + return res +} + +// HandleCalendarEntryViaMail creates a calendar entry for a mytoken and sends it via mail +func HandleCalendarEntryViaMail( + ctx *fiber.Ctx, rlog logrus.Ext1FieldLogger, mt *mytoken.Mytoken, + req pkg4.SubscribeNotificationRequest, +) *model.Response { + rlog.Debug("Handle calendar entry via mail request") + var res *model.Response + _ = db.Transact( + rlog, func(tx *sqlx.Tx) error { + clientMetadata := ctxutils.ClientMetaData(ctx) + id := mt.ID + momMode := req.MomID.Hash() != id.Hash() + if momMode { + id = req.MomID.MTID + if res = auth.RequireMytokenIsParentOrCapability( + rlog, tx, api.CapabilityTokeninfoNotify, + api.CapabilityNotifyAnyToken, mt, id, clientMetadata, + ); res != nil { + return errors.New("rollback") + } + if res = auth.RequireMytokensForSameUser(rlog, tx, id, mt.ID); res != nil { + return errors.New("rollback") + } + } + usedRestriction, errRes := auth.RequireUsableRestrictionOther(rlog, tx, mt, clientMetadata) + if errRes != nil { + res = errRes + return errors.New("rollback") + } + mailInfo, errRes, err := userrepo.GetAndCheckMail(rlog, tx, id) + if err != nil { + res = errRes + return err + } + mtInfo, err := tree.SingleTokenEntry(rlog, tx, id) + if err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + calText, errRes := mailCalendarForMytoken( + rlog, tx, id, mtInfo.Name.String, req.Comment, mailInfo.Mail.String, + ) + if errRes != nil { + res = errRes + return errors.New("rollback") + } + + filename := mtInfo.Name.String + if filename == "" { + filename = id.Hash() + } + notifier.SendICSMail( + mailInfo.Mail.String, + fmt.Sprintf("Mytoken Expiration Calendar Reminder for '%s'", filename), + "You can add the event to your calendar to be notified before the mytoken expires.", + mailing.Attachment{ + Reader: strings.NewReader(calText), + Filename: filename + ".ics", + ContentType: "text/calendar", + }, + ) + + mytokenEvent := api.EventNotificationSubscribed + if momMode { + mytokenEvent = api.EventNotificationSubscribedOther + } + var rollback bool + res, rollback = mytokenutils.DoAfterRequestThingsOther( + rlog, tx, &model.Response{ + Status: http.StatusNoContent, + }, mt, *ctxutils.ClientMetaData(ctx), mytokenEvent, "email calendar entry", usedRestriction, + req.Mytoken.JWT, req.Mytoken.OriginalTokenType, + ) + if rollback { + return errors.New("rollback") + } + return nil + }, + ) + return res +} + +// HandleAddMytoken handles a request to add a mytoken to a calendar +func HandleAddMytoken(ctx *fiber.Ctx) *model.Response { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle add mytoken to calendar request") + + clientMetadata := ctxutils.ClientMetaData(ctx) + calendarName := ctxutils.Params(ctx, "name") + var umt universalmytoken.UniversalMytoken + mt, errRes := auth.RequireValidMytoken(rlog, nil, &umt, ctx) + if errRes != nil { + return errRes + } + + var req pkg.AddMytokenToCalendarRequest + if err := errors.WithStack(ctx.BodyParser(&req)); err != nil { + return model.ErrorToBadRequestErrorResponse(err) + } + + id, momMode, errRes := validateMomMode(rlog, mt, req, clientMetadata) + if errRes != nil { + return errRes + } + usedRestriction, errRes := auth.RequireUsableRestrictionOther(rlog, nil, mt, clientMetadata) + if errRes != nil { + return errRes + } + + var res *model.Response + _ = db.Transact( + rlog, func(tx *sqlx.Tx) error { + info, err := calendarrepo.Get(rlog, tx, id, calendarName) + if err != nil { + _, e := db.ParseError(err) + if e == nil { + res = calendarNotFoundError + } else { + res = model.ErrorToInternalServerErrorResponse(err) + } + return err + } + if err = calendarrepo.AddMytokenToCalendar(rlog, tx, id, info.ID); err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + cal, err := ics.ParseCalendar(strings.NewReader(info.ICS)) + if err != nil { + err = errors.WithStack(err) + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + event, errRes := eventForMytoken(rlog, tx, id, req.Comment, true, calendarName) + if errRes != nil { + res = errRes + return errors.New("rollback") + } + cal.AddVEvent(event) + info.ICS = cal.Serialize() + if err = calendarrepo.Update(rlog, tx, id, info); err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + + mytokenEvent := api.EventNotificationSubscribed + if momMode { + mytokenEvent = api.EventNotificationSubscribedOther + } + resInfo, err := info.ToCalendarInfoResponse(rlog, tx) + if err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + res = &model.Response{ + Status: http.StatusOK, + Response: resInfo, + } + var rollback bool + res, rollback = mytokenutils.DoAfterRequestThingsOther( + rlog, tx, res, mt, *ctxutils.ClientMetaData(ctx), + mytokenEvent, fmt.Sprintf("calendar '%s'", info.Name), usedRestriction, umt.JWT, umt.OriginalTokenType, + ) + if rollback { + return errors.New("rollback") + } + return nil + }, + ) + return res +} + +func validateMomMode( + rlog logrus.Ext1FieldLogger, mt *mytoken.Mytoken, req pkg.AddMytokenToCalendarRequest, + clientMetadata *api.ClientMetaData, +) (mtid.MTID, bool, *model.Response) { + id := mt.ID + momMode := req.MomID.Hash() != id.Hash() + if momMode { + id = req.MomID.MTID + if errRes := auth.RequireMytokenIsParentOrCapability( + rlog, nil, api.CapabilityTokeninfoNotify, api.CapabilityNotifyAnyToken, mt, id, clientMetadata, + ); errRes != nil { + return id, momMode, errRes + } + if errRes := auth.RequireMytokensForSameUser(rlog, nil, id, mt.ID); errRes != nil { + return id, momMode, errRes + } + } + return id, momMode, nil +} + +func eventForMytoken( + rlog logrus.Ext1FieldLogger, tx *sqlx.Tx, id mtid.MTID, comment string, + unsubscribeOption bool, calendarName string, +) (event *ics.VEvent, errRes *model.Response) { + _ = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + mt, err := tree.SingleTokenEntry(rlog, tx, id) + if err != nil { + errRes = model.ErrorToInternalServerErrorResponse(err) + return err + } + if mt.ExpiresAt == 0 { + errRes = model.BadRequestErrorResponse("cannot create an expiration event for non-expiring mytokens") + return nil + } + event = ics.NewEvent(id.Hash()) + now := time.Now() + event.SetCreatedTime(now) + event.SetDtStampTime(now) + event.SetModifiedAt(now) + event.SetStartAt(mt.ExpiresAt.Time()) + event.SetEndAt(mt.ExpiresAt.Time()) + title := "Mytoken expires" + if mt.Name.Valid { + title = fmt.Sprintf("Mytoken '%s' expires", mt.Name.String) + } + event.SetSummary(title) + description := comment + if description != "" { + description += "\n\n" + } + recreateURL, err := actions.CreateRecreateToken(rlog, tx, id) + if err != nil { + errRes = model.ErrorToInternalServerErrorResponse(err) + return err + } + description += fmt.Sprintf( + "To re-create a mytoken with similiar properties follow this link:\n"+ + "%s\n", recreateURL, + ) + if unsubscribeOption { + unsubscribeURL, err := actions.CreateRemoveFromCalendar(rlog, tx, id, calendarName) + if err != nil { + errRes = model.ErrorToInternalServerErrorResponse(err) + return err + } + description += fmt.Sprintf( + "To remove this mytoken from calendar '%s' follow this link:\n"+ + "%s\n", calendarName, unsubscribeURL, + ) + } + event.SetURL(recreateURL) + event.SetDescription(description) + createAlarms(event, mt, 30, 14, 7, 3, 1, 0) + return nil + }, + ) + return +} +func mailCalendarForMytoken(rlog logrus.Ext1FieldLogger, tx *sqlx.Tx, id mtid.MTID, name, comment, to string) ( + string, + *model.Response, +) { + event, errRes := eventForMytoken(rlog, tx, id, comment, false, "") + if errRes != nil { + return "", errRes + } + event.AddAttendee(to) + cal := ics.NewCalendar() + cal.SetMethod(ics.MethodRequest) + cal.SetName(name) + cal.AddVEvent(event) + return cal.Serialize(), nil +} + +func createAlarms(event *ics.VEvent, info tree.MytokenEntry, triggerDaysBeforeExpiration ...int) { + for _, d := range triggerDaysBeforeExpiration { + if a := createAlarm(d, info); a != nil { + event.Components = append(event.Components, a) + } + } +} + +func createAlarm(daysBeforeExpiration int, info tree.MytokenEntry) *ics.VAlarm { + now := time.Now() + expiresAt := info.ExpiresAt.Time() + createdAt := info.CreatedAt.Time() + triggerTime := expiresAt.Add(time.Duration(-24*daysBeforeExpiration) * time.Hour) + if triggerTime.Before(now) { + return nil + } + if triggerTime.Before(createdAt.Add(expiresAt.Sub(createdAt) / 2)) { + return nil + } + alarm := &ics.VAlarm{ + ComponentBase: ics.ComponentBase{}, + } + alarm.SetAction(ics.ActionDisplay) + alarm.SetTrigger(fmt.Sprintf("-PT%dD", daysBeforeExpiration)) + return alarm +} diff --git a/internal/endpoints/notification/calendar/pkg/calendarRequest.go b/internal/endpoints/notification/calendar/pkg/calendarRequest.go new file mode 100644 index 00000000..a2da8ef0 --- /dev/null +++ b/internal/endpoints/notification/calendar/pkg/calendarRequest.go @@ -0,0 +1,32 @@ +package pkg + +import ( + "github.com/oidc-mytoken/api/v0" + + "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" +) + +// AddMytokenToCalendarRequest is type holding the request to add a mytoken to a calendar +type AddMytokenToCalendarRequest struct { + api.AddMytokenToCalendarRequest + MomID mtid.MOMID `json:"mom_id" xml:"mom_id" form:"mom_id"` +} + +// CreateCalendarResponse is the response returned when a new calendar is created +type CreateCalendarResponse struct { + api.NotificationCalendar + pkg.OnlyTokenUpdateRes +} + +// CalendarListResponse is the response returned to list all calendars of a user +type CalendarListResponse struct { + api.CalendarListResponse + pkg.OnlyTokenUpdateRes +} + +// CalendarInfoResponse is a type for holding a response with information about a single calendar +type CalendarInfoResponse struct { + api.CalendarInfo + pkg.OnlyTokenUpdateRes +} diff --git a/internal/endpoints/notification/notifications.go b/internal/endpoints/notification/notifications.go new file mode 100644 index 00000000..05dbb962 --- /dev/null +++ b/internal/endpoints/notification/notifications.go @@ -0,0 +1,442 @@ +package notification + +import ( + "github.com/gofiber/fiber/v2" + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + "github.com/oidc-mytoken/utils/unixtime" + "github.com/oidc-mytoken/utils/utils" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/mytokenrepohelper" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/tree" + "github.com/oidc-mytoken/server/internal/db/dbrepo/userrepo" + "github.com/oidc-mytoken/server/internal/db/notificationsrepo" + "github.com/oidc-mytoken/server/internal/endpoints/notification/calendar" + "github.com/oidc-mytoken/server/internal/endpoints/notification/pkg" + "github.com/oidc-mytoken/server/internal/model" + mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" + "github.com/oidc-mytoken/server/internal/mytoken/restrictions" + "github.com/oidc-mytoken/server/internal/mytoken/universalmytoken" + notifier "github.com/oidc-mytoken/server/internal/notifier/client" + "github.com/oidc-mytoken/server/internal/server/routes" + "github.com/oidc-mytoken/server/internal/utils/auth" + "github.com/oidc-mytoken/server/internal/utils/ctxutils" + "github.com/oidc-mytoken/server/internal/utils/logger" + "github.com/oidc-mytoken/server/internal/utils/mytokenutils" +) + +var managementCodeNotValidError = model.NotFoundErrorResponse("management_code not valid") +var missingManagementCodeError = model.BadRequestErrorResponse("missing management_code") + +// HandleGetByManagementCode returns the api.NotificationInfo for the notification linked to a management code +func HandleGetByManagementCode(ctx *fiber.Ctx) *model.Response { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle notification get request for management code") + managementCode := ctx.Params("code") + if managementCode == "" { + return missingManagementCodeError + } + + var res *model.Response + _ = db.Transact( + rlog, func(tx *sqlx.Tx) error { + info, err := notificationsrepo.GetNotificationForManagementCode(rlog, tx, managementCode) + if err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + if info == nil { + res = managementCodeNotValidError + return errors.New("rollback") + } + res = &model.Response{ + Status: fiber.StatusOK, + Response: info.ManagementCodeNotificationInfoResponse, + } + return nil + }, + ) + return res +} + +// HandleDeleteByManagementCode returns the api.NotificationInfo for the notification linked to a management code +func HandleDeleteByManagementCode(ctx *fiber.Ctx) *model.Response { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle notification get request for management code") + managementCode := ctx.Params("code") + if managementCode == "" { + return missingManagementCodeError + } + + err := notificationsrepo.Delete(rlog, nil, managementCode) + if err != nil { + return model.ErrorToInternalServerErrorResponse(err) + } + return &model.Response{Status: fiber.StatusNoContent} +} + +// HandleGet handles get requests and returns a list of all notifications for a user +func HandleGet(ctx *fiber.Ctx) *model.Response { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle notification get request") + var umt universalmytoken.UniversalMytoken + mt, errRes := auth.RequireValidMytoken(rlog, nil, &umt, ctx) + if errRes != nil { + return errRes + } + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, nil, mt, + ctxutils.ClientMetaData(ctx), api.CapabilityNotifyAnyTokenRead, + ) + if errRes != nil { + return errRes + } + var res *model.Response + if err := db.Transact( + rlog, func(tx *sqlx.Tx) error { + infos, err := notificationsrepo.GetNotificationsForUser(rlog, tx, mt.ID) + if err != nil { + return err + } + res = &model.Response{ + Status: fiber.StatusOK, + Response: &pkg.NotificationsListResponse{ + NotificationsListResponse: api.NotificationsListResponse{ + Notifications: infos, + }, + }, + } + var rollback bool + res, rollback = mytokenutils.DoAfterRequestThingsOther( + rlog, tx, res, mt, *ctxutils.ClientMetaData(ctx), + api.EventNotificationListed, "", usedRestriction, umt.JWT, umt.OriginalTokenType, + ) + if rollback { + return errors.New("rollback") + } + return nil + }, + ); err != nil && res == nil { + res = model.ErrorToInternalServerErrorResponse(err) + } + return res +} + +// HandlePost is the main entry function for handling notification creation requests +func HandlePost(ctx *fiber.Ctx) *model.Response { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle notification create request") + var req pkg.SubscribeNotificationRequest + if err := ctx.BodyParser(&req); err != nil { + return model.ErrorToBadRequestErrorResponse(err) + } + mt, errRes := auth.RequireValidMytoken(rlog, nil, &req.Mytoken, ctx) + if errRes != nil { + return errRes + } + managementCode := utils.RandASCIIString(64) + switch req.NotificationType { + case api.NotificationTypeICSInvite: + return calendar.HandleCalendarEntryViaMail(ctx, rlog, mt, req) + case api.NotificationTypeMail: + return handleNewMailNotification(ctx, rlog, mt, req, managementCode) + case api.NotificationTypeWebsocket: + return &model.ResponseNYI + default: + return model.BadRequestErrorResponse("unknown notification_type") + } +} + +func handleNewMailNotification( + ctx *fiber.Ctx, rlog logrus.Ext1FieldLogger, mt *mytoken.Mytoken, + req pkg.SubscribeNotificationRequest, managementCode string, +) *model.Response { + var res *model.Response + if err := db.Transact( + rlog, func(tx *sqlx.Tx) error { + mtID, requiredCapability, welcomeData, errRes, err := prepareNotificationWelcomeData( + rlog, tx, mt, req, managementCode, + ) + if err != nil { + res = errRes + return err + } + var usedRestriction *restrictions.Restriction + usedRestriction, res = auth.RequireCapabilityAndRestrictionOther( + rlog, tx, mt, ctxutils.ClientMetaData(ctx), requiredCapability, + ) + if res != nil { + return errors.New("rollback") + } + if err = notificationsrepo.NewNotification(rlog, tx, req, mtID, managementCode, ""); err != nil { + return err + } + if req.NotificationClasses.Contains(api.NotificationClassExpiration) { + var withClass []notificationsrepo.NotificationInfoBaseWithClass + if err = tx.Select( + &withClass, `CALL Notifications_GetForManagementCode(?)`, managementCode, + ); err != nil { + return err + } + if err = notificationsrepo.AddScheduledExpirationNotifications( + rlog, tx, withClass[0].NotificationInfoBase, + ); err != nil { + return err + } + } + emailInfo, errRes, err := userrepo.GetAndCheckMail(rlog, tx, mt.ID) + if err != nil { + res = errRes + return err + } + + notifier.SendTemplateEmail( + emailInfo.Mail.String, "New Mytoken Notification Subscription", + emailInfo.PreferHTMLMail, "notification-welcome", welcomeData, + ) + + res = &model.Response{ + Status: fiber.StatusCreated, + Response: &pkg.NotificationsCreateResponse{ + NotificationsCreateResponse: api.NotificationsCreateResponse{ + ManagementCode: managementCode, + }, + }, + } + e := api.EventNotificationCreated + if req.MomID.HashValid() { + e = api.EventNotificationCreatedOther + } + var rollback bool + res, rollback = mytokenutils.DoAfterRequestThingsOther( + rlog, tx, res, mt, *ctxutils.ClientMetaData(ctx), e, "", + usedRestriction, req.Mytoken.JWT, req.Mytoken.OriginalTokenType, + ) + if rollback { + return errors.New("rollback") + } + return nil + }, + ); err != nil && res == nil { + res = model.ErrorToInternalServerErrorResponse(err) + } + return res +} + +func prepareNotificationWelcomeData( + rlog logrus.Ext1FieldLogger, tx *sqlx.Tx, mt *mytoken.Mytoken, + req pkg.SubscribeNotificationRequest, managementCode string, +) (mtid.MOMID, api.Capability, map[string]interface{}, *model.Response, error) { + mtID := mtid.MOMID{MTID: mt.ID} + welcomeData := map[string]interface{}{ + "management-url": routes.NotificationManagementURL(managementCode), + "token-name": mt.Name, + "issuer-url": config.Get().IssuerURL, + "notification_classes": req.NotificationClasses, + } + requiredCapability := api.CapabilityTokeninfoNotify + + if req.MomID.HashValid() { + mtID = req.MomID + if errRes := auth.RequireMytokensForSameUser(rlog, tx, mtID.MTID, mt.ID); errRes != nil { + return mtID, requiredCapability, welcomeData, errRes, errors.New("rollback") + } + name, err := mytokenrepohelper.GetMTName(rlog, tx, mtID.MTID) + if err != nil { + return mtID, requiredCapability, welcomeData, nil, err + } + welcomeData["token-name"] = name.String + requiredCapability = api.CapabilityNotifyAnyToken + } + if !req.UserWide { + welcomeData["mtid"] = mtID.Hash() + } + + return mtID, requiredCapability, welcomeData, nil, nil +} + +// HandleNotificationUpdateClasses handles requests to update the NotificationClasses for a notification +func HandleNotificationUpdateClasses(ctx *fiber.Ctx) *model.Response { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle notification update classes request") + managementCode := ctx.Params("code") + if managementCode == "" { + return missingManagementCodeError + } + var req api.NotificationUpdateNotificationClassesRequest + if err := ctx.BodyParser(&req); err != nil { + return model.ErrorToBadRequestErrorResponse(err) + } + var res *model.Response + err := db.Transact( + rlog, func(tx *sqlx.Tx) error { + info, err := notificationsrepo.GetNotificationForManagementCode(rlog, tx, managementCode) + if err != nil { + return err + } + if info == nil { + res = managementCodeNotValidError + return errors.New("rollback") + } + if err = notificationsrepo.UpdateNotificationClasses( + rlog, tx, info.NotificationID, + req.Classes, + ); err != nil { + return err + } + includedExpBefore := info.Classes.Contains(api.NotificationClassExpiration) + includesExpNow := req.Classes.Contains(api.NotificationClassExpiration) + if includesExpNow && !includedExpBefore { + // exp class was added + if err = notificationsrepo.AddScheduledExpirationNotifications( + rlog, tx, + notificationsrepo.NotificationInfoBase{ + NotificationInfoBase: info.NotificationInfoBase, + WebSocketPath: db.NewNullString(info.WebSocketPath), + UID: info.UID, + }, + ); err != nil { + return err + } + } + if includedExpBefore && !includesExpNow { + // exp class was removed + if err = notificationsrepo.DeleteScheduledExpirationNotifications( + rlog, tx, info.NotificationID, + ); err != nil { + return err + } + } + return nil + }, + ) + if err != nil && res == nil { + res = model.ErrorToInternalServerErrorResponse(err) + } + if res == nil { + res = &model.Response{Status: fiber.StatusNoContent} + } + return res +} + +// HandleNotificationAddToken handles requests to add a mytoken to a notification +func HandleNotificationAddToken(ctx *fiber.Ctx) *model.Response { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle notification add token request") + managementCode := ctx.Params("code") + if managementCode == "" { + return missingManagementCodeError + } + var req pkg.NotificationAddTokenRequest + if err := ctx.BodyParser(&req); err != nil { + return model.ErrorToBadRequestErrorResponse(err) + } + var res *model.Response + err := db.Transact( + rlog, func(tx *sqlx.Tx) error { + mtID := req.MomID + var expiresAt unixtime.UnixTime + var createdAt unixtime.UnixTime + if mtID.HashValid() { + mtInfo, err := tree.SingleTokenEntry(rlog, tx, mtID.MTID) + if err != nil { + return err + } + expiresAt = mtInfo.ExpiresAt + createdAt = mtInfo.CreatedAt + } else { + mt, errRes := auth.RequireValidMytoken(rlog, tx, &req.Mytoken, nil) + if errRes != nil { + res = errRes + return errors.New("rollback") + } + mtID = mtid.MOMID{MTID: mt.ID} + expiresAt = mt.ExpiresAt + createdAt = mt.IssuedAt + } + info, err := notificationsrepo.GetNotificationForManagementCode(rlog, tx, managementCode) + if err != nil { + return err + } + if info == nil { + res = managementCodeNotValidError + return errors.New("rollback") + } + if err = notificationsrepo.AddTokenToNotification( + rlog, tx, info.NotificationID, mtID, + req.IncludeChildren, + ); err != nil { + return err + } + if info.Classes.Contains(api.NotificationClassExpiration) { + if err = notificationsrepo.ScheduleExpirationNotifications( + rlog, tx, info.NotificationID, mtID.MTID, expiresAt, createdAt, + ); err != nil { + return err + } + } + return nil + }, + ) + if err != nil && res == nil { + res = model.ErrorToInternalServerErrorResponse(err) + } + if res == nil { + res = &model.Response{Status: fiber.StatusNoContent} + } + return res +} + +// HandleNotificationRemoveToken handles requests to remove a mytoken from a notification +func HandleNotificationRemoveToken(ctx *fiber.Ctx) *model.Response { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle notification remove token request") + managementCode := ctx.Params("code") + if managementCode == "" { + return missingManagementCodeError + } + var req pkg.NotificationRemoveTokenRequest + if err := ctx.BodyParser(&req); err != nil { + return model.ErrorToBadRequestErrorResponse(err) + } + var res *model.Response + err := db.Transact( + rlog, func(tx *sqlx.Tx) error { + mtID := req.MomID + if !mtID.HashValid() { + mt, errRes := auth.RequireValidMytoken(rlog, tx, &req.Mytoken, nil) + if errRes != nil { + res = errRes + return errors.New("rollback") + } + mtID = mtid.MOMID{MTID: mt.ID} + } + info, err := notificationsrepo.GetNotificationForManagementCode(rlog, tx, managementCode) + if err != nil { + return err + } + if info == nil { + res = managementCodeNotValidError + return errors.New("rollback") + } + if err = notificationsrepo.RemoveTokenFromNotification(rlog, tx, info.NotificationID, mtID); err != nil { + return err + } + return notificationsrepo.DeleteScheduledExpirationNotificationsForMT( + rlog, tx, info.NotificationID, mtID.MTID, + ) + }, + ) + if err != nil && res == nil { + res = model.ErrorToInternalServerErrorResponse(err) + } + if res == nil { + res = &model.Response{Status: fiber.StatusNoContent} + } + return res +} diff --git a/internal/endpoints/notification/pkg/notificationrequest.go b/internal/endpoints/notification/pkg/notificationrequest.go new file mode 100644 index 00000000..ed403792 --- /dev/null +++ b/internal/endpoints/notification/pkg/notificationrequest.go @@ -0,0 +1,42 @@ +package pkg + +import ( + "github.com/oidc-mytoken/api/v0" + + "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" + "github.com/oidc-mytoken/server/internal/mytoken/universalmytoken" +) + +// SubscribeNotificationRequest is type holding the request to create different notifications +type SubscribeNotificationRequest struct { + api.SubscribeNotificationRequest + Mytoken universalmytoken.UniversalMytoken `json:"mytoken" xml:"mytoken" form:"mytoken"` + MomID mtid.MOMID `json:"mom_id" xml:"mom_id" form:"mom_id"` +} + +// NotificationsListResponse is a type holding the response to a notification list request +type NotificationsListResponse struct { + api.NotificationsListResponse + pkg.OnlyTokenUpdateRes +} + +// NotificationAddTokenRequest is a request object for adding a mytoken to an existing notification +type NotificationAddTokenRequest struct { + api.NotificationAddTokenRequest + Mytoken universalmytoken.UniversalMytoken `json:"mytoken" xml:"mytoken" form:"mytoken"` + MomID mtid.MOMID `json:"mom_id" xml:"mom_id" form:"mom_id"` +} + +// NotificationRemoveTokenRequest is a request object for removing a mytoken from a notification +type NotificationRemoveTokenRequest struct { + api.NotificationRemoveTokenRequest + Mytoken universalmytoken.UniversalMytoken `json:"mytoken" xml:"mytoken" form:"mytoken"` + MomID mtid.MOMID `json:"mom_id" xml:"mom_id" form:"mom_id"` +} + +// NotificationsCreateResponse is a type holding the response to a notification creation request +type NotificationsCreateResponse struct { + api.NotificationsCreateResponse + pkg.OnlyTokenUpdateRes +} diff --git a/internal/endpoints/profiles/profileEndpoint.go b/internal/endpoints/profiles/profileEndpoint.go index 63470451..90c3de50 100644 --- a/internal/endpoints/profiles/profileEndpoint.go +++ b/internal/endpoints/profiles/profileEndpoint.go @@ -35,7 +35,7 @@ func handleGetProfiles( ctx *fiber.Ctx, dbReader func(log.Ext1FieldLogger, *sqlx.Tx, string) ([]api.Profile, error), ) error { rlog := logger.GetRequestLogger(ctx) - group := ctx.Params("group", "_") + group := ctxutils.Params(ctx, "group", "_") data, err := dbReader(rlog, nil, group) if err != nil { rlog.Errorf("%s", errorfmt.Full(err)) @@ -52,7 +52,7 @@ func handleUpsertProfiles( returnStatus int, ) error { rlog := logger.GetRequestLogger(ctx) - group := ctx.Params("group", "_") + group := ctxutils.Params(ctx, "group", "_") var req api.Profile err := errors.WithStack(json.Unmarshal(ctx.Body(), &req)) @@ -73,7 +73,7 @@ func handleDeleteProfiles( ctx *fiber.Ctx, dbDo func(log.Ext1FieldLogger, *sqlx.Tx, string, uuid.UUID) error, returnStatus int, ) error { rlog := logger.GetRequestLogger(ctx) - group := ctx.Params("group", "_") + group := ctxutils.Params(ctx, "group", "_") id, err := ctxutils.GetID(ctx) if err != nil { return model.ErrorToBadRequestErrorResponse(err).Send(ctx) diff --git a/internal/endpoints/redirect/redirectEndpoint.go b/internal/endpoints/redirect/redirectEndpoint.go index da977cb3..9ffd4f8d 100644 --- a/internal/endpoints/redirect/redirectEndpoint.go +++ b/internal/endpoints/redirect/redirectEndpoint.go @@ -1,8 +1,6 @@ package redirect import ( - "net/http" - "github.com/gofiber/fiber/v2" "github.com/jmoiron/sqlx" "github.com/oidc-mytoken/api/v0" @@ -40,25 +38,20 @@ func HandleOIDCRedirect(ctx *fiber.Ctx) error { } } oidcErrorDescription := ctx.Query("error_description") - return ctx.Status(httpstatus.StatusOIDPError).Render( - "sites/error", map[string]interface{}{ - "empty-navbar": true, - "error-heading": "OIDC error", - "msg": pkgModel.OIDCError(oidcError, oidcErrorDescription).CombinedMessage(), - }, "layouts/main", + return ctxutils.RenderErrorPage( + ctx, httpstatus.StatusOIDPError, pkgModel.OIDCError( + oidcError, + oidcErrorDescription, + ).CombinedMessage(), "OIDC error", ) } code := ctx.Query("code") - res := authcode.CodeExchange(rlog, oState, code, *ctxutils.ClientMetaData(ctx)) + res, additionErrHTML := authcode.CodeExchange(rlog, oState, code, *ctxutils.ClientMetaData(ctx)) if fasthttp.StatusCodeIsRedirect(res.Status) { return res.Send(ctx) } - return ctx.Status(res.Status).Render( - "sites/error", map[string]interface{}{ - "empty-navbar": true, - "error-heading": http.StatusText(res.Status), - "msg": res.Response.(api.Error).CombinedMessage(), - }, "layouts/main", + return ctxutils.RenderExtendedErrorPage( + ctx, res.Status, res.Response.(api.Error).CombinedMessage(), "", additionErrHTML, ) } diff --git a/internal/endpoints/revocation/revocationEndpoint.go b/internal/endpoints/revocation/revocationEndpoint.go index 9bd21e5e..6996188b 100644 --- a/internal/endpoints/revocation/revocationEndpoint.go +++ b/internal/endpoints/revocation/revocationEndpoint.go @@ -15,25 +15,29 @@ import ( "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/db" helper "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/mytokenrepohelper" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/shorttokenrepo" "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/transfercoderepo" + "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" "github.com/oidc-mytoken/server/internal/model" "github.com/oidc-mytoken/server/internal/mytoken" eventService "github.com/oidc-mytoken/server/internal/mytoken/event" - event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" + pkg2 "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" mytokenPkg "github.com/oidc-mytoken/server/internal/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/rotation" "github.com/oidc-mytoken/server/internal/mytoken/universalmytoken" + "github.com/oidc-mytoken/server/internal/utils/cookies" "github.com/oidc-mytoken/server/internal/utils/ctxutils" "github.com/oidc-mytoken/server/internal/utils/errorfmt" "github.com/oidc-mytoken/server/internal/utils/logger" ) // HandleRevoke handles requests to the revocation endpoint -func HandleRevoke(ctx *fiber.Ctx) error { +func HandleRevoke(ctx *fiber.Ctx) *model.Response { rlog := logger.GetRequestLogger(ctx) rlog.Debug("Handle revocation request") req := api.RevocationRequest{} if err := ctx.BodyParser(&req); err != nil { - return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + return model.ErrorToBadRequestErrorResponse(err) } rlog.WithField("parsed request", fmt.Sprintf("%+v", req)).WithField( "body", string(ctx.Body()), @@ -42,28 +46,62 @@ func HandleRevoke(ctx *fiber.Ctx) error { if req.Token == "" { req.Token = ctx.Cookies("mytoken") if req.Token == "" { - return model.Response{ - Status: fiber.StatusBadRequest, - Response: model.BadRequestError("no token given"), - }.Send(ctx) + return model.BadRequestErrorResponse("no token given") } if req.MOMID == "" { clearCookie = true } } if req.MOMID != "" { - errRes := revokeByID(rlog, req, ctxutils.ClientMetaData(ctx)) - if errRes != nil { - return errRes.Send(ctx) + var res *model.Response + _ = db.Transact( + rlog, func(tx *sqlx.Tx) error { + metadata := ctxutils.ClientMetaData(ctx) + token, err := universalmytoken.Parse(rlog, req.Token) + if err != nil { + res = model.ErrorToBadRequestErrorResponse(err) + return err + } + authToken, err := mytokenPkg.ParseJWT(token.JWT) + if err != nil { + res = model.ErrorToBadRequestErrorResponse(err) + return err + } + errRes := revokeByID(rlog, tx, req, authToken, metadata) + if errRes != nil { + res = errRes + return errors.New("rollback") + } + tokenUpdate, err := rotation.RotateMytokenAfterOtherForResponse( + rlog, tx, token.JWT, authToken, *metadata, token.OriginalTokenType, + ) + if err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + if tokenUpdate != nil { + res = &model.Response{ + Status: fiber.StatusOK, + Response: pkg.OnlyTokenUpdateRes{ + TokenUpdate: tokenUpdate, + }, + Cookies: []*fiber.Cookie{cookies.MytokenCookie(tokenUpdate.Mytoken)}, + } + } + return nil + }, + ) + if res != nil { + return res } - return ctx.SendStatus(fiber.StatusNoContent) + return &model.Response{Status: fiber.StatusNoContent} } errRes := revokeAnyToken(rlog, nil, req.Token, req.OIDCIssuer, req.Recursive) if errRes != nil { - return errRes.Send(ctx) + return errRes } if clearCookie { - return model.Response{ + return &model.Response{ Status: fiber.StatusNoContent, Cookies: []*fiber.Cookie{ { @@ -76,66 +114,71 @@ func HandleRevoke(ctx *fiber.Ctx) error { SameSite: "Strict", }, }, - }.Send(ctx) + } } - return ctx.SendStatus(fiber.StatusNoContent) + return &model.Response{Status: fiber.StatusNoContent} } -func revokeByID(rlog log.Ext1FieldLogger, req api.RevocationRequest, clientMetadata *api.ClientMetaData) *model. - Response { - token, err := universalmytoken.Parse(rlog, req.Token) - if err != nil { - return model.ErrorToBadRequestErrorResponse(err) - } - authToken, err := mytokenPkg.ParseJWT(token.JWT) - if err != nil { - return model.ErrorToBadRequestErrorResponse(err) - } - isParent, err := helper.MOMIDHasParent(rlog, nil, req.MOMID, authToken.ID) - if err != nil { - return model.ErrorToInternalServerErrorResponse(err) - } - if !isParent && !authToken.Capabilities.Has(api.CapabilityRevokeAnyToken) { - return &model.Response{ - Status: fiber.StatusForbidden, - Response: api.Error{ - Error: api.ErrorStrInsufficientCapabilities, - ErrorDescription: fmt.Sprintf( - "The provided token is neither a parent of the token to be revoked"+ - " nor does it have the '%s' capability", api.CapabilityRevokeAnyToken.Name, - ), - }, - } - } - same, err := helper.CheckMytokensAreForSameUser(rlog, nil, req.MOMID, authToken.ID) - if err != nil { - return model.ErrorToInternalServerErrorResponse(err) - } - if !same { - return &model.Response{ - Status: fiber.StatusForbidden, - Response: api.Error{ - Error: api.ErrorStrInvalidGrant, - ErrorDescription: "The provided token cannot be used to revoke this mom_id", - }, - } - } - if err = db.Transact( - rlog, func(tx *sqlx.Tx) error { +func revokeByID( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req api.RevocationRequest, + authToken *mytokenPkg.Mytoken, + clientMetadata *api.ClientMetaData, +) (errRes *model.Response) { + rollback := errors.New("rollback") + _ = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + isParent, err := helper.MOMIDHasParent(rlog, nil, req.MOMID, authToken.ID) + if err != nil { + errRes = model.ErrorToInternalServerErrorResponse(err) + return err + } + if !isParent && !authToken.Capabilities.Has(api.CapabilityRevokeAnyToken) { + errRes = &model.Response{ + Status: fiber.StatusForbidden, + Response: api.Error{ + Error: api.ErrorStrInsufficientCapabilities, + ErrorDescription: fmt.Sprintf( + "The provided token is neither a parent of the token to be revoked"+ + " nor does it have the '%s' capability", api.CapabilityRevokeAnyToken.Name, + ), + }, + } + return rollback + } + same, err := helper.CheckMytokensAreForSameUser(rlog, nil, req.MOMID, authToken.ID) + if err != nil { + errRes = model.ErrorToInternalServerErrorResponse(err) + return err + } + if !same { + errRes = &model.Response{ + Status: fiber.StatusForbidden, + Response: api.Error{ + Error: api.ErrorStrInvalidGrant, + ErrorDescription: "The provided token cannot be used to revoke this mom_id", + }, + } + return rollback + } if err = helper.RevokeMT(rlog, tx, req.MOMID, req.Recursive); err != nil { + errRes = model.ErrorToInternalServerErrorResponse(err) + return err + } + if err = eventService.LogEvent( + rlog, tx, pkg2.MTEvent{ + Event: api.EventRevokedOtherToken, + MTID: authToken.ID, + Comment: fmt.Sprintf("mom_id: %s", req.MOMID), + ClientMetaData: *clientMetadata, + }, + ); err != nil { + errRes = model.ErrorToInternalServerErrorResponse(err) return err } - return eventService.LogEvent( - rlog, tx, eventService.MTEvent{ - Event: event.FromNumber(event.RevokedOtherToken, fmt.Sprintf("mom_id: %s", req.MOMID)), - MTID: authToken.ID, - }, *clientMetadata, - ) + return nil }, - ); err != nil { - return model.ErrorToInternalServerErrorResponse(err) - } - return nil + ) + return } func revokeAnyToken( @@ -146,7 +189,7 @@ func revokeAnyToken( } else if len(token) < api.MinShortTokenLen { // Transfer Code return revokeTransferCode(rlog, tx, token, issuer) } else { // Short Token - shortToken := transfercoderepo.ParseShortToken(token) + shortToken := shorttokenrepo.ParseShortToken(token) var valid bool if err := db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { @@ -175,10 +218,7 @@ func revokeMytoken(rlog log.Ext1FieldLogger, tx *sqlx.Tx, jwt, issuer string, re return nil } if issuer != "" && mt.OIDCIssuer != issuer { - return &model.Response{ - Status: fiber.StatusBadRequest, - Response: model.BadRequestError("token not for specified issuer"), - } + return model.BadRequestErrorResponse("token not for specified issuer") } return mytoken.RevokeMytoken(rlog, tx, mt.ID, jwt, recursive, mt.OIDCIssuer) } diff --git a/internal/endpoints/settings/email/email.go b/internal/endpoints/settings/email/email.go new file mode 100644 index 00000000..27a5a827 --- /dev/null +++ b/internal/endpoints/settings/email/email.go @@ -0,0 +1,190 @@ +package email + +import ( + "github.com/gofiber/fiber/v2" + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/db/dbrepo/userrepo" + "github.com/oidc-mytoken/server/internal/endpoints/actions" + "github.com/oidc-mytoken/server/internal/endpoints/settings" + my "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/model" + eventService "github.com/oidc-mytoken/server/internal/mytoken/event" + "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" + mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" + "github.com/oidc-mytoken/server/internal/mytoken/rotation" + "github.com/oidc-mytoken/server/internal/mytoken/universalmytoken" + notifier "github.com/oidc-mytoken/server/internal/notifier/client" + "github.com/oidc-mytoken/server/internal/notifier/server/mailing/mailtemplates" + "github.com/oidc-mytoken/server/internal/utils/auth" + "github.com/oidc-mytoken/server/internal/utils/cookies" + "github.com/oidc-mytoken/server/internal/utils/ctxutils" + "github.com/oidc-mytoken/server/internal/utils/logger" +) + +// MailSettingsInfoResponse is a type for the response for listing mail settings +type MailSettingsInfoResponse struct { + api.MailSettingsInfoResponse + TokenUpdate *my.MytokenResponse `json:"token_update,omitempty"` +} + +// SetTokenUpdate implements the pkg.TokenUpdatableResponse interface +func (res *MailSettingsInfoResponse) SetTokenUpdate(tokenUpdate *my.MytokenResponse) { + res.TokenUpdate = tokenUpdate +} + +// HandleGet handles GET requests to the email settings endpoint +func HandleGet(ctx *fiber.Ctx) *model.Response { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle get email info request") + var reqMytoken universalmytoken.UniversalMytoken + + return settings.HandleSettingsHelper( + ctx, nil, &reqMytoken, api.CapabilityEmailRead, &api.EventEmailSettingsListed, "", fiber.StatusOK, + func(tx *sqlx.Tx, mt *mytoken.Mytoken) (my.TokenUpdatableResponse, *model.Response) { + info, err := userrepo.GetMail(rlog, tx, mt.ID) + if err != nil { + return nil, model.ErrorToInternalServerErrorResponse(err) + } + return &MailSettingsInfoResponse{ + MailSettingsInfoResponse: api.MailSettingsInfoResponse{ + EmailAddress: info.Mail.String, + EmailVerified: info.MailVerified, + PreferHTMLMail: info.PreferHTMLMail, + }, + }, nil + }, false, + ) +} + +func changeEmailAddress( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, email string, + clientMetaData *api.ClientMetaData, +) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + if err := userrepo.ChangeEmail(rlog, tx, mtID, email); err != nil { + return err + } + verificationURL, err := actions.CreateVerifyEmail(rlog, tx, mtID) + if err != nil { + return err + } + mailInfo, err := userrepo.GetMail(rlog, tx, mtID) + if err != nil { + return err + } + if err = eventService.LogEvent( + rlog, tx, pkg.MTEvent{ + Event: api.EventEmailChanged, + MTID: mtID, + Comment: email, + ClientMetaData: *clientMetaData, + }, + ); err != nil { + return err + } + notifier.SendTemplateEmail( + email, mailtemplates.SubjectVerifyMail, mailInfo.PreferHTMLMail, + mailtemplates.TemplateVerifyMail, map[string]any{ + "issuer": config.Get().IssuerURL, + "link": verificationURL, + }, + ) + return nil + }, + ) +} + +func changePreferredMimeType( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, preferHTML bool, + clientMetaData *api.ClientMetaData, +) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + + if err := userrepo.ChangePreferredMailType(rlog, tx, mtID, preferHTML); err != nil { + return err + } + eventComment := "to plain text" + if preferHTML { + eventComment = "to html" + } + if err := eventService.LogEvent( + rlog, tx, pkg.MTEvent{ + Event: api.EventEmailMimetypeChanged, + Comment: eventComment, + MTID: mtID, + ClientMetaData: *clientMetaData, + }, + ); err != nil { + return err + } + return nil + }, + ) +} + +// HandlePut handles PUT requests to the email settings endpoint, i.e. it updates email settings +func HandlePut(ctx *fiber.Ctx) *model.Response { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle update email settings request") + var req api.UpdateMailSettingsRequest + if err := ctx.BodyParser(&req); err != nil { + return model.ErrorToBadRequestErrorResponse(err) + } + if req.PreferHTMLMail == nil && req.EmailAddress == "" { + return model.BadRequestErrorResponse("no request parameter given") + } + var reqMytoken universalmytoken.UniversalMytoken + mt, errRes := auth.RequireValidMytoken(rlog, nil, &reqMytoken, ctx) + if errRes != nil { + return errRes + } + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, nil, mt, ctxutils.ClientMetaData(ctx), api.CapabilityEmail, + ) + if errRes != nil { + return errRes + } + var tokenUpdate *my.MytokenResponse + clientMetaData := ctxutils.ClientMetaData(ctx) + if err := db.Transact( + rlog, func(tx *sqlx.Tx) error { + if req.PreferHTMLMail != nil { + if err := changePreferredMimeType(rlog, tx, mt.ID, *req.PreferHTMLMail, clientMetaData); err != nil { + return err + } + } + if req.EmailAddress != "" { + if err := changeEmailAddress(rlog, tx, mt.ID, req.EmailAddress, clientMetaData); err != nil { + return err + } + } + if err := usedRestriction.UsedOther(rlog, tx, mt.ID); err != nil { + return err + } + + tu, err := rotation.RotateMytokenAfterOtherForResponse( + rlog, tx, reqMytoken.JWT, mt, *clientMetaData, reqMytoken.OriginalTokenType, + ) + tokenUpdate = tu + return err + }, + ); err != nil { + return model.ErrorToInternalServerErrorResponse(err) + } + if tokenUpdate != nil { + return &model.Response{ + Status: fiber.StatusOK, + Response: my.OnlyTokenUpdateRes{TokenUpdate: tokenUpdate}, + Cookies: []*fiber.Cookie{cookies.MytokenCookie(tokenUpdate.Mytoken)}, + } + } + return &model.Response{Status: fiber.StatusNoContent} +} diff --git a/internal/endpoints/settings/grants/grants.go b/internal/endpoints/settings/grants/grants.go index d16c8523..16279e7f 100644 --- a/internal/endpoints/settings/grants/grants.go +++ b/internal/endpoints/settings/grants/grants.go @@ -11,7 +11,6 @@ import ( request "github.com/oidc-mytoken/server/internal/endpoints/settings/grants/pkg" my "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" "github.com/oidc-mytoken/server/internal/model" - event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" "github.com/oidc-mytoken/server/internal/mytoken/universalmytoken" @@ -20,13 +19,13 @@ import ( // HandleListGrants handles GET requests to the grants endpoints and returns a list of enabled/disabled grant types for // the user -func HandleListGrants(ctx *fiber.Ctx) error { +func HandleListGrants(ctx *fiber.Ctx) *model.Response { rlog := logger.GetRequestLogger(ctx) rlog.Debug("Handle get grant type request") var reqMytoken universalmytoken.UniversalMytoken return settings.HandleSettingsHelper( - ctx, &reqMytoken, api.CapabilityGrantsRead, event.FromNumber(event.GrantsListed, ""), fiber.StatusOK, + ctx, nil, &reqMytoken, api.CapabilityGrantsRead, &api.EventGrantsListed, "", fiber.StatusOK, func(tx *sqlx.Tx, mt *mytoken.Mytoken) (my.TokenUpdatableResponse, *model.Response) { grants, err := grantrepo.Get(rlog, tx, mt.ID) if err != nil { @@ -42,38 +41,35 @@ func HandleListGrants(ctx *fiber.Ctx) error { } // HandleEnableGrant handles requests to enable a grant type -func HandleEnableGrant(ctx *fiber.Ctx) error { +func HandleEnableGrant(ctx *fiber.Ctx) *model.Response { rlog := logger.GetRequestLogger(ctx) rlog.Debug("Handle enable grant type request") - return handleEditGrant(rlog, ctx, grantrepo.Enable, event.GrantEnabled, fiber.StatusCreated) + return handleEditGrant(rlog, ctx, grantrepo.Enable, api.EventGrantEnabled, fiber.StatusCreated) } // HandleDisableGrant handles requests to disable a grant type -func HandleDisableGrant(ctx *fiber.Ctx) error { +func HandleDisableGrant(ctx *fiber.Ctx) *model.Response { rlog := logger.GetRequestLogger(ctx) rlog.Debug("Handle disable grant type request") - return handleEditGrant(rlog, ctx, grantrepo.Disable, event.GrantDisabled, fiber.StatusNoContent) + return handleEditGrant(rlog, ctx, grantrepo.Disable, api.EventGrantDisabled, fiber.StatusNoContent) } func handleEditGrant( rlog log.Ext1FieldLogger, ctx *fiber.Ctx, dbCallBack func(rlog log.Ext1FieldLogger, tx *sqlx.Tx, myid mtid.MTID, grant model.GrantType) error, - evt, okStatus int, -) error { + evt api.Event, okStatus int, +) *model.Response { req := request.GrantTypeRequest{GrantType: -1} if err := ctx.BodyParser(&req); err != nil { - return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + return model.ErrorToBadRequestErrorResponse(err) } if !req.GrantType.Valid() { - return model.Response{ - Status: fiber.StatusBadRequest, - Response: model.BadRequestError("no valid 'grant_type' found"), - }.Send(ctx) + return model.BadRequestErrorResponse("no valid 'grant_type' found") } rlog.Trace("Parsed grant type request") return settings.HandleSettingsHelper( - ctx, &req.Mytoken, api.CapabilityGrants, event.FromNumber(evt, req.GrantType.String()), okStatus, + ctx, nil, &req.Mytoken, api.CapabilityGrants, &evt, req.GrantType.String(), okStatus, func(tx *sqlx.Tx, mt *mytoken.Mytoken) (my.TokenUpdatableResponse, *model.Response) { if err := dbCallBack(rlog, tx, mt.ID, req.GrantType); err != nil { return nil, model.ErrorToInternalServerErrorResponse(err) diff --git a/internal/endpoints/settings/grants/ssh/ssh.go b/internal/endpoints/settings/grants/ssh/ssh.go index 88f08e7c..28860f24 100644 --- a/internal/endpoints/settings/grants/ssh/ssh.go +++ b/internal/endpoints/settings/grants/ssh/ssh.go @@ -21,7 +21,6 @@ import ( "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/polling" "github.com/oidc-mytoken/server/internal/model" "github.com/oidc-mytoken/server/internal/model/profiled" - event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" "github.com/oidc-mytoken/server/internal/mytoken/universalmytoken" "github.com/oidc-mytoken/server/internal/oidc/authcode" @@ -34,14 +33,15 @@ import ( ) // HandleGetSSHInfo handles requests to return information about a user's ssh keys -func HandleGetSSHInfo(ctx *fiber.Ctx) error { +func HandleGetSSHInfo(ctx *fiber.Ctx) *model.Response { rlog := logger.GetRequestLogger(ctx) rlog.Debug("Handle get ssh info request") var reqMytoken universalmytoken.UniversalMytoken return settings.HandleSettingsHelper( - ctx, &reqMytoken, api.CapabilitySSHGrantRead, event.FromNumber(event.SSHKeyListed, ""), fiber.StatusOK, + ctx, nil, &reqMytoken, api.CapabilitySSHGrantRead, &api.EventSSHKeyListed, "", fiber.StatusOK, func(tx *sqlx.Tx, mt *mytoken.Mytoken) (my.TokenUpdatableResponse, *model.Response) { info, err := sshrepo.GetAllSSHInfo(rlog, tx, mt.ID) + _, err = db.ParseError(err) if err != nil { rlog.Errorf("%s", errorfmt.Full(err)) return nil, model.ErrorToInternalServerErrorResponse(err) @@ -62,33 +62,29 @@ func HandleGetSSHInfo(ctx *fiber.Ctx) error { } // HandleDeleteSSHKey handles requests to delete a user's ssh public key -func HandleDeleteSSHKey(ctx *fiber.Ctx) error { +func HandleDeleteSSHKey(ctx *fiber.Ctx) *model.Response { rlog := logger.GetRequestLogger(ctx) rlog.Debug("Handle delete ssh key request") req := request.SSHKeyDeleteRequest{} if err := ctx.BodyParser(&req); err != nil { - return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + return model.ErrorToBadRequestErrorResponse(err) } rlog.Trace("Parsed delete ssh key request") if req.SSHKeyFingerprint == "" { if req.SSHKey == "" { - return model.Response{ - Status: fiber.StatusBadRequest, - Response: model.BadRequestError("One of the required parameters 'ssh_key' or 'ssh_key_hash' must be given"), - }.Send(ctx) + return model.BadRequestErrorResponse( + "One of the required parameters 'ssh_key' or 'ssh_key_hash' must be given", + ) } sshKey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(req.SSHKey)) if err != nil { - return model.Response{ - Status: fiber.StatusBadRequest, - Response: model.BadRequestError("could not parse ssh public key"), - }.Send(ctx) + return model.BadRequestErrorResponse("could not parse ssh public key") } req.SSHKeyFingerprint = gossh.FingerprintSHA256(sshKey) } return settings.HandleSettingsHelper( - ctx, &req.Mytoken, api.CapabilitySSHGrant, nil, fiber.StatusNoContent, + ctx, nil, &req.Mytoken, api.CapabilitySSHGrant, nil, "", fiber.StatusNoContent, func(tx *sqlx.Tx, mt *mytoken.Mytoken) (my.TokenUpdatableResponse, *model.Response) { if err := sshrepo.Delete(rlog, tx, mt.ID, req.SSHKeyFingerprint); err != nil { rlog.Errorf("%s", errorfmt.Full(err)) @@ -101,10 +97,10 @@ func HandleDeleteSSHKey(ctx *fiber.Ctx) error { // HandlePost handles POST requests to the ssh grant endpoint, this includes the initial request to add an ssh public // key as well as the following polling requests. -func HandlePost(ctx *fiber.Ctx) error { +func HandlePost(ctx *fiber.Ctx) *model.Response { grantType, err := ctxutils.GetGrantType(ctx) if err != nil { - return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + return model.ErrorToBadRequestErrorResponse(err) } switch grantType { case model.GrantTypeMytoken: @@ -112,35 +108,28 @@ func HandlePost(ctx *fiber.Ctx) error { case model.GrantTypePollingCode: return handlePollingCode(ctx) default: - res := model.Response{ + return &model.Response{ Status: fiber.StatusBadRequest, Response: api.ErrorUnsupportedGrantType, } - return res.Send(ctx) } } // handleAddSSHKey handles the initial request to add an ssh public key -func handleAddSSHKey(ctx *fiber.Ctx) error { +func handleAddSSHKey(ctx *fiber.Ctx) *model.Response { rlog := logger.GetRequestLogger(ctx) rlog.Debug("Handle add ssh key request") req := request.SSHKeyAddRequest{} if err := ctx.BodyParser(&req); err != nil { - return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + return model.ErrorToBadRequestErrorResponse(err) } rlog.Trace("Parsed add ssh key request") if req.SSHKey == "" { - return model.Response{ - Status: fiber.StatusBadRequest, - Response: model.BadRequestError("required parameter 'ssh_key' is missing"), - }.Send(ctx) + return model.BadRequestErrorResponse("required parameter 'ssh_key' is missing") } sshKey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(req.SSHKey)) if err != nil { - return model.Response{ - Status: fiber.StatusBadRequest, - Response: model.BadRequestError("could not parse ssh public key"), - }.Send(ctx) + return model.BadRequestErrorResponse("could not parse ssh public key") } sshKeyFP := gossh.FingerprintSHA256(sshKey) if len(req.Capabilities) == 0 { @@ -148,7 +137,7 @@ func handleAddSSHKey(ctx *fiber.Ctx) error { } return settings.HandleSettingsHelper( - ctx, &req.Mytoken, api.CapabilitySSHGrant, event.FromNumber(event.SSHKeyAdded, ""), fiber.StatusOK, + ctx, nil, &req.Mytoken, api.CapabilitySSHGrant, &api.EventSSHKeyAdded, "", fiber.StatusOK, func(tx *sqlx.Tx, mt *mytoken.Mytoken) (my.TokenUpdatableResponse, *model.Response) { return handleAddSSHSettingsCallback(rlog, ctx, &req, sshKeyFP, tx, mt) }, false, @@ -249,28 +238,28 @@ func extractSSHKeyNameFromMTName(mtName string) string { } // handlePollingCode handles the polling requests to finish adding an ssh public key -func handlePollingCode(ctx *fiber.Ctx) error { +func handlePollingCode(ctx *fiber.Ctx) *model.Response { rlog := logger.GetRequestLogger(ctx) req := my.NewPollingCodeRequest() if err := json.Unmarshal(ctx.Body(), &req); err != nil { - return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + return model.ErrorToBadRequestErrorResponse(err) } clientMetaData := ctxutils.ClientMetaData(ctx) mt, token, status, errRes := polling.CheckPollingCodeReq(rlog, req, *clientMetaData, true) if errRes != nil { - return errRes.Send(ctx) + return errRes } user := utils.RandASCIIString(16) userHash := hashutils.SHA3_512Str([]byte(user)) encryptedMT, err := cryptutils.AES256Encrypt(token, user) if err != nil { rlog.Errorf("%s", errorfmt.Full(err)) - return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + return model.ErrorToInternalServerErrorResponse(err) } mtName, err := mytokenrepohelper.GetMTName(rlog, nil, mt.ID) if err != nil { rlog.Errorf("%s", errorfmt.Full(err)) - return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + return model.ErrorToInternalServerErrorResponse(err) } name := extractSSHKeyNameFromMTName(mtName.String) data := sshrepo.SSHInfoIn{ @@ -282,13 +271,13 @@ func handlePollingCode(ctx *fiber.Ctx) error { } if err = sshrepo.Insert(rlog, nil, data); err != nil { rlog.Errorf("%s", errorfmt.Full(err)) - return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + return model.ErrorToInternalServerErrorResponse(err) } - return model.Response{ + return &model.Response{ Status: fiber.StatusOK, Response: api.SSHKeyAddFinalResponse{ SSHUser: user, SSHHostConfig: ssh.CreateHostConfigEntry(user, name), }, - }.Send(ctx) + } } diff --git a/internal/endpoints/settings/settings.go b/internal/endpoints/settings/settings.go index c2852c2e..6a5351c1 100644 --- a/internal/endpoints/settings/settings.go +++ b/internal/endpoints/settings/settings.go @@ -1,28 +1,24 @@ package settings import ( - "fmt" - "github.com/gofiber/fiber/v2" "github.com/jmoiron/sqlx" "github.com/oidc-mytoken/api/v0" "github.com/oidc-mytoken/utils/utils" + "github.com/pkg/errors" "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/db" my "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" serverModel "github.com/oidc-mytoken/server/internal/model" - eventService "github.com/oidc-mytoken/server/internal/mytoken/event" - event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" - "github.com/oidc-mytoken/server/internal/mytoken/rotation" "github.com/oidc-mytoken/server/internal/mytoken/universalmytoken" "github.com/oidc-mytoken/server/internal/server/paths" "github.com/oidc-mytoken/server/internal/utils/auth" - "github.com/oidc-mytoken/server/internal/utils/cookies" "github.com/oidc-mytoken/server/internal/utils/ctxutils" "github.com/oidc-mytoken/server/internal/utils/errorfmt" "github.com/oidc-mytoken/server/internal/utils/logger" + "github.com/oidc-mytoken/server/internal/utils/mytokenutils" ) // InitSettings initializes the settings metadata @@ -31,104 +27,78 @@ func InitSettings() { settingsMetadata.GrantTypeEndpoint = utils.CombineURLPath( config.Get().IssuerURL, apiPaths.UserSettingEndpoint, "grants", ) + settingsMetadata.EmailEndpoint = utils.CombineURLPath( + config.Get().IssuerURL, apiPaths.UserSettingEndpoint, "email", + ) } -var settingsMetadata = api.SettingsMetaData{ - GrantTypeEndpoint: "grants", -} +var settingsMetadata = api.SettingsMetaData{} // HandleSettings handles Metadata requests to the settings endpoint -func HandleSettings(ctx *fiber.Ctx) error { - res := serverModel.Response{ +func HandleSettings(*fiber.Ctx) *serverModel.Response { + return &serverModel.Response{ Status: fiber.StatusOK, Response: settingsMetadata, } - return res.Send(ctx) } // HandleSettingsHelper is a helper wrapper function that handles various settings request with the help of a callback func HandleSettingsHelper( ctx *fiber.Ctx, + tx *sqlx.Tx, reqMytoken *universalmytoken.UniversalMytoken, requiredCapability api.Capability, - logEvent *event.Event, + logEvent *api.Event, + eventComment string, okStatus int, callback func(tx *sqlx.Tx, mt *mytoken.Mytoken) (my.TokenUpdatableResponse, *serverModel.Response), tokenGoneAfterCallback bool, -) error { +) *serverModel.Response { rlog := logger.GetRequestLogger(ctx) - mt, errRes := auth.RequireValidMytoken(rlog, nil, reqMytoken, ctx) - if errRes != nil { - return errRes.Send(ctx) - } - usedRestriction, errRes := auth.CheckCapabilityAndRestriction( - rlog, nil, mt, ctx.IP(), nil, nil, requiredCapability, - ) - if errRes != nil { - return errRes.Send(ctx) - } - var tokenUpdate *my.MytokenResponse - var rsp my.TokenUpdatableResponse - if err := db.Transact( - rlog, func(tx *sqlx.Tx) (err error) { - rsp, errRes = callback(tx, mt) + + var res *serverModel.Response + if err := db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + mt, errRes := auth.RequireValidMytoken(rlog, tx, reqMytoken, ctx) if errRes != nil { - return fmt.Errorf("dummy") + res = errRes + return errors.New("rollback") } - if tokenGoneAfterCallback { - return + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, tx, mt, ctxutils.ClientMetaData(ctx), requiredCapability, + ) + if errRes != nil { + res = errRes + return errors.New("rollback") } - clientMetaData := ctxutils.ClientMetaData(ctx) - if logEvent != nil { - if err = eventService.LogEvent( - rlog, tx, eventService.MTEvent{ - Event: logEvent, - MTID: mt.ID, - }, *clientMetaData, - ); err != nil { - return - } + rsp, errRes := callback(tx, mt) + if errRes != nil { + res = errRes + return errors.New("rollback") } - if usedRestriction != nil { - if err = usedRestriction.UsedOther(rlog, tx, mt.ID); err != nil { - return - } + if okStatus == 0 { + okStatus = fiber.StatusOK } - tokenUpdate, err = rotation.RotateMytokenAfterOtherForResponse( - rlog, tx, reqMytoken.JWT, mt, *clientMetaData, reqMytoken.OriginalTokenType, + res = &serverModel.Response{ + Status: okStatus, + Response: rsp, + } + if tokenGoneAfterCallback { + return nil + } + var rollback bool + res, rollback = mytokenutils.DoAfterRequestThingsOther( + rlog, tx, res, mt, *ctxutils.ClientMetaData(ctx), + *logEvent, eventComment, usedRestriction, reqMytoken.JWT, reqMytoken.OriginalTokenType, ) - return + if rollback { + return errors.New("rollback") + } + return nil }, - ); err != nil { - if errRes != nil { - return errRes.Send(ctx) - } + ); err != nil && res == nil { rlog.Errorf("%s", errorfmt.Full(err)) - return serverModel.ErrorToInternalServerErrorResponse(err).Send(ctx) - } - - var cake []*fiber.Cookie - if tokenUpdate != nil { - if rsp == nil { - rsp = &onlyTokenUpdateRes{} - } - rsp.SetTokenUpdate(tokenUpdate) - cake = []*fiber.Cookie{cookies.MytokenCookie(tokenUpdate.Mytoken)} - okStatus = fiber.StatusOK + res = serverModel.ErrorToInternalServerErrorResponse(err) } - return serverModel.Response{ - Status: okStatus, - Response: rsp, - Cookies: cake, - }.Send(ctx) -} - -type onlyTokenUpdateRes struct { - api.OnlyTokenUpdateResponse - TokenUpdate *my.MytokenResponse `json:"token_update,omitempty"` -} - -// SetTokenUpdate implements the pkg.TokenUpdatableResponse interface -func (res *onlyTokenUpdateRes) SetTokenUpdate(tokenUpdate *my.MytokenResponse) { - res.TokenUpdate = tokenUpdate + return res } diff --git a/internal/endpoints/token/access/accessTokenEndpoint.go b/internal/endpoints/token/access/accessTokenEndpoint.go index 8290c392..538ee6ee 100644 --- a/internal/endpoints/token/access/accessTokenEndpoint.go +++ b/internal/endpoints/token/access/accessTokenEndpoint.go @@ -7,6 +7,7 @@ import ( "github.com/jmoiron/sqlx" "github.com/oidc-mytoken/api/v0" "github.com/oidc-mytoken/utils/utils/jwtutils" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/oidc-mytoken/server/internal/db" @@ -16,10 +17,11 @@ import ( response "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" "github.com/oidc-mytoken/server/internal/model" eventService "github.com/oidc-mytoken/server/internal/mytoken/event" - event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" "github.com/oidc-mytoken/server/internal/mytoken/restrictions" "github.com/oidc-mytoken/server/internal/mytoken/rotation" + "github.com/oidc-mytoken/server/internal/oidc/oidcreqres" "github.com/oidc-mytoken/server/internal/oidc/refresh" "github.com/oidc-mytoken/server/internal/utils" "github.com/oidc-mytoken/server/internal/utils/auth" @@ -30,12 +32,12 @@ import ( ) // HandleAccessTokenEndpoint handles request on the access token endpoint -func HandleAccessTokenEndpoint(ctx *fiber.Ctx) error { +func HandleAccessTokenEndpoint(ctx *fiber.Ctx) *model.Response { rlog := logger.GetRequestLogger(ctx) rlog.Debug("Handle access token request") req := request.NewAccessTokenRequest() if err := ctx.BodyParser(&req); err != nil { - return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + return model.ErrorToBadRequestErrorResponse(err) } rlog.Trace("Parsed access token request") if req.Mytoken.JWT == "" { @@ -43,27 +45,27 @@ func HandleAccessTokenEndpoint(ctx *fiber.Ctx) error { } if errRes := auth.RequireGrantType(rlog, model.GrantTypeMytoken, req.GrantType); errRes != nil { - return errRes.Send(ctx) + return errRes } mt, errRes := auth.RequireValidMytoken(rlog, nil, &req.Mytoken, ctx) if errRes != nil { - return errRes.Send(ctx) + return errRes } - usedRestriction, errRes := auth.CheckCapabilityAndRestriction( - rlog, nil, mt, ctx.IP(), + usedRestriction, errRes := auth.RequireCapabilityAndRestriction( + rlog, nil, mt, ctxutils.ClientMetaData(ctx), utils.SplitIgnoreEmpty(req.Scope, " "), utils.SplitIgnoreEmpty(req.Audience, " "), api.CapabilityAT, ) if errRes != nil { - return errRes.Send(ctx) + return errRes } provider, errRes := auth.RequireMatchingIssuer(rlog, mt.OIDCIssuer, &req.Issuer) if errRes != nil { - return errRes.Send(ctx) + return errRes } - return HandleAccessTokenRefresh(rlog, mt, req, *ctxutils.ClientMetaData(ctx), provider, usedRestriction).Send(ctx) + return HandleAccessTokenRefresh(rlog, mt, req, *ctxutils.ClientMetaData(ctx), provider, usedRestriction) } func parseScopesAndAudienceToUse( @@ -95,58 +97,67 @@ func HandleAccessTokenRefresh( rlog log.Ext1FieldLogger, mt *mytoken.Mytoken, req request.AccessTokenRequest, networkData api.ClientMetaData, provider model.Provider, usedRestriction *restrictions.Restriction, ) *model.Response { - rt, rtFound, dbErr := cryptstore.GetRefreshToken(rlog, nil, mt.ID, req.Mytoken.JWT) - if dbErr != nil { - rlog.Errorf("%s", errorfmt.Full(dbErr)) - return model.ErrorToInternalServerErrorResponse(dbErr) - } - if !rtFound { - return &model.Response{ - Status: fiber.StatusUnauthorized, - Response: model.InvalidTokenError("No refresh token attached"), - } - } + var errRes *model.Response + var tokenUpdate *response.MytokenResponse + var oidcRes *oidcreqres.OIDCTokenResponse + var retScopes string + var retAudiences []string + if err := db.Transact( + rlog, func(tx *sqlx.Tx) error { + rt, rtFound, dbErr := cryptstore.GetRefreshToken(rlog, tx, mt.ID, req.Mytoken.JWT) + if dbErr != nil { + return dbErr + } + if !rtFound { + errRes = &model.Response{ + Status: fiber.StatusUnauthorized, + Response: model.InvalidTokenError("No refresh token attached"), + } + return errors.New("rollback") + } - scopes, auds := parseScopesAndAudienceToUse( - req.Scope, strings.Split(req.Audience, " "), usedRestriction, provider.Scopes(), - ) - oidcRes, oidcErrRes, err := refresh.DoFlowAndUpdateDB(rlog, provider, mt.ID, req.Mytoken.JWT, rt, scopes, auds) - if err != nil { - rlog.Errorf("%s", errorfmt.Full(err)) - return model.ErrorToInternalServerErrorResponse(err) - } - if oidcErrRes != nil { - return &model.Response{ - Status: oidcErrRes.Status, - Response: model.OIDCError(oidcErrRes.Error, oidcErrRes.ErrorDescription), - } - } + scopes, auds := parseScopesAndAudienceToUse( + req.Scope, strings.Split(req.Audience, " "), usedRestriction, provider.Scopes(), + ) + opRes, oidcErrRes, err := refresh.DoFlowAndUpdateDB( + rlog, tx, provider, mt.ID, req.Mytoken.JWT, rt, scopes, auds, + ) + if err != nil { + return err + } + if oidcErrRes != nil { + errRes = &model.Response{ + Status: oidcErrRes.Status, + Response: model.OIDCError(oidcErrRes.Error, oidcErrRes.ErrorDescription), + } + return errors.New("rollback") + } + oidcRes = opRes - retScopes := oidcRes.Scopes - if retScopes == "" { - retScopes = scopes - } - retAudiences, _ := jwtutils.GetAudiencesFromJWT(rlog, oidcRes.AccessToken) - at := accesstokenrepo.AccessToken{ - Token: oidcRes.AccessToken, - IP: networkData.IP, - Comment: req.Comment, - Mytoken: mt, - Scopes: utils.SplitIgnoreEmpty(retScopes, " "), - Audiences: retAudiences, - } + retScopes = oidcRes.Scopes + if retScopes == "" { + retScopes = scopes + } + retAudiences, _ = jwtutils.GetAudiencesFromJWT(rlog, oidcRes.AccessToken) + at := accesstokenrepo.AccessToken{ + Token: oidcRes.AccessToken, + IP: networkData.IP, + Comment: req.Comment, + Mytoken: mt, + Scopes: utils.SplitIgnoreEmpty(retScopes, " "), + Audiences: retAudiences, + } - var tokenUpdate *response.MytokenResponse - if err = db.Transact( - rlog, func(tx *sqlx.Tx) error { if err = at.Store(rlog, tx); err != nil { return err } if err = eventService.LogEvent( - rlog, tx, eventService.MTEvent{ - Event: event.FromNumber(event.ATCreated, "Used grant_type mytoken"), - MTID: mt.ID, - }, networkData, + rlog, tx, pkg.MTEvent{ + Event: api.EventATCreated, + Comment: "Used grant_type mytoken", + MTID: mt.ID, + ClientMetaData: networkData, + }, ); err != nil { return err } @@ -161,6 +172,9 @@ func HandleAccessTokenRefresh( return err }, ); err != nil { + if errRes != nil { + return errRes + } rlog.Errorf("%s", errorfmt.Full(err)) return model.ErrorToInternalServerErrorResponse(err) } diff --git a/internal/endpoints/token/mytoken/mytokenEndpoint.go b/internal/endpoints/token/mytoken/mytokenEndpoint.go index 093bbf6e..65acb071 100644 --- a/internal/endpoints/token/mytoken/mytokenEndpoint.go +++ b/internal/endpoints/token/mytoken/mytokenEndpoint.go @@ -20,16 +20,16 @@ import ( ) // HandleMytokenEndpoint handles requests on the mytoken endpoint -func HandleMytokenEndpoint(ctx *fiber.Ctx) error { +func HandleMytokenEndpoint(ctx *fiber.Ctx) *model.Response { rlog := logger.GetRequestLogger(ctx) grantType, err := ctxutils.GetGrantType(ctx) if err != nil { - return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + return model.ErrorToBadRequestErrorResponse(err) } rlog.WithField("grant_type", grantType.String()).Trace("Received mytoken request") switch grantType { case model.GrantTypeMytoken: - return mytoken.HandleMytokenFromMytoken(ctx).Send(ctx) + return mytoken.HandleMytokenFromMytoken(ctx) case model.GrantTypeOIDCFlow: return handleOIDCFlow(ctx) case model.GrantTypePollingCode: @@ -38,27 +38,26 @@ func HandleMytokenEndpoint(ctx *fiber.Ctx) error { } case model.GrantTypeTransferCode: if config.Get().Features.TransferCodes.Enabled { - return mytoken.HandleMytokenFromTransferCode(ctx).Send(ctx) + return mytoken.HandleMytokenFromTransferCode(ctx) } } - res := model.Response{ + return &model.Response{ Status: fiber.StatusBadRequest, Response: api.ErrorUnsupportedGrantType, } - return res.Send(ctx) } -func handleOIDCFlow(ctx *fiber.Ctx) error { +func handleOIDCFlow(ctx *fiber.Ctx) *model.Response { req := response.NewOIDCFlowRequest() if err := json.Unmarshal(ctx.Body(), req); err != nil { - return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + return model.ErrorToBadRequestErrorResponse(err) } if p := provider2.GetProvider(req.Issuer); p == nil { if !utils.StringInSlice(req.Issuer, oidcfed.Issuers()) { - return model.Response{ + return &model.Response{ Status: fiber.StatusBadRequest, Response: api.ErrorUnknownIssuer, - }.Send(ctx) + } } } if len(req.Capabilities.Capabilities) == 0 { @@ -68,14 +67,13 @@ func handleOIDCFlow(ctx *fiber.Ctx) error { case model.OIDCFlowAuthorizationCode: authCodeReq := &response.AuthCodeFlowRequest{OIDCFlowRequest: *req} if err := json.Unmarshal(ctx.Body(), authCodeReq); err != nil { - return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + return model.ErrorToBadRequestErrorResponse(err) } - return authcode.StartAuthCodeFlow(ctx, authCodeReq).Send(ctx) + return authcode.StartAuthCodeFlow(ctx, authCodeReq) default: - res := model.Response{ + return &model.Response{ Status: fiber.StatusBadRequest, Response: api.ErrorUnsupportedOIDCFlow, } - return res.Send(ctx) } } diff --git a/internal/endpoints/token/mytoken/pkg/authCodeFlowRequest.go b/internal/endpoints/token/mytoken/pkg/authCodeFlowRequest.go index 26d48002..e0600c58 100644 --- a/internal/endpoints/token/mytoken/pkg/authCodeFlowRequest.go +++ b/internal/endpoints/token/mytoken/pkg/authCodeFlowRequest.go @@ -58,6 +58,7 @@ func (r *AuthCodeFlowRequest) Scan(src interface{}) error { // Value implements the driver.Valuer interface func (r AuthCodeFlowRequest) Value() (driver.Value, error) { // skipcq: CRT-P0003 + r.IncludedProfiles = nil // skipcq: RVV-B0006 v, err := json.Marshal(r) return v, errors.WithStack(err) } diff --git a/internal/endpoints/token/mytoken/pkg/myTokenResponse.go b/internal/endpoints/token/mytoken/pkg/myTokenResponse.go index ce40117d..52d16022 100644 --- a/internal/endpoints/token/mytoken/pkg/myTokenResponse.go +++ b/internal/endpoints/token/mytoken/pkg/myTokenResponse.go @@ -14,3 +14,15 @@ type MytokenResponse struct { Restrictions restrictions.Restrictions `json:"restrictions,omitempty"` TokenUpdate *MytokenResponse `json:"token_update,omitempty"` } + +// OnlyTokenUpdateRes is a response that contains only a TokenUpdate and is used when a rotating mytoken was used but +// no response is returned otherwise +type OnlyTokenUpdateRes struct { + api.OnlyTokenUpdateResponse + TokenUpdate *MytokenResponse `json:"token_update,omitempty"` +} + +// SetTokenUpdate implements the pkg.TokenUpdatableResponse interface +func (res *OnlyTokenUpdateRes) SetTokenUpdate(tokenUpdate *MytokenResponse) { + res.TokenUpdate = tokenUpdate +} diff --git a/internal/endpoints/token/mytoken/pkg/tokenUpdatableResponse.go b/internal/endpoints/token/mytoken/pkg/tokenUpdatableResponse.go index e117bb23..c5114f38 100644 --- a/internal/endpoints/token/mytoken/pkg/tokenUpdatableResponse.go +++ b/internal/endpoints/token/mytoken/pkg/tokenUpdatableResponse.go @@ -1,6 +1,6 @@ package pkg -// TokenUpdatableResponse is an interface for responses that can have a MytokenResponse as an token update (after token +// TokenUpdatableResponse is an interface for responses that can have a MytokenResponse as a token update (after token // rotation) type TokenUpdatableResponse interface { SetTokenUpdate(response *MytokenResponse) diff --git a/internal/endpoints/token/mytoken/polling/pollingEndpoint.go b/internal/endpoints/token/mytoken/polling/pollingEndpoint.go index a9b71477..abeae253 100644 --- a/internal/endpoints/token/mytoken/polling/pollingEndpoint.go +++ b/internal/endpoints/token/mytoken/polling/pollingEndpoint.go @@ -17,16 +17,16 @@ import ( ) // HandlePollingCode handles a request on the polling endpoint -func HandlePollingCode(ctx *fiber.Ctx) error { +func HandlePollingCode(ctx *fiber.Ctx) *model.Response { rlog := logger.GetRequestLogger(ctx) req := response.NewPollingCodeRequest() if err := json.Unmarshal(ctx.Body(), &req); err != nil { - return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + return model.ErrorToBadRequestErrorResponse(err) } clientMetaData := ctxutils.ClientMetaData(ctx) mt, token, pollingCodeStatus, errRes := CheckPollingCodeReq(rlog, req, *clientMetaData, false) if errRes != nil { - return errRes.Send(ctx) + return errRes } maxTokenLen := 0 if pollingCodeStatus.MaxTokenLen != nil { @@ -35,12 +35,12 @@ func HandlePollingCode(ctx *fiber.Ctx) error { res, err := mt.ToTokenResponse(rlog, pollingCodeStatus.ResponseType, maxTokenLen, *clientMetaData, token) if err != nil { rlog.Errorf("%s", errorfmt.Full(err)) - return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + return model.ErrorToInternalServerErrorResponse(err) } - return model.Response{ + return &model.Response{ Status: fiber.StatusOK, Response: res, - }.Send(ctx) + } } // CheckPollingCodeReq checks a pkg.PollingCodeRequest and returns the linked mytoken if valid diff --git a/internal/endpoints/token/mytoken/transferEndpoint.go b/internal/endpoints/token/mytoken/transferEndpoint.go index 02eb71b9..814ca8a4 100644 --- a/internal/endpoints/token/mytoken/transferEndpoint.go +++ b/internal/endpoints/token/mytoken/transferEndpoint.go @@ -16,18 +16,15 @@ import ( ) // HandleCreateTransferCodeForExistingMytoken handles request to create a transfer code for an existing mytoken -func HandleCreateTransferCodeForExistingMytoken(ctx *fiber.Ctx) error { +func HandleCreateTransferCodeForExistingMytoken(ctx *fiber.Ctx) *model.Response { rlog := logger.GetRequestLogger(ctx) var req pkg.CreateTransferCodeRequest if err := json.Unmarshal(ctx.Body(), &req); err != nil { - return model.Response{ - Status: fiber.StatusBadRequest, - Response: model.BadRequestError(errorfmt.Error(err)), - }.Send(ctx) + return model.BadRequestErrorResponse(errorfmt.Error(err)) } mt, errRes := auth.RequireValidMytoken(rlog, nil, &req.Mytoken, ctx) if errRes != nil { - return errRes.Send(ctx) + return errRes } transferCode, expiresIn, err := mytoken.CreateTransferCode( @@ -35,9 +32,9 @@ func HandleCreateTransferCodeForExistingMytoken(ctx *fiber.Ctx) error { ) if err != nil { rlog.Errorf("%s", errorfmt.Full(err)) - return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + return model.ErrorToInternalServerErrorResponse(err) } - res := &model.Response{ + return &model.Response{ Status: fiber.StatusOK, Response: pkg.TransferCodeResponse{ MytokenType: model.ResponseTypeTransferCode, @@ -47,5 +44,4 @@ func HandleCreateTransferCodeForExistingMytoken(ctx *fiber.Ctx) error { }, }, } - return res.Send(ctx) } diff --git a/internal/endpoints/tokeninfo/history.go b/internal/endpoints/tokeninfo/history.go index 87b524f7..683671cc 100644 --- a/internal/endpoints/tokeninfo/history.go +++ b/internal/endpoints/tokeninfo/history.go @@ -18,7 +18,7 @@ import ( "github.com/oidc-mytoken/server/internal/endpoints/tokeninfo/pkg" "github.com/oidc-mytoken/server/internal/model" eventService "github.com/oidc-mytoken/server/internal/mytoken/event" - event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" + pkg2 "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" "github.com/oidc-mytoken/server/internal/mytoken/restrictions" "github.com/oidc-mytoken/server/internal/mytoken/rotation" @@ -27,11 +27,12 @@ import ( ) func doTokenInfoHistory( - rlog log.Ext1FieldLogger, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, clientMetadata *api.ClientMetaData, + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, + clientMetadata *api.ClientMetaData, usedRestriction *restrictions.Restriction, ) (history eventrepo.EventHistory, tokenUpdate *response.MytokenResponse, err error) { - err = db.Transact( - rlog, func(tx *sqlx.Tx) error { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { var ids []any if len(req.MOMIDs) > 0 { for _, id := range req.MOMIDs { @@ -75,15 +76,16 @@ func doTokenInfoHistory( if err != nil { return err } - ev := event.FromNumber(event.TokenInfoHistory, "") + ev := api.EventTokenInfoHistory if len(req.MOMIDs) > 0 { - ev = event.FromNumber(event.TokenInfoHistoryOtherToken, "") + ev = api.EventTokenInfoHistoryOtherToken } return eventService.LogEvent( - rlog, tx, eventService.MTEvent{ - Event: ev, - MTID: mt.ID, - }, *clientMetadata, + rlog, tx, pkg2.MTEvent{ + Event: ev, + MTID: mt.ID, + ClientMetaData: *clientMetadata, + }, ) }, ) @@ -91,18 +93,17 @@ func doTokenInfoHistory( } func handleTokenInfoHistory( - rlog log.Ext1FieldLogger, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, clientMetadata *api.ClientMetaData, -) model.Response { - usedRestriction, errRes := auth.RequireUsableRestrictionOther( - rlog, nil, mt, clientMetadata.IP, nil, nil, - ) + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, + clientMetadata *api.ClientMetaData, +) *model.Response { + usedRestriction, errRes := auth.RequireUsableRestrictionOther(rlog, nil, mt, clientMetadata) if errRes != nil { - return *errRes + return errRes } - history, tokenUpdate, err := doTokenInfoHistory(rlog, req, mt, clientMetadata, usedRestriction) + history, tokenUpdate, err := doTokenInfoHistory(rlog, tx, req, mt, clientMetadata, usedRestriction) if err != nil { rlog.Errorf("%s", errorfmt.Full(err)) - return *model.ErrorToInternalServerErrorResponse(err) + return model.ErrorToInternalServerErrorResponse(err) } rsp := pkg.NewTokeninfoHistoryResponse(history, tokenUpdate) return makeTokenInfoResponse(rsp, tokenUpdate) @@ -110,30 +111,34 @@ func handleTokenInfoHistory( // HandleTokenInfoHistory handles a tokeninfo history request func HandleTokenInfoHistory( - rlog log.Ext1FieldLogger, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, clientMetadata *api.ClientMetaData, -) model.Response { + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, + clientMetadata *api.ClientMetaData, +) *model.Response { // If we call this function it means the token is valid. + rlog.Debug("Handle tokeninfo history request") if len(req.MOMIDs) == 0 { - if errRes := auth.RequireCapability(rlog, api.CapabilityTokeninfoHistory, mt); errRes != nil { - return *errRes + if errRes := auth.RequireCapability( + rlog, tx, api.CapabilityTokeninfoHistory, mt, clientMetadata, + ); errRes != nil { + return errRes } - return handleTokenInfoHistory(rlog, req, mt, clientMetadata) + return handleTokenInfoHistory(rlog, tx, req, mt, clientMetadata) } - if !mt.Capabilities.Has(api.CapabilityHistoryAnyToken) { - for _, momid := range req.MOMIDs { + for _, momid := range req.MOMIDs { + if !mt.Capabilities.Has(api.CapabilityHistoryAnyToken) { if momid == api.MOMIDValueThis || momid == api.MOMIDValueChildren { continue } if strings.HasPrefix(momid, api.MOMIDValueChildren+"@") { momid = momid[len(api.MOMIDValueChildren)+1:] } - isParent, err := helper.MOMIDHasParent(rlog, nil, momid, mt.ID) + isParent, err := helper.MOMIDHasParent(rlog, tx, momid, mt.ID) if err != nil { - return *model.ErrorToInternalServerErrorResponse(err) + return model.ErrorToInternalServerErrorResponse(err) } if !isParent { - return model.Response{ + return &model.Response{ Status: fiber.StatusForbidden, Response: api.Error{ Error: api.ErrorStrInsufficientCapabilities, @@ -145,23 +150,23 @@ func HandleTokenInfoHistory( }, } } + } - same, err := helper.CheckMytokensAreForSameUser(rlog, nil, momid, mt.ID) - if err != nil { - return *model.ErrorToInternalServerErrorResponse(err) - } - if !same { - return model.Response{ - Status: fiber.StatusForbidden, - Response: api.Error{ - Error: api.ErrorStrInvalidGrant, - ErrorDescription: fmt.Sprintf( - "The provided token cannot be used to obtain history for mom_id '%s'", momid, - ), - }, - } + same, err := helper.CheckMytokensAreForSameUser(rlog, tx, momid, mt.ID) + if err != nil { + return model.ErrorToInternalServerErrorResponse(err) + } + if !same { + return &model.Response{ + Status: fiber.StatusForbidden, + Response: api.Error{ + Error: api.ErrorStrInvalidGrant, + ErrorDescription: fmt.Sprintf( + "The provided token cannot be used to obtain history for mom_id '%s'", momid, + ), + }, } } } - return handleTokenInfoHistory(rlog, req, mt, clientMetadata) + return handleTokenInfoHistory(rlog, tx, req, mt, clientMetadata) } diff --git a/internal/endpoints/tokeninfo/introspect.go b/internal/endpoints/tokeninfo/introspect.go index f28843cc..ac40d98d 100644 --- a/internal/endpoints/tokeninfo/introspect.go +++ b/internal/endpoints/tokeninfo/introspect.go @@ -11,44 +11,46 @@ import ( "github.com/oidc-mytoken/server/internal/endpoints/tokeninfo/pkg" "github.com/oidc-mytoken/server/internal/model" eventService "github.com/oidc-mytoken/server/internal/mytoken/event" - event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" + pkg2 "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" "github.com/oidc-mytoken/server/internal/utils/auth" - "github.com/oidc-mytoken/server/internal/utils/errorfmt" ) // HandleTokenInfoIntrospect handles token introspection func HandleTokenInfoIntrospect( rlog log.Ext1FieldLogger, + tx *sqlx.Tx, mt *mytoken.Mytoken, origionalTokenType model.ResponseType, clientMetadata *api.ClientMetaData, -) model.Response { +) *model.Response { // If we call this function it means the token is valid. - if errRes := auth.RequireCapability(rlog, api.CapabilityTokeninfoIntrospect, mt); errRes != nil { - return *errRes + if errRes := auth.RequireCapability( + rlog, tx, api.CapabilityTokeninfoIntrospect, mt, clientMetadata, + ); errRes != nil { + return errRes } var usedToken mytoken.UsedMytoken if err := db.RunWithinTransaction( - rlog, nil, func(tx *sqlx.Tx) error { + rlog, tx, func(tx *sqlx.Tx) error { tmp, err := mt.ToUsedMytoken(rlog, tx) if err != nil { return err } usedToken = *tmp return eventService.LogEvent( - rlog, tx, eventService.MTEvent{ - Event: event.FromNumber(event.TokenInfoIntrospect, ""), - MTID: mt.ID, - }, *clientMetadata, + rlog, tx, pkg2.MTEvent{ + Event: api.EventTokenInfoIntrospect, + MTID: mt.ID, + ClientMetaData: *clientMetadata, + }, ) }, ); err != nil { - rlog.Errorf("%s", errorfmt.Full(err)) - return *model.ErrorToInternalServerErrorResponse(err) + return nil } - return model.Response{ + return &model.Response{ Status: fiber.StatusOK, Response: pkg.TokeninfoIntrospectResponse{ TokeninfoIntrospectResponse: api.TokeninfoIntrospectResponse{ diff --git a/internal/endpoints/tokeninfo/list.go b/internal/endpoints/tokeninfo/list.go index eef80d24..fbc786dc 100644 --- a/internal/endpoints/tokeninfo/list.go +++ b/internal/endpoints/tokeninfo/list.go @@ -15,7 +15,7 @@ import ( "github.com/oidc-mytoken/server/internal/endpoints/tokeninfo/pkg" "github.com/oidc-mytoken/server/internal/model" eventService "github.com/oidc-mytoken/server/internal/mytoken/event" - event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" + pkg2 "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" "github.com/oidc-mytoken/server/internal/mytoken/restrictions" "github.com/oidc-mytoken/server/internal/mytoken/rotation" @@ -24,11 +24,12 @@ import ( ) func doTokenInfoList( - rlog log.Ext1FieldLogger, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, clientMetadata *api.ClientMetaData, + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, + clientMetadata *api.ClientMetaData, usedRestriction *restrictions.Restriction, ) (tokenList []*tree.MytokenEntryTree, tokenUpdate *response.MytokenResponse, err error) { - err = db.Transact( - rlog, func(tx *sqlx.Tx) error { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { tokenList, err = tree.AllTokens(rlog, tx, mt.ID) if err != nil && !errors.Is(err, sql.ErrNoRows) { return err @@ -46,10 +47,11 @@ func doTokenInfoList( return err } return eventService.LogEvent( - rlog, tx, eventService.MTEvent{ - Event: event.FromNumber(event.TokenInfoListMTs, ""), - MTID: mt.ID, - }, *clientMetadata, + rlog, tx, pkg2.MTEvent{ + Event: api.EventTokenInfoListMTs, + MTID: mt.ID, + ClientMetaData: *clientMetadata, + }, ) }, ) @@ -58,19 +60,20 @@ func doTokenInfoList( // HandleTokenInfoList handles a tokeninfo list request func HandleTokenInfoList( - rlog log.Ext1FieldLogger, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, clientMetadata *api.ClientMetaData, -) model.Response { + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, + clientMetadata *api.ClientMetaData, +) *model.Response { // If we call this function it means the token is valid. - usedRestriction, errRes := auth.CheckCapabilityAndRestriction( - rlog, nil, mt, clientMetadata.IP, nil, nil, api.CapabilityListMT, + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, tx, mt, clientMetadata, api.CapabilityListMT, ) if errRes != nil { - return *errRes + return errRes } - tokenList, tokenUpdate, err := doTokenInfoList(rlog, req, mt, clientMetadata, usedRestriction) + tokenList, tokenUpdate, err := doTokenInfoList(rlog, tx, req, mt, clientMetadata, usedRestriction) if err != nil { rlog.Errorf("%s", errorfmt.Full(err)) - return *model.ErrorToInternalServerErrorResponse(err) + return model.ErrorToInternalServerErrorResponse(err) } rsp := pkg.NewTokeninfoListResponse(tokenList, tokenUpdate) return makeTokenInfoResponse(rsp, tokenUpdate) diff --git a/internal/endpoints/tokeninfo/notifications.go b/internal/endpoints/tokeninfo/notifications.go new file mode 100644 index 00000000..3f3f5d2d --- /dev/null +++ b/internal/endpoints/tokeninfo/notifications.go @@ -0,0 +1,169 @@ +package tokeninfo + +import ( + "fmt" + + "github.com/gofiber/fiber/v2" + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/db" + helper "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/mytokenrepohelper" + "github.com/oidc-mytoken/server/internal/db/notificationsrepo" + "github.com/oidc-mytoken/server/internal/endpoints/tokeninfo/pkg" + "github.com/oidc-mytoken/server/internal/model" + eventService "github.com/oidc-mytoken/server/internal/mytoken/event" + pkg2 "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" + mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/restrictions" + "github.com/oidc-mytoken/server/internal/mytoken/rotation" + "github.com/oidc-mytoken/server/internal/utils/auth" + "github.com/oidc-mytoken/server/internal/utils/cookies" + "github.com/oidc-mytoken/server/internal/utils/errorfmt" +) + +func doTokenInfoNotifications( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, + clientMetadata *api.ClientMetaData, + usedRestriction *restrictions.Restriction, +) (res pkg.TokeninfoNotificationsResponse, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + if len(req.MOMIDs) > 1 { + res.MomIDMapping = make(map[string]api.NotificationsCombinedResponse) + for _, id := range req.MOMIDs { + if id == api.MOMIDValueThis { + id = mt.ID.String() + } + var data api.NotificationsCombinedResponse + data.Notifications, data.Calendars, err = notificationsrepo.GetNotificationsAndCalendarsForMT( + rlog, tx, id, + ) + if err != nil { + return err + } + res.MomIDMapping[id] = data + } + } else { + var id any + id = mt.ID + if len(req.MOMIDs) > 0 { + id = req.MOMIDs[0] + } + res.Notifications, res.Calendars, err = notificationsrepo.GetNotificationsAndCalendarsForMT( + rlog, tx, id, + ) + if err != nil { + return err + } + } + if usedRestriction == nil { + return nil + } + if err = usedRestriction.UsedOther(rlog, tx, mt.ID); err != nil { + return err + } + res.TokenUpdate, err = rotation.RotateMytokenAfterOtherForResponse( + rlog, tx, req.Mytoken.JWT, mt, *clientMetadata, req.Mytoken.OriginalTokenType, + ) + if err != nil { + return err + } + ev := api.EventTokenInfoNotifications + if len(req.MOMIDs) > 0 { + ev = api.EventTokenInfoNotificationsOtherToken + } + return eventService.LogEvent( + rlog, tx, pkg2.MTEvent{ + Event: ev, + MTID: mt.ID, + ClientMetaData: *clientMetadata, + }, + ) + }, + ) + return +} + +func handleTokenInfoNotifications( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, + clientMetadata *api.ClientMetaData, +) *model.Response { + usedRestriction, errRes := auth.RequireUsableRestrictionOther(rlog, nil, mt, clientMetadata) + if errRes != nil { + return errRes + } + res, err := doTokenInfoNotifications(rlog, tx, req, mt, clientMetadata, usedRestriction) + if err != nil { + rlog.Errorf("%s", errorfmt.Full(err)) + return model.ErrorToInternalServerErrorResponse(err) + } + rsp := &model.Response{ + Status: fiber.StatusOK, + Response: res, + } + if res.TokenUpdate != nil { + rsp.Cookies = []*fiber.Cookie{cookies.MytokenCookie(res.TokenUpdate.Mytoken)} + } + return rsp +} + +// HandleTokenInfoNotifications handles a tokeninfo notifications request +func HandleTokenInfoNotifications( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, + clientMetadata *api.ClientMetaData, +) *model.Response { + // If we call this function it means the token is valid. + + rlog.Debug("Handle tokeninfo notifications request") + if len(req.MOMIDs) == 0 { + if errRes := auth.RequireCapability( + rlog, tx, api.CapabilityTokeninfoNotify, mt, clientMetadata, + ); errRes != nil { + return errRes + } + return handleTokenInfoNotifications(rlog, tx, req, mt, clientMetadata) + } + for _, momid := range req.MOMIDs { + if !mt.Capabilities.Has(api.CapabilityNotifyAnyTokenRead) { + if momid == api.MOMIDValueThis { + continue + } + isParent, err := helper.MOMIDHasParent(rlog, tx, momid, mt.ID) + if err != nil { + return model.ErrorToInternalServerErrorResponse(err) + } + if !isParent { + return &model.Response{ + Status: fiber.StatusForbidden, + Response: api.Error{ + Error: api.ErrorStrInsufficientCapabilities, + ErrorDescription: fmt.Sprintf( + "The provided token is neither a parent of the the token with "+ + " mom_id '%s' nor does it have the '%s' capability", momid, + api.CapabilityNotifyAnyTokenRead.Name, + ), + }, + } + } + } + + same, err := helper.CheckMytokensAreForSameUser(rlog, tx, momid, mt.ID) + if err != nil { + return model.ErrorToInternalServerErrorResponse(err) + } + if !same { + return &model.Response{ + Status: fiber.StatusForbidden, + Response: api.Error{ + Error: api.ErrorStrInvalidGrant, + ErrorDescription: fmt.Sprintf( + "The provided token cannot be used to obtain notifications for mom_id '%s'", momid, + ), + }, + } + } + } + return handleTokenInfoNotifications(rlog, tx, req, mt, clientMetadata) +} diff --git a/internal/endpoints/tokeninfo/pkg/tokeninfoNotificationsResponse.go b/internal/endpoints/tokeninfo/pkg/tokeninfoNotificationsResponse.go new file mode 100644 index 00000000..b42ff964 --- /dev/null +++ b/internal/endpoints/tokeninfo/pkg/tokeninfoNotificationsResponse.go @@ -0,0 +1,14 @@ +package pkg + +import ( + "github.com/oidc-mytoken/api/v0" + + my "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" +) + +// TokeninfoNotificationsResponse is a type for responses to tokeninfo notification requests +type TokeninfoNotificationsResponse struct { + // on update check api.TokeninfoNotificationsResponse + api.TokeninfoNotificationsResponse + TokenUpdate *my.MytokenResponse `json:"token_update,omitempty"` +} diff --git a/internal/endpoints/tokeninfo/subtokens.go b/internal/endpoints/tokeninfo/subtokens.go index 0348fa6d..a609b251 100644 --- a/internal/endpoints/tokeninfo/subtokens.go +++ b/internal/endpoints/tokeninfo/subtokens.go @@ -14,7 +14,7 @@ import ( "github.com/oidc-mytoken/server/internal/endpoints/tokeninfo/pkg" "github.com/oidc-mytoken/server/internal/model" eventService "github.com/oidc-mytoken/server/internal/mytoken/event" - event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" + pkg2 "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" "github.com/oidc-mytoken/server/internal/mytoken/restrictions" "github.com/oidc-mytoken/server/internal/mytoken/rotation" @@ -23,11 +23,12 @@ import ( ) func doTokenInfoSubtokens( - rlog log.Ext1FieldLogger, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, clientMetadata *api.ClientMetaData, + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, + clientMetadata *api.ClientMetaData, usedRestriction *restrictions.Restriction, ) (tokenTree tree.MytokenEntryTree, tokenUpdate *response.MytokenResponse, err error) { - err = db.Transact( - rlog, func(tx *sqlx.Tx) error { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { tokenTree, err = tree.TokenSubTree(rlog, tx, mt.ID) if err != nil && !errors.Is(err, sql.ErrNoRows) { return err @@ -45,10 +46,11 @@ func doTokenInfoSubtokens( return err } return eventService.LogEvent( - rlog, tx, eventService.MTEvent{ - Event: event.FromNumber(event.TokenInfoSubtokens, ""), - MTID: mt.ID, - }, *clientMetadata, + rlog, tx, pkg2.MTEvent{ + Event: api.EventTokenInfoSubtokens, + MTID: mt.ID, + ClientMetaData: *clientMetadata, + }, ) }, ) @@ -57,19 +59,20 @@ func doTokenInfoSubtokens( // HandleTokenInfoSubtokens handles a tokeninfo list subtokens request func HandleTokenInfoSubtokens( - rlog log.Ext1FieldLogger, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, clientMetadata *api.ClientMetaData, -) model.Response { + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, + clientMetadata *api.ClientMetaData, +) *model.Response { // If we call this function it means the token is valid. - usedRestriction, errRes := auth.CheckCapabilityAndRestriction( - rlog, nil, mt, clientMetadata.IP, nil, nil, api.CapabilityTokeninfoSubtokens, + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, tx, mt, clientMetadata, api.CapabilityTokeninfoSubtokens, ) if errRes != nil { - return *errRes + return errRes } - tokenTree, tokenUpdate, err := doTokenInfoSubtokens(rlog, req, mt, clientMetadata, usedRestriction) + tokenTree, tokenUpdate, err := doTokenInfoSubtokens(rlog, tx, req, mt, clientMetadata, usedRestriction) if err != nil { rlog.Errorf("%s", errorfmt.Full(err)) - return *model.ErrorToInternalServerErrorResponse(err) + return model.ErrorToInternalServerErrorResponse(err) } rsp := pkg.NewTokeninfoSubtokensResponse(tokenTree, tokenUpdate) return makeTokenInfoResponse(rsp, tokenUpdate) diff --git a/internal/endpoints/tokeninfo/tokeninfo.go b/internal/endpoints/tokeninfo/tokeninfo.go index 639ac6a9..8c7ed97f 100644 --- a/internal/endpoints/tokeninfo/tokeninfo.go +++ b/internal/endpoints/tokeninfo/tokeninfo.go @@ -16,40 +16,39 @@ import ( ) // HandleTokenInfo handles requests to the tokeninfo endpoint -func HandleTokenInfo(ctx *fiber.Ctx) error { +func HandleTokenInfo(ctx *fiber.Ctx) *model.Response { rlog := logger.GetRequestLogger(ctx) var req pkg.TokenInfoRequest if err := json.Unmarshal(ctx.Body(), &req); err != nil { - return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + return model.ErrorToBadRequestErrorResponse(err) } mt, errRes := auth.RequireValidMytoken(rlog, nil, &req.Mytoken, ctx) if errRes != nil { - return errRes.Send(ctx) + return errRes } clientMetadata := ctxutils.ClientMetaData(ctx) switch req.Action { case model.TokeninfoActionIntrospect: - return HandleTokenInfoIntrospect(rlog, mt, req.Mytoken.OriginalTokenType, clientMetadata).Send(ctx) + return HandleTokenInfoIntrospect(rlog, nil, mt, req.Mytoken.OriginalTokenType, clientMetadata) + case model.TokeninfoActionNotifications: + return HandleTokenInfoNotifications(rlog, nil, &req, mt, clientMetadata) case model.TokeninfoActionEventHistory: - return HandleTokenInfoHistory(rlog, &req, mt, clientMetadata).Send(ctx) + return HandleTokenInfoHistory(rlog, nil, &req, mt, clientMetadata) case model.TokeninfoActionSubtokenTree: - return HandleTokenInfoSubtokens(rlog, &req, mt, clientMetadata).Send(ctx) + return HandleTokenInfoSubtokens(rlog, nil, &req, mt, clientMetadata) case model.TokeninfoActionListMytokens: - return HandleTokenInfoList(rlog, &req, mt, clientMetadata).Send(ctx) + return HandleTokenInfoList(rlog, nil, &req, mt, clientMetadata) default: - return model.Response{ - Status: fiber.StatusBadRequest, - Response: model.BadRequestError(fmt.Sprintf("unknown action '%s'", req.Action.String())), - }.Send(ctx) + return model.BadRequestErrorResponse(fmt.Sprintf("unknown action '%s'", req.Action.String())) } } -func makeTokenInfoResponse(rsp interface{}, tokenUpdate *response.MytokenResponse) model.Response { +func makeTokenInfoResponse(rsp interface{}, tokenUpdate *response.MytokenResponse) *model.Response { var cake []*fiber.Cookie if tokenUpdate != nil { cake = []*fiber.Cookie{cookies.MytokenCookie(tokenUpdate.Mytoken)} } - return model.Response{ + return &model.Response{ Status: fiber.StatusOK, Response: rsp, Cookies: cake, diff --git a/internal/endpoints/consent/pkg/capability.go b/internal/endpoints/webentities/capability.go similarity index 96% rename from internal/endpoints/consent/pkg/capability.go rename to internal/endpoints/webentities/capability.go index a9c652f2..149356c7 100644 --- a/internal/endpoints/consent/pkg/capability.go +++ b/internal/endpoints/webentities/capability.go @@ -1,4 +1,4 @@ -package pkg +package webentities import ( "strings" @@ -128,8 +128,12 @@ var normalCapabilities = []string{ api.CapabilityTokeninfoIntrospect.Name, api.CapabilityTokeninfoHistory.Name, api.CapabilityTokeninfoSubtokens.Name, + api.CapabilityTokeninfoNotify.Name, api.CapabilityGrantsRead.Name, api.CapabilitySSHGrantRead.Name, + api.CapabilityEmail.Name, + api.CapabilityEmailRead.Name, + api.CapabilityNotifyAnyTokenRead.Name, } var warningCapabilities = []string{ api.CapabilityListMT.Name, @@ -138,6 +142,7 @@ var warningCapabilities = []string{ api.CapabilityRevokeAnyToken.Name, api.CapabilityHistoryAnyToken.Name, api.CapabilityManageMTs.Name, + api.CapabilityNotifyAnyToken.Name, } var dangerCapabilities = []string{ api.CapabilitySettings.Name, diff --git a/internal/endpoints/webentities/notificationclass.go b/internal/endpoints/webentities/notificationclass.go new file mode 100644 index 00000000..ffbd5daf --- /dev/null +++ b/internal/endpoints/webentities/notificationclass.go @@ -0,0 +1,64 @@ +package webentities + +import ( + "strings" + + "github.com/oidc-mytoken/api/v0" +) + +// WebNotificationClass is type for representing api.NoticationClass in the web compatible with WebCapability +type WebNotificationClass struct { + ReadWriteCapability *api.NotificationClass + Children []*WebNotificationClass +} + +// WebNotificationClasses creates a slice of WebNotificationClass from []api.NotificationClass +func WebNotificationClasses(ncs []*api.NotificationClass) (wnc []*WebNotificationClass) { + for _, nc := range ncs { + wnc = append( + wnc, webNotificationClassFromNotificationClass(nc), + ) + } + return +} + +// AllWebNotificationClass returns all WebNotificationClass as a tree +func AllWebNotificationClass() []*WebNotificationClass { + return allWebNotificationClass +} + +var allWebNotificationClass []*WebNotificationClass + +func init() { + if allWebNotificationClass == nil { + allWebNotificationClass = []*WebNotificationClass{} + } + for _, nc := range api.AllNotificationClasses { + if strings.Contains(nc.Name, ":") { + continue + } + allWebNotificationClass = append( + allWebNotificationClass, notificationClassToWebNotificationClass(nc), + ) + } +} + +func notificationClassToWebNotificationClass(nc *api.NotificationClass) *WebNotificationClass { + var childs []*WebNotificationClass + for _, c := range nc.GetChildren() { + childs = append(childs, notificationClassToWebNotificationClass(c)) + } + return &WebNotificationClass{ + ReadWriteCapability: nc, + Children: childs, + } +} + +func webNotificationClassFromNotificationClass(nc *api.NotificationClass) *WebNotificationClass { + for _, wnc := range allWebNotificationClass { + if wnc.ReadWriteCapability.Name == nc.Name { + return wnc + } + } + return nil +} diff --git a/internal/endpoints/consent/pkg/restriction.go b/internal/endpoints/webentities/restriction.go similarity index 95% rename from internal/endpoints/consent/pkg/restriction.go rename to internal/endpoints/webentities/restriction.go index 9be4f546..50082317 100644 --- a/internal/endpoints/consent/pkg/restriction.go +++ b/internal/endpoints/webentities/restriction.go @@ -1,4 +1,4 @@ -package pkg +package webentities import ( "encoding/json" diff --git a/internal/endpoints/consent/pkg/utils.go b/internal/endpoints/webentities/utils.go similarity index 95% rename from internal/endpoints/consent/pkg/utils.go rename to internal/endpoints/webentities/utils.go index 00453450..8126d42e 100644 --- a/internal/endpoints/consent/pkg/utils.go +++ b/internal/endpoints/webentities/utils.go @@ -1,4 +1,4 @@ -package pkg +package webentities // Bootstrap text color classes const ( diff --git a/internal/jws/jws.go b/internal/jws/jws.go index 509450b7..66c3e221 100644 --- a/internal/jws/jws.go +++ b/internal/jws/jws.go @@ -13,8 +13,8 @@ import ( "github.com/golang-jwt/jwt" "github.com/lestrrat-go/jwx/jwa" - "github.com/lestrrat-go/jwx/jwk" "github.com/pkg/errors" + "github.com/zachmann/go-oidfed/pkg/jwk" "github.com/oidc-mytoken/server/internal/config" ) @@ -117,7 +117,7 @@ type signingKeys map[KeyUsage]signingKeyMaterial type signingKeyMaterial struct { SK crypto.Signer PK crypto.PublicKey - JWKS jwk.Set + JWKS jwk.JWKS } var keys signingKeys @@ -145,7 +145,7 @@ func GetPublicKey(usage KeyUsage) (pk crypto.PublicKey) { } // GetJWKS returns the jwks -func GetJWKS(usage KeyUsage) (jwks jwk.Set) { +func GetJWKS(usage KeyUsage) (jwks jwk.JWKS) { k, ok := keys[usage] if ok { jwks = k.JWKS @@ -196,25 +196,10 @@ func loadKey(keyfile string, usage KeyUsage, alg jwa.SignatureAlgorithm) { } keyData, found := keys[usage] if !found { - keyData = signingKeyMaterial{ - JWKS: jwk.NewSet(), - } + keyData = signingKeyMaterial{} } keyData.SK = sk keyData.PK = sk.Public() - key, err := jwk.New(keyData.PK) - if err != nil { - panic(err) - } - if err = jwk.AssignKeyID(key); err != nil { - panic(err) - } - if err = key.Set(jwk.KeyUsageKey, jwk.ForSignature); err != nil { - panic(err) - } - if err = key.Set(jwk.AlgorithmKey, alg); err != nil { - panic(err) - } - keyData.JWKS.Add(key) + keyData.JWKS = jwk.KeyToJWKS(keyData.PK, alg) keys[usage] = keyData } diff --git a/internal/model/ipparseresult.go b/internal/model/ipparseresult.go new file mode 100644 index 00000000..4b21021c --- /dev/null +++ b/internal/model/ipparseresult.go @@ -0,0 +1,11 @@ +package model + +import ( + "net" +) + +// IPParseResult holds the result of ip parsing +type IPParseResult struct { + IP net.IP + IPNet *net.IPNet +} diff --git a/internal/model/keyvalue.go b/internal/model/keyvalue.go new file mode 100644 index 00000000..fd8d1963 --- /dev/null +++ b/internal/model/keyvalue.go @@ -0,0 +1,10 @@ +package model + +// KeyValue is a type for a key-value pair +type KeyValue struct { + Key string `json:"key"` + Value any `json:"value"` +} + +// KeyValues is a slice of KeyValue +type KeyValues []KeyValue diff --git a/internal/model/oidcFlow.go b/internal/model/oidcFlow.go index 87e20207..e64a4dd8 100644 --- a/internal/model/oidcFlow.go +++ b/internal/model/oidcFlow.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" ) -// OIDCFlow is a enum like type for oidc flows +// OIDCFlow is an enum like type for oidc flows type OIDCFlow int var oidcFlows = [...]string{api.OIDCFlowAuthorizationCode} diff --git a/internal/model/provider.go b/internal/model/provider.go index a6241f56..26287a5e 100644 --- a/internal/model/provider.go +++ b/internal/model/provider.go @@ -7,6 +7,7 @@ import ( "github.com/oidc-mytoken/server/pkg/oauth2x" ) +// Provider is an interface type for OIDC providers type Provider interface { Name() string Issuer() string @@ -21,12 +22,14 @@ type Provider interface { ) (string, error) } +// AudienceConf is a type for holding configuration about audience type AudienceConf struct { RFC8707 bool `yaml:"use_rfc8707"` RequestParameter string `yaml:"request_parameter"` SpaceSeparateAuds bool `yaml:"space_separate_auds"` } +// Constants for audience parameters const ( AudienceParameterAudience = "audience" AudienceParameterResource = "resource" diff --git a/internal/model/response.go b/internal/model/response.go index 06898711..fa9cfe81 100644 --- a/internal/model/response.go +++ b/internal/model/response.go @@ -26,6 +26,9 @@ func (r Response) Send(ctx *fiber.Ctx) error { if fasthttp.StatusCodeIsRedirect(r.Status) { return ctx.Redirect(r.Response.(string), r.Status) } + if r.Response == nil { + return ctx.SendStatus(r.Status) + } return ctx.Status(r.Status).JSON(r.Response) } @@ -45,6 +48,25 @@ func ErrorToBadRequestErrorResponse(err error) *Response { } } +// NotFoundErrorResponse returns a error response for a not found error +func NotFoundErrorResponse(msg string) *Response { + return &Response{ + Status: fiber.StatusNotFound, + Response: api.Error{ + Error: "not_found", + ErrorDescription: msg, + }, + } +} + +// BadRequestErrorResponse returns a error response for a not found error +func BadRequestErrorResponse(msg string) *Response { + return &Response{ + Status: fiber.StatusBadRequest, + Response: BadRequestError(msg), + } +} + // ResponseNYI is the server response when something is not yet implemented var ResponseNYI = Response{ Status: fiber.StatusNotImplemented, diff --git a/internal/model/responseType.go b/internal/model/responseType.go index fa28d066..a99be0e1 100644 --- a/internal/model/responseType.go +++ b/internal/model/responseType.go @@ -10,7 +10,7 @@ import ( "gopkg.in/yaml.v3" ) -// ResponseType is a enum like type for response types +// ResponseType is an enum like type for response types type ResponseType int var responseTypes = [...]string{ diff --git a/internal/model/tokeninfoAction.go b/internal/model/tokeninfoAction.go index 922a03ef..bce2344a 100644 --- a/internal/model/tokeninfoAction.go +++ b/internal/model/tokeninfoAction.go @@ -20,6 +20,7 @@ const ( // assert that these are in the same order as api.AllTokeninfoActions TokeninfoActionEventHistory TokeninfoActionSubtokenTree TokeninfoActionListMytokens + TokeninfoActionNotifications maxTokeninfoAction ) diff --git a/internal/model/version/VERSION b/internal/model/version/VERSION index f76f9131..2774f858 100644 --- a/internal/model/version/VERSION +++ b/internal/model/version/VERSION @@ -1 +1 @@ -0.9.2 \ No newline at end of file +0.10.0 \ No newline at end of file diff --git a/internal/mytoken/event/event.go b/internal/mytoken/event/event.go index e48fe8e0..2ca71e0e 100644 --- a/internal/mytoken/event/event.go +++ b/internal/mytoken/event/event.go @@ -4,32 +4,28 @@ import ( "github.com/jmoiron/sqlx" log "github.com/sirupsen/logrus" - "github.com/oidc-mytoken/api/v0" - "github.com/oidc-mytoken/server/internal/db/dbrepo/eventrepo" "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" - "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" + notifier "github.com/oidc-mytoken/server/internal/notifier/client" ) -// MTEvent is type for mytoken events -type MTEvent struct { - *event.Event - MTID mtid.MTID -} - // LogEvent logs an event to the database -func LogEvent(rlog log.Ext1FieldLogger, tx *sqlx.Tx, event MTEvent, clientMetaData api.ClientMetaData) error { - return (&eventrepo.EventDBObject{ +func LogEvent(rlog log.Ext1FieldLogger, tx *sqlx.Tx, event pkg.MTEvent) error { + if err := (&eventrepo.EventDBObject{ Event: event.Event, + Comment: event.Comment, MTID: event.MTID, - ClientMetaData: clientMetaData, - }).Store(rlog, tx) + ClientMetaData: event.ClientMetaData, + }).Store(rlog, tx); err != nil { + return err + } + return notifier.SendNotificationsForEvent(rlog, tx, event) } // LogEvents logs multiple events for the same token to the database -func LogEvents(rlog log.Ext1FieldLogger, tx *sqlx.Tx, events []MTEvent, clientMetaData api.ClientMetaData) error { +func LogEvents(rlog log.Ext1FieldLogger, tx *sqlx.Tx, events []pkg.MTEvent) error { for _, e := range events { - if err := LogEvent(rlog, tx, e, clientMetaData); err != nil { + if err := LogEvent(rlog, tx, e); err != nil { return err } } diff --git a/internal/mytoken/event/pkg/event.go b/internal/mytoken/event/pkg/event.go deleted file mode 100644 index c466506b..00000000 --- a/internal/mytoken/event/pkg/event.go +++ /dev/null @@ -1,113 +0,0 @@ -package event - -import ( - "database/sql/driver" - - "github.com/pkg/errors" -) - -// Event is an enum like type for events -type Event struct { - Type int - Comment string -} - -func (e *Event) String() string { - if e.Type < 0 || e.Type >= len(AllEvents) { - return "" - } - return AllEvents[e.Type] -} - -// Valid checks that Event is a defined Event -func (e *Event) Valid() bool { - return e.Type < maxEvent && e.Type >= 0 -} - -// Value implements the sql.Valuer interface -func (e *Event) Value() (driver.Value, error) { - return e.String(), nil -} - -// Scan implements the sql.Scanner interface -func (e *Event) Scan(src interface{}) error { - number := eventStringToInt(src.(string)) - if number < 0 { - return errors.New("unknown event") - } - e.Type = number - return nil -} - -// NewEvent creates a new Event from the event string -func NewEvent(typ, comment string) *Event { - number := eventStringToInt(typ) - return FromNumber(number, comment) -} - -// FromNumber creates an Event from the number -func FromNumber(number int, comment string) *Event { - if number < 0 { - return nil - } - return &Event{ - Type: number, - Comment: comment, - } -} - -func eventStringToInt(str string) int { - for i, e := range AllEvents { - if str == e { - return i - } - } - return -1 -} - -// AllEvents hold all possible Events -var AllEvents = [...]string{ - "unknown", - "created", - "AT_created", - "MT_created", - "tokeninfo_introspect", - "tokeninfo_history", - "tokeninfo_subtokens", - "tokeninfo_list_mytokens", - "inherited_RT", - "transfer_code_created", - "transfer_code_used", - "token_rotated", - "settings_grant_enabled", - "settings_grant_disabled", - "settings_grants_listed", - "ssh_keys_listed", - "ssh_key_added", - "revoked_other_token", - "tokeninfo_history_other_token", -} - -// Events for Mytokens -const ( - UnknownEvent = iota - MTCreated - ATCreated - SubtokenCreated - TokenInfoIntrospect - TokenInfoHistory - TokenInfoSubtokens - TokenInfoListMTs - InheritedRT - TransferCodeCreated - TransferCodeUsed - MTRotated - GrantEnabled - GrantDisabled - GrantsListed - SSHKeyListed - SSHKeyAdded - RevokedOtherToken - TokenInfoHistoryOtherToken - maxEvent -) diff --git a/internal/mytoken/event/pkg/mtevent.go b/internal/mytoken/event/pkg/mtevent.go new file mode 100644 index 00000000..09070f87 --- /dev/null +++ b/internal/mytoken/event/pkg/mtevent.go @@ -0,0 +1,15 @@ +package pkg + +import ( + "github.com/oidc-mytoken/api/v0" + + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" +) + +// MTEvent is type for mytoken events +type MTEvent struct { + Event api.Event + Comment string + MTID mtid.MTID + api.ClientMetaData +} diff --git a/internal/mytoken/mytokenHandler.go b/internal/mytoken/mytokenHandler.go index fbd81070..264d3f36 100644 --- a/internal/mytoken/mytokenHandler.go +++ b/internal/mytoken/mytokenHandler.go @@ -19,10 +19,11 @@ import ( dbhelper "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/mytokenrepohelper" "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/transfercoderepo" "github.com/oidc-mytoken/server/internal/db/dbrepo/refreshtokenrepo" + "github.com/oidc-mytoken/server/internal/db/notificationsrepo" response "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" "github.com/oidc-mytoken/server/internal/model" eventService "github.com/oidc-mytoken/server/internal/mytoken/event" - event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" "github.com/oidc-mytoken/server/internal/mytoken/restrictions" @@ -111,10 +112,10 @@ func HandleMytokenFromTransferCode(ctx *fiber.Ctx) *model.Response { // HandleMytokenFromMytokenReqChecks handles the necessary req checks for a pkg.MytokenFromMytokenRequest func HandleMytokenFromMytokenReqChecks( - rlog log.Ext1FieldLogger, req *response.MytokenFromMytokenRequest, ip string, + rlog log.Ext1FieldLogger, req *response.MytokenFromMytokenRequest, clientData *api.ClientMetaData, ctx *fiber.Ctx, ) (*restrictions.Restriction, *mytoken.Mytoken, *model.Response) { - req.Restrictions.ReplaceThisIP(ip) + req.Restrictions.ReplaceThisIP(clientData.IP) req.Restrictions.ClearUnsupportedKeys() rlog.Trace("Parsed mytoken request") @@ -124,8 +125,8 @@ func HandleMytokenFromMytokenReqChecks( if errRes != nil { return nil, nil, errRes } - usedRestriction, errRes := auth.CheckCapabilityAndRestriction( - rlog, nil, mt, ip, nil, nil, api.CapabilityCreateMT, + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, nil, mt, clientData, api.CapabilityCreateMT, ) if errRes != nil { return nil, nil, errRes @@ -144,7 +145,7 @@ func HandleMytokenFromMytoken(ctx *fiber.Ctx) *model.Response { if err := errors.WithStack(json.Unmarshal(ctx.Body(), &req)); err != nil { return model.ErrorToBadRequestErrorResponse(err) } - usedRestriction, mt, errRes := HandleMytokenFromMytokenReqChecks(rlog, req, ctx.IP(), ctx) + usedRestriction, mt, errRes := HandleMytokenFromMytokenReqChecks(rlog, req, ctxutils.ClientMetaData(ctx), ctx) if errRes != nil { return errRes } @@ -178,20 +179,31 @@ func HandleMytokenFromMytokenReq( if err = ste.Store(rlog, tx, "Used grant_type mytoken"); err != nil { return } + if err = notificationsrepo.ExpandNotificationsToChildrenIfApplicable( + rlog, tx, parent.ID, ste.ID, + ); err != nil { + return err + } + if err = notificationsrepo.ScheduleExpirationNotificationsIfNeeded( + rlog, tx, ste.ID, ste.Token.ExpiresAt, ste.Token.IssuedAt, + ); err != nil { + return err + } return eventService.LogEvents( - rlog, tx, []eventService.MTEvent{ + rlog, tx, []pkg.MTEvent{ { - Event: event.FromNumber(event.InheritedRT, "Got RT from parent"), - MTID: ste.ID, + Event: api.EventInheritedRT, + Comment: "Got RT from parent", + MTID: ste.ID, + ClientMetaData: *networkData, }, { - Event: event.FromNumber( - event.SubtokenCreated, - strings.TrimSpace(fmt.Sprintf("Created MT %s", req.GeneralMytokenRequest.Name)), - ), - MTID: parent.ID, + Event: api.EventSubtokenCreated, + Comment: strings.TrimSpace(fmt.Sprintf("Created MT %s", req.GeneralMytokenRequest.Name)), + MTID: parent.ID, + ClientMetaData: *networkData, }, - }, *networkData, + }, ) }, ); err != nil { @@ -235,24 +247,15 @@ func createMytokenEntry( } } if changed := req.Restrictions.EnforceMaxLifetime(parent.OIDCIssuer); changed && req.FailOnRestrictionsNotTighter { - return nil, &model.Response{ - Status: fiber.StatusBadRequest, - Response: model.BadRequestError("requested restrictions do not respect maximum mytoken lifetime"), - } + return nil, model.BadRequestErrorResponse("requested restrictions do not respect maximum mytoken lifetime") } r, ok := restrictions.Tighten(rlog, parent.Restrictions, req.Restrictions.Restrictions) if !ok && req.FailOnRestrictionsNotTighter { - return nil, &model.Response{ - Status: fiber.StatusBadRequest, - Response: model.BadRequestError("requested restrictions are not subset of original restrictions"), - } + return nil, model.BadRequestErrorResponse("requested restrictions are not subset of original restrictions") } c := api.TightenCapabilities(parent.Capabilities, req.Capabilities.Capabilities) if len(c) == 0 { - return nil, &model.Response{ - Status: fiber.StatusBadRequest, - Response: model.BadRequestError("mytoken to be issued cannot have any of the requested capabilities"), - } + return nil, model.BadRequestErrorResponse("mytoken to be issued cannot have any of the requested capabilities") } var rot *api.Rotation if req.Rotation != nil { diff --git a/internal/mytoken/pkg/mtid/momid.go b/internal/mytoken/pkg/mtid/momid.go index 82ae6c14..d299fa1d 100644 --- a/internal/mytoken/pkg/mtid/momid.go +++ b/internal/mytoken/pkg/mtid/momid.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" ) +// MOMID is a type for the mom-id (manage-other-mytokens)-id type MOMID struct { MTID } diff --git a/internal/mytoken/pkg/mtid/mtid.go b/internal/mytoken/pkg/mtid/mtid.go index 61eefc4d..0f639a10 100644 --- a/internal/mytoken/pkg/mtid/mtid.go +++ b/internal/mytoken/pkg/mtid/mtid.go @@ -19,9 +19,9 @@ type MTID struct { // New creates a new MTID func New() (MTID, error) { - uuid, err := uuid.NewV4() + u, err := uuid.NewV4() return MTID{ - UUID: uuid, + UUID: u, }, errors.WithStack(err) } diff --git a/internal/mytoken/pkg/mytoken.go b/internal/mytoken/pkg/mytoken.go index dcc11195..62de4284 100644 --- a/internal/mytoken/pkg/mytoken.go +++ b/internal/mytoken/pkg/mytoken.go @@ -1,6 +1,7 @@ package mytoken import ( + "encoding/json" "fmt" "github.com/golang-jwt/jwt" @@ -13,12 +14,14 @@ import ( "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/mytokenrepohelper" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/shorttokenrepo" "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/transfercoderepo" response "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" "github.com/oidc-mytoken/server/internal/jws" "github.com/oidc-mytoken/server/internal/model" eventService "github.com/oidc-mytoken/server/internal/mytoken/event" - event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" "github.com/oidc-mytoken/server/internal/mytoken/restrictions" "github.com/oidc-mytoken/server/internal/mytoken/universalmytoken" @@ -77,7 +80,7 @@ func (mt *Mytoken) verifySubject() bool { // VerifyCapabilities verifies that this Mytoken has the required capabilities func (mt *Mytoken) VerifyCapabilities(required ...api.Capability) bool { - if mt.Capabilities == nil || len(mt.Capabilities) == 0 { + if len(mt.Capabilities) == 0 { return false } for _, c := range required { @@ -182,7 +185,7 @@ func (mt *Mytoken) toMytokenResponse(jwt string) response.MytokenResponse { } func (mt *Mytoken) toShortMytokenResponse(rlog log.Ext1FieldLogger, jwt string) (response.MytokenResponse, error) { - shortToken, err := transfercoderepo.NewShortToken(jwt, mt.ID) + shortToken, err := shorttokenrepo.NewShortToken(jwt, mt.ID) if err != nil { return response.MytokenResponse{}, err } @@ -222,12 +225,12 @@ func CreateTransferCode( return err } return eventService.LogEvent( - rlog, tx, eventService.MTEvent{ - Event: event.FromNumber( - event.TransferCodeCreated, fmt.Sprintf("token type: %s", responseType.String()), - ), - MTID: myID, - }, clientMetaData, + rlog, tx, pkg.MTEvent{ + Event: api.EventTransferCodeCreated, + Comment: fmt.Sprintf("token type: %s", responseType.String()), + MTID: myID, + ClientMetaData: clientMetaData, + }, ) }, ) @@ -300,7 +303,7 @@ func parseJWT(token string, skipCalimsValidation bool) (*Mytoken, error) { SkipClaimsValidation: skipCalimsValidation, } tok, err := parser.ParseWithClaims( - token, &Mytoken{}, func(t *jwt.Token) (interface{}, error) { + token, &Mytoken{}, func(_ *jwt.Token) (interface{}, error) { return jws.GetPublicKey(jws.KeyUsageMytokenSigning), nil }, ) @@ -310,7 +313,54 @@ func parseJWT(token string, skipCalimsValidation bool) (*Mytoken, error) { if mt, ok := tok.Claims.(*Mytoken); ok && tok.Valid { mt.jwt = token - return mt, nil + return mt, specialTokenHandling(mt) } return nil, errors.New("token not valid") } + +// DBMetadata return the mytokenrepohelper.MytokenDBMetadata for a Mytoken +func (mt *Mytoken) DBMetadata() (meta mytokenrepohelper.MytokenDBMetadata, err error) { + creator := func(i any) (db.NullString, error) { + data := "" + if i != nil { + dataBytes, err := json.Marshal(i) + if err != nil { + return db.NullString{}, errors.WithStack(err) + } + data = string(dataBytes) + } + return db.NewNullString(data), nil + } + + meta.Capabilities, err = creator(mt.Capabilities) + if err != nil { + return + } + meta.Rotation, err = creator(mt.Rotation) + if err != nil { + return + } + meta.Restrictions, err = creator(mt.Restrictions) + if err != nil { + return + } + return +} + +func specialTokenHandling(mt *Mytoken) error { + if mt.Version.Before( + api.TokenVersion{ + Major: 0, + Minor: 7, + }, + ) { + meta, err := mt.DBMetadata() + if err != nil { + return err + } + return mytokenrepohelper.SetMetadata( + log.StandardLogger(), nil, mt.ID, meta, + ) + } + return nil +} diff --git a/internal/mytoken/restrictions/restriction.go b/internal/mytoken/restrictions/restriction.go index c598ed84..51ff6207 100644 --- a/internal/mytoken/restrictions/restriction.go +++ b/internal/mytoken/restrictions/restriction.go @@ -45,6 +45,7 @@ type Restriction struct { api.Restriction `json:",inline"` } +// NewRestrictionsFromAPI turn api.Restrictions into Restrictions func NewRestrictionsFromAPI(apis api.Restrictions) (rs Restrictions) { for _, a := range apis { rs = append( diff --git a/internal/mytoken/rotation/rotation.go b/internal/mytoken/rotation/rotation.go index 98d57940..d76bfffd 100644 --- a/internal/mytoken/rotation/rotation.go +++ b/internal/mytoken/rotation/rotation.go @@ -11,7 +11,7 @@ import ( "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" "github.com/oidc-mytoken/server/internal/model" eventService "github.com/oidc-mytoken/server/internal/mytoken/event" - event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" + pkg2 "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" ) @@ -32,10 +32,11 @@ func rotateMytoken( return err } return eventService.LogEvent( - rlog, tx, eventService.MTEvent{ - Event: event.FromNumber(event.MTRotated, ""), - MTID: rotated.ID, - }, clientMetaData, + rlog, tx, pkg2.MTEvent{ + Event: api.EventMTRotated, + MTID: rotated.ID, + ClientMetaData: clientMetaData, + }, ) }, ); err != nil { @@ -44,8 +45,8 @@ func rotateMytoken( return rotated, true, nil } -// RotateMytokenAfterAT rotates a mytoken after it was used to obtain an AT if rotation is enabled for that case -func RotateMytokenAfterAT( +// rotateMytokenAfterAT rotates a mytoken after it was used to obtain an AT if rotation is enabled for that case +func rotateMytokenAfterAT( rlog log.Ext1FieldLogger, tx *sqlx.Tx, oldJWT string, old *mytoken.Mytoken, clientMetaData api.ClientMetaData, ) (*mytoken.Mytoken, bool, error) { if old.Rotation == nil { @@ -57,9 +58,9 @@ func RotateMytokenAfterAT( return rotateMytoken(rlog, tx, oldJWT, old, clientMetaData) } -// RotateMytokenAfterOther rotates a mytoken after it was used for other usages than AT if rotation is enabled for that +// rotateMytokenAfterOther rotates a mytoken after it was used for other usages than AT if rotation is enabled for that // case -func RotateMytokenAfterOther( +func rotateMytokenAfterOther( rlog log.Ext1FieldLogger, tx *sqlx.Tx, oldJWT string, old *mytoken.Mytoken, clientMetaData api.ClientMetaData, ) (*mytoken.Mytoken, bool, error) { if old.Rotation == nil { @@ -77,7 +78,7 @@ func RotateMytokenAfterOtherForResponse( rlog log.Ext1FieldLogger, tx *sqlx.Tx, oldJWT string, old *mytoken.Mytoken, clientMetaData api.ClientMetaData, responseType model.ResponseType, ) (*pkg.MytokenResponse, error) { - my, rotated, err := RotateMytokenAfterOther(rlog, tx, oldJWT, old, clientMetaData) + my, rotated, err := rotateMytokenAfterOther(rlog, tx, oldJWT, old, clientMetaData) if err != nil { return nil, err } @@ -94,7 +95,7 @@ func RotateMytokenAfterATForResponse( rlog log.Ext1FieldLogger, tx *sqlx.Tx, oldJWT string, old *mytoken.Mytoken, clientMetaData api.ClientMetaData, responseType model.ResponseType, ) (*pkg.MytokenResponse, error) { - my, rotated, err := RotateMytokenAfterAT(rlog, tx, oldJWT, old, clientMetaData) + my, rotated, err := rotateMytokenAfterAT(rlog, tx, oldJWT, old, clientMetaData) if err != nil { return nil, err } diff --git a/internal/mytoken/universalmytoken/universalmytoken.go b/internal/mytoken/universalmytoken/universalmytoken.go index 49133230..236ab617 100644 --- a/internal/mytoken/universalmytoken/universalmytoken.go +++ b/internal/mytoken/universalmytoken/universalmytoken.go @@ -8,7 +8,7 @@ import ( "github.com/pkg/errors" log "github.com/sirupsen/logrus" - "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/transfercoderepo" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/shorttokenrepo" "github.com/oidc-mytoken/server/internal/model" "github.com/oidc-mytoken/server/internal/utils" ) @@ -50,7 +50,7 @@ func Parse(rlog log.Ext1FieldLogger, token string) (UniversalMytoken, error) { OriginalTokenType: model.ResponseTypeToken, }, nil } - shortToken := transfercoderepo.ParseShortToken(token) + shortToken := shorttokenrepo.ParseShortToken(token) jwt, valid, dbErr := shortToken.JWT(rlog, nil) var validErr error if !valid { diff --git a/internal/notifier/client/notifier_client.go b/internal/notifier/client/notifier_client.go new file mode 100644 index 00000000..c711bbfc --- /dev/null +++ b/internal/notifier/client/notifier_client.go @@ -0,0 +1,246 @@ +package notifier + +import ( + "fmt" + + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + "github.com/oidc-mytoken/utils/httpclient" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/mytokenrepohelper" + "github.com/oidc-mytoken/server/internal/db/dbrepo/userrepo" + "github.com/oidc-mytoken/server/internal/db/notificationsrepo" + "github.com/oidc-mytoken/server/internal/model" + pkg2 "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" + "github.com/oidc-mytoken/server/internal/notifier/pkg" + server "github.com/oidc-mytoken/server/internal/notifier/server" + "github.com/oidc-mytoken/server/internal/notifier/server/mailing" + "github.com/oidc-mytoken/server/internal/server/routes" + "github.com/oidc-mytoken/server/internal/utils/geoip" +) + +type notifierClient interface { + SendEmailRequest(req pkg.EmailNotificationRequest) +} + +var notifier notifierClient + +// Init initializes the notifier client, +// depending on the server deployment model either for communicating with a standalone notifier server or with the +// integrated one. +func Init() { + if !config.Get().Features.Notifications.AnyEnabled { + return + } + if nsURL := config.Get().Features.Notifications.NotifierServer; nsURL != "" { + initStandalone(nsURL) + } else { + initIntegraded() + } + initScheduler() +} + +func initStandalone(serverURL string) { + notifier = standaloneNotifier{ + serverAddress: serverURL, + paths: server.ServerPaths.Prefix(serverURL), + } +} +func initIntegraded() { + notifier = integratedNotifier{} + server.InitIntegrated() +} + +type standaloneNotifier struct { + serverAddress string + paths server.Paths +} +type integratedNotifier struct{} + +// SendEmailRequest sends a pkg.EmailNotificationRequest to the standalone notifier server +func (n standaloneNotifier) SendEmailRequest(req pkg.EmailNotificationRequest) { + _, err := httpclient.Do().R(). + SetBody(req). + Post(n.paths.Email) + if err != nil { + log.WithError(err).Error("error while sending notification request to notifier server") + } +} + +// SendEmailRequest sends a pkg.EmailNotificationRequest to the integrated notification server +func (integratedNotifier) SendEmailRequest(req pkg.EmailNotificationRequest) { + _ = server.HandleEmailRequest(req) +} + +// SendTemplateEmail sends a templated email through the relevant notification server +func SendTemplateEmail(to, subject string, preferHTML bool, template string, binding any) { + // skipcq GO-E1007 + go func() { + req := pkg.EmailNotificationRequest{ + To: to, + Subject: subject, + PreferHTML: preferHTML, + Template: template, + BindingData: binding, + } + notifier.SendEmailRequest(req) + }() +} + +// SendICSMail sends an ics calendar invite via email through the relevant notification server +func SendICSMail(to, subject, text string, attachments ...mailing.Attachment) { + // skipcq GO-E1007 + go func() { + req := pkg.EmailNotificationRequest{ + To: to, + Subject: subject, + Text: text, + Attachments: attachments, + ICSInvite: true, + } + notifier.SendEmailRequest(req) + }() +} + +// SendNotificationsForEvent sends all relevant notifications for an event through the relevant notification server, +// if there are any +func SendNotificationsForEvent(rlog log.Ext1FieldLogger, tx *sqlx.Tx, e pkg2.MTEvent) error { + rlog.WithField("event", e.Event.String()).Debug("checking and sending notification for event") + nc := api.NotificationClassFromEvent(e.Event) + if nc == nil { + return nil + } + rlog.WithField("notification_class", nc.Name).Trace("found notification class") + notifications, err := notificationsrepo.GetNotificationsForMTAndClass(rlog, tx, e.MTID, nc) + if err != nil { + return err + } + if len(notifications) == 0 { + return nil + } + rlog.WithField("number_notifications", len(notifications)).Trace("found notifications for the class and token") + return sendNotificationsForNotificationInfos(rlog, tx, e.MTID, notifications, nc.Name, &e.ClientMetaData, &e, nil) +} + +// SendNotificationsForSubClass sends all relevant notifications for a Notification( +// sub)class through the relevant notification server, if there are any +func SendNotificationsForSubClass( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, + nc *api.NotificationClass, clientData *api.ClientMetaData, additionalData model.KeyValues, + additionalCallbackCheck func() (bool, error), +) error { + rlog.WithField("notification_class", nc.Name).Debug("checking and sending notification for (sub)class") + allNotifications, err := notificationsrepo.GetNotificationsForMT(rlog, tx, mtID) + if err != nil { + return err + } + rlog.WithField("number_all_notifications", len(allNotifications)).Trace("found notifications for token") + var notifications []api.NotificationInfoBase + for _, n := range allNotifications { + thisNC := api.NewNotificationClass(n.Class) + if thisNC.Contains(nc) { + notifications = append(notifications, n.NotificationInfoBase.NotificationInfoBase) + } + } + if len(notifications) == 0 { + return nil + } + if additionalCallbackCheck != nil { + ok, err := additionalCallbackCheck() + if err != nil { + return err + } + if !ok { + return nil + } + } + rlog.WithField("number_filtered_notifications", len(notifications)).Trace("found notifications for token and class") + return sendNotificationsForNotificationInfos( + rlog, tx, mtID, notifications, nc.Name, clientData, nil, additionalData, + ) +} + +func sendNotificationsForNotificationInfos( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, + notifications []api.NotificationInfoBase, notificationClassName string, + clientData *api.ClientMetaData, + e *pkg2.MTEvent, additionalData model.KeyValues, +) error { + mailAlreadySent := false + for _, n := range notifications { + switch n.Type { + case api.NotificationTypeMail: + if !mailAlreadySent { + mailAlreadySent = true + emailInfo, err := userrepo.GetMail(rlog, tx, mtID) + if err != nil { + return err + } + if !emailInfo.Mail.Valid { + return errors.New("no email set for user") + } + if !emailInfo.MailVerified { + return errors.New("notification email not verified") + } + tokenName, err := mytokenrepohelper.GetMTName(rlog, tx, mtID) + if err != nil { + return err + } + bindingData := map[string]any{ + "management-url": routes.NotificationManagementURL(n.ManagementCode), + } + if emailInfo.PreferHTMLMail { + bindingData["ip"] = clientData.IP + bindingData["user-agent"] = clientData.UserAgent + bindingData["country"] = geoip.Country(clientData.IP) + bindingData["notification-class"] = notificationClassName + bindingData["mom_id"] = mtID.Hash() + bindingData["token-name"] = tokenName.String + if e != nil { + bindingData["event"] = e.Event.String() + bindingData["comment"] = e.Comment + } + if additionalData != nil { + bindingData["additional-data"] = additionalData + } + } else { + tableData := map[string]string{} + if tokenName.Valid { + tableData["Mytoken Name"] = tokenName.String + } + tableData["Mytoken Mom ID"] = mtID.Hash() + tableData["IP"] = clientData.IP + tableData["User-Agent"] = clientData.UserAgent + if country := geoip.Country(clientData.IP); country != "" { + tableData["Location"] = country + } + tableData["Notification Reason"] = notificationClassName + + if e != nil { + tableData["Event"] = e.Event.String() + tableData["Comment"] = e.Comment + } + for _, kv := range additionalData { + tableData[kv.Key] = fmt.Sprintf("%v", kv.Value) + } + txtTable := generateSimpleTable(nil, tableData) + bindingData["txt-table"] = txtTable + } + rlog.Debug("sending notification mail") + SendTemplateEmail( + emailInfo.Mail.String, fmt.Sprintf("mytoken notification: %s", notificationClassName), + emailInfo.PreferHTMLMail, "notification", bindingData, + ) + + } + case api.NotificationTypeWebsocket: + return errors.New("not yet implemented") + } + + } + return nil +} diff --git a/internal/notifier/client/scheduler.go b/internal/notifier/client/scheduler.go new file mode 100644 index 00000000..135fe4ec --- /dev/null +++ b/internal/notifier/client/scheduler.go @@ -0,0 +1,148 @@ +package notifier + +import ( + "fmt" + "time" + + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + "github.com/oidc-mytoken/utils/unixtime" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/mytokenrepohelper" + "github.com/oidc-mytoken/server/internal/db/dbrepo/userrepo" + "github.com/oidc-mytoken/server/internal/db/notificationsrepo" + "github.com/oidc-mytoken/server/internal/endpoints/actions" + "github.com/oidc-mytoken/server/internal/server/routes" +) + +func initScheduler() { + ticker := time.NewTicker(time.Minute) + go func() { + for range ticker.C { + checkingForDueNotifications() + } + }() +} + +func checkingForDueNotifications() { + logger := log.StandardLogger() + logger.Trace("Checking for notifications to send") + var abort bool + for { + if err := db.Transact( + logger, func(tx *sqlx.Tx) error { + n, err := notificationsrepo.PopOneScheduledNotification(logger, tx) + if err != nil { + log.WithError(err).Error("error popping scheduled notification") + return err + } + if n == nil { + abort = true + return nil + } + return handleDueNotification(logger, tx, n) + }, + ); err != nil { + break + } + if abort { + logger.Trace("No notifications due") + break + } + } +} + +func handleDueNotification(logger log.Ext1FieldLogger, tx *sqlx.Tx, n *notificationsrepo.ScheduledNotification) error { + logger.Trace("Got a notification") + switch n.Type { + case api.NotificationTypeMail: + return handleDueMailNotification(logger, tx, n) + case api.NotificationTypeWebsocket: + // NYI + return nil + default: + return errors.New("unknown notification type") + } +} + +func handleDueMailNotification( + logger log.Ext1FieldLogger, tx *sqlx.Tx, + n *notificationsrepo.ScheduledNotification, +) error { + emailInfo, err := userrepo.GetMail(logger, tx, n.MTID) + if err != nil { + return err + } + if !emailInfo.MailVerified { + return nil + } + name, err := mytokenrepohelper.GetMTName(logger, tx, n.MTID) + if err != nil { + return err + } + var subject string + var template string + var bindingData map[string]any + switch n.Class { + case notificationsrepo.ScheduleClassExp: + exp_, ok := n.AdditionalInfo[notificationsrepo.AdditionalInfoKeyExpiresAt].(float64) + if !ok { + logger.Error("'expires_at' missing or wrong time in scheduled notification of class 'exp'") + return nil + } + exp := unixtime.UnixTime(exp_) + diff := time.Until(exp.Time()) + var diffStr string + if diff < 24*time.Hour { + diff = diff.Round(time.Hour) + diffStr = fmt.Sprintf("%d hours", diff/time.Hour) + } else { + diff = diff.Round(24 * time.Hour) + diffStr = fmt.Sprintf("%d days", diff/(24*time.Hour)) + } + var quotedName string + if name.Valid { + quotedName = fmt.Sprintf(" '%s'", name.String) + } + subject = fmt.Sprintf("mytoken%s expires in %s", quotedName, diffStr) + template = "notification-exp" + recreateURL, err := actions.CreateRecreateToken(logger, tx, n.MTID) + if err != nil { + return err + } + unsubscribeURL, err := actions.GetUnsubscribeScheduled( + logger, tx, n.MTID, n.NotificationID, + ) + if err != nil { + return err + } + bindingData = map[string]any{ + "management-url": routes.NotificationManagementURL(n.ManagementCode), + "recreate-url": recreateURL, + "unsubscribe-exp-this-token-url": unsubscribeURL, + } + if emailInfo.MailVerified { + bindingData["token-name"] = name.String + bindingData["mom_id"] = n.MTID.Hash() + bindingData["expires_at"] = exp.Time().String() + } else { + tableData := map[string]string{} + if name.Valid { + tableData["Mytoken Name"] = name.String + } + tableData["Mytoken Mom ID"] = n.MTID.Hash() + tableData["Expires"] = exp.Time().String() + txtTable := generateSimpleTable(nil, tableData) + bindingData["txt-table"] = txtTable + } + default: + return nil + } + SendTemplateEmail( + emailInfo.Mail.String, subject, emailInfo.PreferHTMLMail, template, bindingData, + ) + return nil +} diff --git a/internal/notifier/client/txt_table_writer.go b/internal/notifier/client/txt_table_writer.go new file mode 100644 index 00000000..60906d50 --- /dev/null +++ b/internal/notifier/client/txt_table_writer.go @@ -0,0 +1,36 @@ +package notifier + +import ( + "bytes" + "io" + + "github.com/olekukonko/tablewriter" +) + +func fPrintTable(out io.Writer, headers []string, data [][]string) { + t := tablewriter.NewWriter(out) + t.SetHeader(headers) + // t.SetRowLine(true) + t.SetColumnSeparator("I") + t.AppendBulk(data) + t.Render() +} + +func fPrintSimpleTable(out io.Writer, headers []string, data map[string]string) { + dataSlice := make([][]string, 0) + for k, v := range data { + dataSlice = append( + dataSlice, []string{ + k, + v, + }, + ) + } + fPrintTable(out, headers, dataSlice) +} + +func generateSimpleTable(headers []string, data map[string]string) string { + buf := bytes.NewBufferString("") + fPrintSimpleTable(buf, headers, data) + return buf.String() +} diff --git a/internal/notifier/pkg/notificationRequest.go b/internal/notifier/pkg/notificationRequest.go new file mode 100644 index 00000000..061e71c4 --- /dev/null +++ b/internal/notifier/pkg/notificationRequest.go @@ -0,0 +1,18 @@ +package pkg + +import ( + "github.com/oidc-mytoken/server/internal/notifier/server/mailing" +) + +// EmailNotificationRequest holds a request to send an email notification +type EmailNotificationRequest struct { + To string `json:"to"` + Subject string `json:"subject"` + Text string `json:"text,omitempty"` + PreferHTML bool `json:"prefer_html"` + Template string `json:"template,omitempty"` + BindingData any `json:"binding_data,omitempty"` + ICSInvite bool `json:"ics_invite,omitempty"` + Attachments []mailing.Attachment `json:"attachments,omitempty"` + ScheduleID string `json:"schedule_id,omitempty"` +} diff --git a/internal/notifier/server/mailing/mailing.go b/internal/notifier/server/mailing/mailing.go new file mode 100644 index 00000000..1cd15c7e --- /dev/null +++ b/internal/notifier/server/mailing/mailing.go @@ -0,0 +1,241 @@ +package mailing + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/smtp" + "strings" + "time" + + "github.com/jordan-wright/email" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/notifier/server/mailing/mailtemplates" + "github.com/oidc-mytoken/server/internal/utils/multilimiter" +) + +var mailPool *email.Pool +var fromAddress string + +// Init initializes the mailing +func Init(conf config.MailNotificationConf) { + if !conf.Enabled { + HTMLMailSender = noopSender{} + PlainTextMailSender = noopSender{} + return + } + fromAddress = conf.MailServer.FromAddress + var err error + mailPool, err = email.NewPool( + fmt.Sprintf("%s:%d", conf.MailServer.Host, conf.MailServer.Port), + 4, + smtp.PlainAuth("", conf.MailServer.Username, conf.MailServer.Password, conf.MailServer.Host), + ) + if err != nil { + log.WithError(err).Fatal("could not connect to email server") + } + log.Info("Connected to mail server") + mailtemplates.Init() +} + +var limits *multilimiter.MultiStore + +func init() { + var err error + limits, err = multilimiter.NewDefaultMultiStore() + if err != nil { + panic(err) + } +} + +func sendEMail(mail *email.Email) error { + err := errors.WithStack(mailPool.Send(mail, 10*time.Second)) + if err == nil { + return nil + } + log.WithError(err).Error("error while sending mail") + m := err.Error() + if strings.Contains(m, "broken pipe") { + // retry + time.Sleep(300 * time.Millisecond) + return errors.WithStack(mailPool.Send(mail, 10*time.Second)) + } + return err +} + +// SendEMail send the passed email.Email +func SendEMail(mail *email.Email) error { + ok, reset, firstFailed, err := limits.Take(context.Background(), mail.To[0]) + if err != nil { + return err + } + if ok { + return sendEMail(mail) + } + if firstFailed { + text := []byte(fmt.Sprintf( + "You have reached your mail limit on this mytoken server. "+ + "The limit will reset in %s", time.Until(reset), + )) + if mail.HTML != nil { + mail.HTML = text + } else { + mail.Text = text + } + mail.Attachments = nil + mail.Subject = "mail limit reached" + err = sendEMail(mail) + if err != nil { + log.WithError(err).Error("error while sending mail limit mail") + } + } + return errors.Errorf("mail limit reached; limit will reset in %s", time.Until(reset)) +} + +// Attachment is a type holding information about an email attachment +type Attachment struct { + Reader io.Reader + Filename string + ContentType string +} +type attachmentMarshal struct { + Data []byte `json:"d"` + Filename string `json:"f"` + ContentType string `json:"ct"` +} + +// MarshalJSON implements the json.Marshaler interface +func (a Attachment) MarshalJSON() ([]byte, error) { + readerData, err := io.ReadAll(a.Reader) + if err != nil { + return nil, errors.WithStack(err) + } + aa := attachmentMarshal{ + Data: readerData, + Filename: a.Filename, + ContentType: a.ContentType, + } + return json.Marshal(aa) +} + +// UnmarshalJSON implements the json.Unmarshaler +func (a *Attachment) UnmarshalJSON(data []byte) error { + var aa attachmentMarshal + if err := json.Unmarshal(data, &aa); err != nil { + return errors.WithStack(err) + } + a.Filename = aa.Filename + a.ContentType = aa.ContentType + a.Reader = bytes.NewReader(aa.Data) + return nil +} + +// MailSender is an interface for types that can send mails +type MailSender interface { + Send(to, subject, text string, attachments ...Attachment) error +} + +// TemplateMailSender is an interface for types that can send template mails +type TemplateMailSender interface { + SendTemplate(to, subject, template string, binding any) error + MailSender +} + +type plainTextMailSender struct{} +type htmlMailSender struct{} +type icsMailSender struct{} +type noopSender struct{} + +// PlainTextMailSender is a MailSender that sends plain text mails +var PlainTextMailSender TemplateMailSender = plainTextMailSender{} + +// HTMLMailSender is a MailSender that sends html mails +var HTMLMailSender TemplateMailSender = htmlMailSender{} + +// ICSMailSender is a MailSender that sends calendar invitations +var ICSMailSender MailSender = icsMailSender{} + +// Send implements the MailSender interface +func (noopSender) Send(_, _, _ string, _ ...Attachment) error { + return nil +} + +// SendTemplate implements the TemplateMailSender interface +func (noopSender) SendTemplate(_, _, _ string, _ any) error { + return nil +} + +// Send implements the MailSender interface +func (plainTextMailSender) Send(to, subject, text string, attachments ...Attachment) error { + mail := &email.Email{ + From: fromAddress, + To: []string{to}, + Subject: subject, + Text: []byte(text), + } + for _, a := range attachments { + _, err := mail.Attach(a.Reader, a.Filename, a.ContentType) + if err != nil { + return errors.WithStack(err) + } + } + return SendEMail(mail) +} + +// SendTemplate implements the TemplateMailSender interface +func (s plainTextMailSender) SendTemplate(to, subject, template string, binding any) error { + text, err := mailtemplates.Text(template, binding) + if err != nil { + return err + } + return s.Send(to, subject, text) +} + +// Send implements the MailSender interface +func (htmlMailSender) Send(to, subject, text string, attachments ...Attachment) error { + mail := &email.Email{ + From: fromAddress, + To: []string{to}, + Subject: subject, + HTML: []byte(text), + } + for _, a := range attachments { + _, err := mail.Attach(a.Reader, a.Filename, a.ContentType) + if err != nil { + return errors.WithStack(err) + } + } + return SendEMail(mail) +} + +// SendTemplate implements the TemplateMailSender interface +func (s htmlMailSender) SendTemplate(to, subject, template string, binding any) error { + text, err := mailtemplates.HTML(template, binding) + if err != nil { + return err + } + return s.Send(to, subject, text) +} + +// Send implements the MailSender interface +func (icsMailSender) Send(to, subject, text string, attachments ...Attachment) error { + mail := &email.Email{ + From: fromAddress, + To: []string{to}, + Subject: subject, + Text: []byte(text), + } + for _, a := range attachments { + aa, err := mail.Attach(a.Reader, a.Filename, a.ContentType) + if err != nil { + return errors.WithStack(err) + } + aa.Header.Set("Content-Disposition", "inline") + } + return SendEMail(mail) +} diff --git a/internal/notifier/server/mailing/mailtemplates/mailtemplates.go b/internal/notifier/server/mailing/mailtemplates/mailtemplates.go new file mode 100644 index 00000000..705347a5 --- /dev/null +++ b/internal/notifier/server/mailing/mailtemplates/mailtemplates.go @@ -0,0 +1,69 @@ +package mailtemplates + +import ( + "bytes" + "embed" + "io/fs" + "net/http" + + "github.com/gofiber/template/mustache/v2" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/utils/fileio" +) + +// Subjects +const ( + SubjectVerifyMail = "mytoken notifications - Verify email" +) + +// TemplateNames +const ( + TemplateVerifyMail = "verify_mail" +) + +//go:embed templates +var _templates embed.FS +var templates fs.FS + +var engine *mustache.Engine + +func init() { + var err error + templates, err = fs.Sub(_templates, "templates") + if err != nil { + log.WithError(err).Fatal() + } +} + +// Init initializes the mail templates +func Init() { + overWriteDir := config.Get().Features.Notifications.Mail.OverwriteDir + engine = mustache.NewFileSystem( + fileio.NewLocalAndOtherSearcherFilesystem(overWriteDir, http.FS(templates)), + ".mustache", + ) + if err := engine.Load(); err != nil { + log.WithError(err).Fatal() + } +} + +func render(name, suffix string, bindData any) (string, error) { + var buf bytes.Buffer + if err := engine.Render(&buf, name+suffix, bindData); err != nil { + return "", errors.WithStack(err) + } + return buf.String(), nil +} + +// HTML renders a html-suffix file +func HTML(name string, bindData any) (string, error) { + return render(name, ".html", bindData) +} + +// Text renders a txt-suffix file +func Text(name string, bindData any) (string, error) { + return render(name, ".txt", bindData) +} diff --git a/internal/notifier/server/mailing/mailtemplates/templates/notification-exp.html.mustache b/internal/notifier/server/mailing/mailtemplates/templates/notification-exp.html.mustache new file mode 100644 index 00000000..29609fac --- /dev/null +++ b/internal/notifier/server/mailing/mailtemplates/templates/notification-exp.html.mustache @@ -0,0 +1,51 @@ +

Your mytoken will expire soon. Here are the details:

+ + + + + {{#token-name}} + + + + + {{/token-name}} + + + + + + + + + + {{#additional-data}} + + + + + {{/additional-data}} +
Mytoken Name{{.}}
Mytoken Mom ID{{mom_id}}
Expires At{{expires_at}}
{{Key}}{{Value}}
+ + +

+To re-create a mytoken with similar properties, follow this link: {{recreate-url}} +

+ +

+If you created a new mytoken or do not need it anymore, you might want to unsubscribe from further expiration +notifications for this mytoken: + {{unsubscribe-exp-this-token-url}} +

+ +

+ If you want to manage the whole notification subscription, you can do so at: + {{management-url}} +

+ +Sincerly,
+the mytoken notification bot. diff --git a/internal/notifier/server/mailing/mailtemplates/templates/notification-exp.txt.mustache b/internal/notifier/server/mailing/mailtemplates/templates/notification-exp.txt.mustache new file mode 100644 index 00000000..f221ad10 --- /dev/null +++ b/internal/notifier/server/mailing/mailtemplates/templates/notification-exp.txt.mustache @@ -0,0 +1,13 @@ +Your mytoken will expire soon. Here are the details: + +{{txt-table}} + +To re-create a mytoken with similar properties, follow this link: {{{recreate-url}}} + +If you created a new mytoken or do not need it anymore, you might want to unsubscribe from further expiration +notifications for this mytoken: {{{unsubscribe-exp-this-token-url}}} + +If you want to manage the whole notification subscription, you can do so at: {{{management-url}}} + +Sincerly, +the mytoken notification bot. diff --git a/internal/notifier/server/mailing/mailtemplates/templates/notification-welcome.html.mustache b/internal/notifier/server/mailing/mailtemplates/templates/notification-welcome.html.mustache new file mode 100644 index 00000000..a84cd489 --- /dev/null +++ b/internal/notifier/server/mailing/mailtemplates/templates/notification-welcome.html.mustache @@ -0,0 +1,22 @@ +

You have been successfully subscribed to receive notifications for + {{#mtid}} + your mytoken {{#token-name}}'{{.}}'{{/token-name}} with the mom id '{{.}}' + {{/mtid}} + {{^mtid}} + all your mytokens + {{/mtid}} + on {{issuer-url}}. +

+

You will be notified about the following things:

+ + +

+ If you want to manage this notification subscription, you can do so here. +

+ +Sincerly,
+the mytoken notification bot. diff --git a/internal/notifier/server/mailing/mailtemplates/templates/notification-welcome.txt.mustache b/internal/notifier/server/mailing/mailtemplates/templates/notification-welcome.txt.mustache new file mode 100644 index 00000000..c774947e --- /dev/null +++ b/internal/notifier/server/mailing/mailtemplates/templates/notification-welcome.txt.mustache @@ -0,0 +1,10 @@ +You have been successfully subscribed to receive notifications for {{#mtid}}your mytoken {{#token-name}}'{{.}}'{{/token-name}} with the mom id '{{.}}'{{/mtid}}{{^mtid}}all your mytokens{{/mtid}} on {{issuer-url}}. +You will be notified about the following things: +{{#notification-classes}} +- {{Name}} +{{/notification-classes}} + +If you want to manage this notification subscription, you can do so at: {{management-url}} + +Sincerly, +the mytoken notification bot. diff --git a/internal/notifier/server/mailing/mailtemplates/templates/notification.html.mustache b/internal/notifier/server/mailing/mailtemplates/templates/notification.html.mustache new file mode 100644 index 00000000..4d8abe8d --- /dev/null +++ b/internal/notifier/server/mailing/mailtemplates/templates/notification.html.mustache @@ -0,0 +1,65 @@ +

Your mytoken was used for something we should notify you about. Here are the details:

+ + + + + {{#token-name}} + + + + + {{/token-name}} + + + + + + + + + + + + + {{#country}} + + + + + {{/country}} + + + + + + {{#event}} + + + + + {{/event}} + {{#comment}} + + + + + {{/comment}} + {{#additional-data}} + + + + + {{/additional-data}} +
Mytoken Name{{.}}
Mytoken Mom ID{{mom_id}}
IP{{ip}}
User-Agent{{user-agent}}
Location{{.}}
Notification Reason{{notification-class}}
Event{{.}}
Comment{{.}}
{{Key}}{{Value}}
+ +

+ If you want to manage this notification subscription, you can do so here. +

+ +Sincerly,
+the mytoken notification bot. diff --git a/internal/notifier/server/mailing/mailtemplates/templates/notification.txt.mustache b/internal/notifier/server/mailing/mailtemplates/templates/notification.txt.mustache new file mode 100644 index 00000000..c8732684 --- /dev/null +++ b/internal/notifier/server/mailing/mailtemplates/templates/notification.txt.mustache @@ -0,0 +1,8 @@ +Your mytoken was used for something we should notify you about. Here are the details: + +{{txt-table}} + +If you want to manage this notification subscription, you can do so at: {{management-url}} + +Sincerly, +the mytoken notification bot. diff --git a/internal/notifier/server/mailing/mailtemplates/templates/verify_mail.html.mustache b/internal/notifier/server/mailing/mailtemplates/templates/verify_mail.html.mustache new file mode 100644 index 00000000..b2fd2092 --- /dev/null +++ b/internal/notifier/server/mailing/mailtemplates/templates/verify_mail.html.mustache @@ -0,0 +1,13 @@ +

+ Your email address was entered to be used for mytoken notifications at {{ issuer }}.
+You must verify that this email address belongs to you. Please verify by clicking on the following link or copy it to +the address bar of your browser: +

+ +

+{{{ link }}} +

+ +Sincerly,
+the mytoken mail sender bot. diff --git a/internal/notifier/server/mailing/mailtemplates/templates/verify_mail.txt.mustache b/internal/notifier/server/mailing/mailtemplates/templates/verify_mail.txt.mustache new file mode 100644 index 00000000..ff5bf811 --- /dev/null +++ b/internal/notifier/server/mailing/mailtemplates/templates/verify_mail.txt.mustache @@ -0,0 +1,8 @@ +Your email address was entered to be used for mytoken notifications at {{ issuer }}. +You must verify that this email address belongs to you. Please verify by clicking on the following link or copy it to +the address bar of your browser: + +{{{ link }}} + +Sincerly, +the mytoken mail sender bot. \ No newline at end of file diff --git a/internal/notifier/server/notifier_server.go b/internal/notifier/server/notifier_server.go new file mode 100644 index 00000000..83b6a69a --- /dev/null +++ b/internal/notifier/server/notifier_server.go @@ -0,0 +1,102 @@ +package notifier + +import ( + "encoding/json" + "time" + + utils2 "github.com/oidc-mytoken/utils/utils" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/notifier/pkg" + "github.com/oidc-mytoken/server/internal/notifier/server/mailing" + "github.com/oidc-mytoken/server/internal/utils" + "github.com/oidc-mytoken/server/internal/utils/cache" +) + +// ServerPaths holds the server paths +var ServerPaths = Paths{ + Email: "/email", +} + +// Paths holds the server paths +type Paths struct { + Email string +} + +// Prefix prefixes all values in Paths with the specified prefix by url-combining them +func (p Paths) Prefix(prefix string) (out Paths) { + m := utils.StructToStringMapUsingJSONTags(p) + for k, v := range m { + m[k] = utils2.CombineURLPath(prefix, v) + } + jsonData, err := json.Marshal(m) + if err != nil { + panic(err) + } + if err = json.Unmarshal(jsonData, &out); err != nil { + panic(err) + } + return +} + +// InitStandalone initializes a standalone notifier server +func InitStandalone(mailConf config.MailNotificationConf) { + initCommon(mailConf) + cache.SetCache(cache.NewInternalCache(3 * time.Minute)) + startServer() +} + +// InitIntegrated initializes the integrated notifier "server" +func InitIntegrated() { + initCommon(config.Get().Features.Notifications.Mail) +} + +func initCommon(mailConf config.MailNotificationConf) { + mailing.Init(mailConf) + // TODO at this place we would spin up ws +} + +// HandleEmailRequest handles a pkg.EmailNotificationRequest +func HandleEmailRequest(req pkg.EmailNotificationRequest) error { + log.WithField("req", req).Info("Handling email request") + sID := req.ScheduleID + returnError := func(err error) error { + return err + } + if sID != "" { + if found, err := cache.Get(cache.ScheduledNotifications, sID, &struct{}{}); err == nil && found { + return nil + } else { + returnError = func(err error) error { + if err == nil { + return cache.Set(cache.ScheduledNotifications, sID, struct{}{}) + } + return err + } + } + } + if req.ICSInvite { + if err := mailing.ICSMailSender.Send(req.To, req.Subject, req.Text, req.Attachments...); err != nil { + log.WithError(err).Error("error while sending ics mail invite") + return returnError(err) + } + return returnError(nil) + } + sender := mailing.PlainTextMailSender + if req.PreferHTML { + sender = mailing.HTMLMailSender + } + if req.Template != "" { + if err := sender.SendTemplate(req.To, req.Subject, req.Template, req.BindingData); err != nil { + log.WithError(err).Error("error while sending templated mail") + return returnError(err) + } + return returnError(nil) + } + if err := sender.Send(req.To, req.Subject, req.Text, req.Attachments...); err != nil { + log.WithError(err).Error("error while sending mail") + return returnError(err) + } + return returnError(nil) +} diff --git a/internal/notifier/server/server.go b/internal/notifier/server/server.go new file mode 100644 index 00000000..a901ec9c --- /dev/null +++ b/internal/notifier/server/server.go @@ -0,0 +1,43 @@ +package notifier + +import ( + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/helmet" + "github.com/gofiber/fiber/v2/middleware/recover" + "github.com/gofiber/fiber/v2/middleware/requestid" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/notifier/pkg" +) + +var server *fiber.App + +var serverConfig = fiber.Config{ + ReadTimeout: 30 * time.Second, + WriteTimeout: 90 * time.Second, + IdleTimeout: 150 * time.Second, + ReadBufferSize: 32768, +} + +func startServer() { + server = fiber.New(serverConfig) + server.Use(recover.New()) + server.Use(helmet.New()) + server.Use(requestid.New()) + + server.Post( + ServerPaths.Email, func(ctx *fiber.Ctx) error { + var req pkg.EmailNotificationRequest + if err := ctx.BodyParser(&req); err != nil { + return err + } + if err := HandleEmailRequest(req); err != nil { + return err + } + return ctx.Status(fiber.StatusNoContent).Send(nil) + }, + ) + log.WithError(server.Listen(":40111")).Fatal() +} diff --git a/internal/oidc/authcode/authcode.go b/internal/oidc/authcode/authcode.go deleted file mode 100644 index 9878f57e..00000000 --- a/internal/oidc/authcode/authcode.go +++ /dev/null @@ -1,317 +0,0 @@ -package authcode - -import ( - "database/sql" - "fmt" - "net/url" - - "github.com/gofiber/fiber/v2" - "github.com/jmoiron/sqlx" - "github.com/oidc-mytoken/api/v0" - "github.com/oidc-mytoken/utils/httpclient" - "github.com/oidc-mytoken/utils/unixtime" - "github.com/oidc-mytoken/utils/utils" - "github.com/oidc-mytoken/utils/utils/jwtutils" - "github.com/oidc-mytoken/utils/utils/ternary" - "github.com/pkg/errors" - log "github.com/sirupsen/logrus" - - "github.com/oidc-mytoken/server/internal/config" - "github.com/oidc-mytoken/server/internal/db" - "github.com/oidc-mytoken/server/internal/db/dbrepo/accesstokenrepo" - "github.com/oidc-mytoken/server/internal/db/dbrepo/authcodeinforepo" - "github.com/oidc-mytoken/server/internal/db/dbrepo/authcodeinforepo/state" - "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo" - "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/transfercoderepo" - response "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" - "github.com/oidc-mytoken/server/internal/model" - mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" - "github.com/oidc-mytoken/server/internal/mytoken/restrictions" - "github.com/oidc-mytoken/server/internal/oidc/oidcreqres" - "github.com/oidc-mytoken/server/internal/oidc/pkce" - provider2 "github.com/oidc-mytoken/server/internal/oidc/provider" - "github.com/oidc-mytoken/server/internal/server/httpstatus" - "github.com/oidc-mytoken/server/internal/server/routes" - iutils "github.com/oidc-mytoken/server/internal/utils" - "github.com/oidc-mytoken/server/internal/utils/cookies" - "github.com/oidc-mytoken/server/internal/utils/errorfmt" - "github.com/oidc-mytoken/server/internal/utils/logger" -) - -// GetAuthorizationURL creates an authorization url -func GetAuthorizationURL( - rlog log.Ext1FieldLogger, tx *sqlx.Tx, provider model.Provider, oState *state.State, - restrictions restrictions.Restrictions, -) (string, error) { - rlog.Debug("Generating authorization url") - pkceCode := pkce.NewS256PKCE(utils.RandASCIIString(44)) - if err := db.RunWithinTransaction( - rlog, tx, func(tx *sqlx.Tx) error { - return authcodeinforepo.SetCodeVerifier(rlog, tx, oState, pkceCode.Verifier()) - }, - ); err != nil { - return "", err - } - pkceChallenge, _ := pkceCode.Challenge() - return provider.GetAuthorizationURL( - rlog, oState.State(), pkceChallenge, restrictions.GetScopes(), restrictions.GetAudiences(), - ) -} - -func trustedRedirectURI(redirectURI string) bool { - for _, r := range config.Get().Features.OIDCFlows.AuthCode.Web.TrustedRedirectsRegex { - if r.MatchString(redirectURI) { - return true - } - } - return false -} - -// StartAuthCodeFlow starts an authorization code flow -func StartAuthCodeFlow(ctx *fiber.Ctx, req *response.AuthCodeFlowRequest) *model.Response { - rlog := logger.GetRequestLogger(ctx) - rlog.Debug("Handle authcode") - native := req.Native() && config.Get().Features.Polling.Enabled - if !native && req.RedirectURI == "" { - return &model.Response{ - Status: fiber.StatusBadRequest, - Response: api.Error{ - Error: api.ErrorStrInvalidRequest, - ErrorDescription: "parameter redirect_uri must be given for client_type=web", - }, - } - } - req.Restrictions.ReplaceThisIP(ctx.IP()) - req.Restrictions.ClearUnsupportedKeys() - p := provider2.GetProvider(req.Issuer) - if p == nil { - return &model.Response{ - Status: fiber.StatusBadRequest, - Response: api.ErrorUnknownIssuer, - } - } - req.Issuer = p.Issuer() - exp := req.Restrictions.GetExpires() - if exp > 0 && exp < unixtime.Now() { - return &model.Response{ - Status: fiber.StatusBadRequest, - Response: model.BadRequestError("token would already be expired"), - } - } - - oState, consentCode := state.CreateState() - authFlowInfo := authcodeinforepo.AuthFlowInfo{ - AuthFlowInfoOut: authcodeinforepo.AuthFlowInfoOut{ - State: oState, - AuthCodeFlowRequest: *req, - }, - } - res := api.AuthCodeFlowResponse{ - ConsentURI: utils.CombineURLPath(routes.ConsentEndpoint, consentCode.String()), - } - if native { - poll := authFlowInfo.State.PollingCode(rlog) - authFlowInfo.PollingCode = transfercoderepo.CreatePollingCode(poll, req.ResponseType, req.MaxTokenLen) - res.PollingInfo = api.PollingInfo{ - PollingCode: poll, - PollingCodeExpiresIn: config.Get().Features.Polling.PollingCodeExpiresAfter, - PollingInterval: config.Get().Features.Polling.PollingInterval, - } - } - if err := authFlowInfo.Store(rlog, nil); err != nil { - rlog.Errorf("%s", errorfmt.Full(err)) - return model.ErrorToInternalServerErrorResponse(err) - } - if !native && trustedRedirectURI(req.RedirectURI) { - authURI, err := GetAuthorizationURL( - rlog, nil, p, state.NewState(consentCode.GetState()), - req.Restrictions.Restrictions, - ) - if err != nil { - rlog.Errorf("%s", errorfmt.Full(err)) - return model.ErrorToInternalServerErrorResponse(err) - } - return &model.Response{ - Status: httpstatus.StatusOKForward, - Response: map[string]string{ - "authorization_uri": authURI, - }, - } - } - return &model.Response{ - Status: fiber.StatusOK, - Response: res, - } -} - -// CodeExchange performs an oidc code exchange it creates the mytoken and stores it in the database -func CodeExchange( - rlog log.Ext1FieldLogger, oState *state.State, code string, networkData api.ClientMetaData, -) *model.Response { - rlog.Debug("Handle code exchange") - authInfo, err := authcodeinforepo.GetAuthFlowInfoByState(rlog, oState) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return &model.Response{ - Status: fiber.StatusBadRequest, - Response: api.ErrorStateMismatch, - } - } - rlog.Errorf("%s", errorfmt.Full(err)) - return model.ErrorToInternalServerErrorResponse(err) - } - p := provider2.GetProvider(authInfo.Issuer) - if p == nil { - return &model.Response{ - Status: fiber.StatusBadRequest, - Response: api.ErrorUnknownIssuer, - } - } - params := url.Values{} - params.Set("grant_type", "authorization_code") - params.Set("code_verifier", authInfo.CodeVerifier) - params.Set("code", code) - params.Set("redirect_uri", routes.RedirectURI) - params.Set("client_id", p.ClientID()) - - httpRes, err := p.AddClientAuthentication(httpclient.Do().R(), p.Endpoints().Token). - SetFormDataFromValues(params). - SetResult(&oidcreqres.OIDCTokenResponse{}). - SetError(&oidcreqres.OIDCErrorResponse{}). - Post(p.Endpoints().Token) - if err != nil { - rlog.Errorf("%s", errorfmt.Full(err)) - return model.ErrorToInternalServerErrorResponse(err) - } - if errRes, ok := httpRes.Error().(*oidcreqres.OIDCErrorResponse); ok && errRes != nil && errRes.Error != "" { - return &model.Response{ - Status: httpRes.RawResponse.StatusCode, - Response: model.OIDCError(errRes.Error, errRes.ErrorDescription), - } - } - oidcTokenRes, ok := httpRes.Result().(*oidcreqres.OIDCTokenResponse) - if !ok { - return &model.Response{ - Status: httpstatus.StatusOIDPError, - Response: model.ErrorWithoutDescription("could not unmarshal OP response"), - } - } - - if oidcTokenRes.RefreshToken == "" { - return &model.Response{ - Status: httpstatus.StatusOIDPError, - Response: api.ErrorNoRefreshToken, - } - } - scopes := authInfo.Restrictions.GetScopes() - scopesStr := oidcTokenRes.Scopes - if scopesStr != "" { - scopes = iutils.SplitIgnoreEmpty(scopesStr, " ") - authInfo.Restrictions.SetMaxScopes(scopes) // Update restrictions with correct scopes - } - audiences := authInfo.Restrictions.GetAudiences() - if tmp, ok := jwtutils.GetAudiencesFromJWT(rlog, oidcTokenRes.AccessToken); ok { - audiences = tmp - } - authInfo.Restrictions.SetMaxAudiences(audiences) // Update restrictions with correct audiences - - oidcSub, ok := jwtutils.GetStringFromJWT(rlog, oidcTokenRes.IDToken, "sub") - if !ok { - return &model.Response{ - Status: httpstatus.StatusOIDPError, - Response: model.ErrorWithoutDescription("could not get 'subject' from id token"), - } - } - var ste *mytokenrepo.MytokenEntry - if err = db.Transact( - rlog, func(tx *sqlx.Tx) error { - ste, err = createMytokenEntry(rlog, tx, authInfo, oidcTokenRes.RefreshToken, oidcSub, networkData) - if err != nil { - return err - } - at := accesstokenrepo.AccessToken{ - Token: oidcTokenRes.AccessToken, - IP: networkData.IP, - Comment: "Initial Access Token from authorization code flow", - Mytoken: ste.Token, - Scopes: scopes, - Audiences: audiences, - } - if err = at.Store(rlog, tx); err != nil { - return err - } - if authInfo.PollingCode { - jwt, err := ste.Token.ToJWT() - if err != nil { - return err - } - if err = transfercoderepo.LinkPollingCodeToMT( - rlog, tx, oState.PollingCode(rlog), jwt, ste.ID, - ); err != nil { - return err - } - } - return authcodeinforepo.DeleteAuthFlowInfoByState(rlog, tx, oState) - }, - ); err != nil { - rlog.Errorf("%s", errorfmt.Full(err)) - return model.ErrorToInternalServerErrorResponse(err) - } - if authInfo.PollingCode { - uri := "/native" - if authInfo.ApplicationName != "" { - uri = fmt.Sprintf("%s?application=%s", uri, authInfo.ApplicationName) - } - return &model.Response{ - Status: fiber.StatusSeeOther, - Response: uri, - } - } - res, err := ste.Token.ToTokenResponse(rlog, authInfo.ResponseType, authInfo.MaxTokenLen, networkData, "") - if err != nil { - rlog.Errorf("%s", errorfmt.Full(err)) - return model.ErrorToInternalServerErrorResponse(err) - } - var cookie *fiber.Cookie - if authInfo.ResponseType == model.ResponseTypeTransferCode { - cookie = cookies.TransferCodeCookie(res.TransferCode, int(res.ExpiresIn)) - } else { - cookie = cookies.MytokenCookie(res.Mytoken) - } - return &model.Response{ - Status: fiber.StatusSeeOther, - Response: ternary.IfNotEmptyOr(authInfo.RedirectURI, "/home"), - Cookies: []*fiber.Cookie{cookie}, - } -} - -func createMytokenEntry( - rlog log.Ext1FieldLogger, tx *sqlx.Tx, authFlowInfo *authcodeinforepo.AuthFlowInfoOut, rt, - oidcSub string, networkData api.ClientMetaData, -) (*mytokenrepo.MytokenEntry, error) { - var rot *api.Rotation - if authFlowInfo.Rotation != nil { - rot = &authFlowInfo.Rotation.Rotation - } - mt, err := mytoken.NewMytoken( - oidcSub, - authFlowInfo.Issuer, - authFlowInfo.Name, - authFlowInfo.Restrictions.Restrictions, - authFlowInfo.Capabilities.Capabilities, - rot, - unixtime.Now(), - ) - if err != nil { - return nil, err - } - mte := mytokenrepo.NewMytokenEntry(mt, authFlowInfo.Name, networkData) - mte.Token.AuthTime = unixtime.Now() - if err = mte.InitRefreshToken(rt); err != nil { - return nil, err - } - if err = mte.Store(rlog, tx, "Used grant_type oidc_flow authorization_code"); err != nil { - return nil, err - } - return mte, nil -} diff --git a/internal/oidc/authcode/authcode_init.go b/internal/oidc/authcode/authcode_init.go new file mode 100644 index 00000000..6faf8500 --- /dev/null +++ b/internal/oidc/authcode/authcode_init.go @@ -0,0 +1,128 @@ +package authcode + +import ( + "github.com/gofiber/fiber/v2" + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + "github.com/oidc-mytoken/utils/unixtime" + "github.com/oidc-mytoken/utils/utils" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/db/dbrepo/authcodeinforepo" + "github.com/oidc-mytoken/server/internal/db/dbrepo/authcodeinforepo/state" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/transfercoderepo" + response "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/model" + "github.com/oidc-mytoken/server/internal/mytoken/restrictions" + "github.com/oidc-mytoken/server/internal/oidc/pkce" + provider2 "github.com/oidc-mytoken/server/internal/oidc/provider" + "github.com/oidc-mytoken/server/internal/server/httpstatus" + "github.com/oidc-mytoken/server/internal/server/routes" + "github.com/oidc-mytoken/server/internal/utils/errorfmt" + "github.com/oidc-mytoken/server/internal/utils/logger" +) + +// GetAuthorizationURL creates an authorization url +func GetAuthorizationURL( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, provider model.Provider, oState *state.State, + restrictions restrictions.Restrictions, +) (string, error) { + rlog.Debug("Generating authorization url") + pkceCode := pkce.NewS256PKCE(utils.RandASCIIString(44)) + if err := db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + return authcodeinforepo.SetCodeVerifier(rlog, tx, oState, pkceCode.Verifier()) + }, + ); err != nil { + return "", err + } + pkceChallenge, _ := pkceCode.Challenge() + return provider.GetAuthorizationURL( + rlog, oState.State(), pkceChallenge, restrictions.GetScopes(), restrictions.GetAudiences(), + ) +} + +func trustedRedirectURI(redirectURI string) bool { + for _, r := range config.Get().Features.OIDCFlows.AuthCode.Web.TrustedRedirectsRegex { + if r.MatchString(redirectURI) { + return true + } + } + return false +} + +// StartAuthCodeFlow starts an authorization code flow +func StartAuthCodeFlow(ctx *fiber.Ctx, req *response.AuthCodeFlowRequest) *model.Response { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle authcode") + native := req.Native() && config.Get().Features.Polling.Enabled + if !native && req.RedirectURI == "" { + return &model.Response{ + Status: fiber.StatusBadRequest, + Response: api.Error{ + Error: api.ErrorStrInvalidRequest, + ErrorDescription: "parameter redirect_uri must be given for client_type=web", + }, + } + } + req.Restrictions.ReplaceThisIP(ctx.IP()) + req.Restrictions.ClearUnsupportedKeys() + p := provider2.GetProvider(req.Issuer) + if p == nil { + return &model.Response{ + Status: fiber.StatusBadRequest, + Response: api.ErrorUnknownIssuer, + } + } + req.Issuer = p.Issuer() + exp := req.Restrictions.GetExpires() + if exp > 0 && exp < unixtime.Now() { + return model.BadRequestErrorResponse("token would already be expired") + } + + oState, consentCode := state.CreateState() + authFlowInfo := authcodeinforepo.AuthFlowInfo{ + AuthFlowInfoOut: authcodeinforepo.AuthFlowInfoOut{ + State: oState, + AuthCodeFlowRequest: *req, + }, + } + res := api.AuthCodeFlowResponse{ + ConsentURI: utils.CombineURLPath(routes.ConsentEndpoint, consentCode.String()), + } + if native { + poll := authFlowInfo.State.PollingCode(rlog) + authFlowInfo.PollingCode = transfercoderepo.CreatePollingCode(poll, req.ResponseType, req.MaxTokenLen) + res.PollingInfo = api.PollingInfo{ + PollingCode: poll, + PollingCodeExpiresIn: config.Get().Features.Polling.PollingCodeExpiresAfter, + PollingInterval: config.Get().Features.Polling.PollingInterval, + } + } + if err := authFlowInfo.Store(rlog, nil); err != nil { + rlog.Errorf("%s", errorfmt.Full(err)) + return model.ErrorToInternalServerErrorResponse(err) + } + if !native && trustedRedirectURI(req.RedirectURI) { + authURI, err := GetAuthorizationURL( + rlog, nil, p, state.NewState(consentCode.GetState()), + req.Restrictions.Restrictions, + ) + if err != nil { + rlog.Errorf("%s", errorfmt.Full(err)) + return model.ErrorToInternalServerErrorResponse(err) + } + return &model.Response{ + Status: httpstatus.StatusOKForward, + Response: map[string]string{ + "authorization_uri": authURI, + }, + } + } + return &model.Response{ + Status: fiber.StatusOK, + Response: res, + } +} diff --git a/internal/oidc/authcode/codeexchange.go b/internal/oidc/authcode/codeexchange.go new file mode 100644 index 00000000..27a74d34 --- /dev/null +++ b/internal/oidc/authcode/codeexchange.go @@ -0,0 +1,361 @@ +package authcode + +import ( + "database/sql" + "fmt" + "net/url" + "slices" + + "github.com/gofiber/fiber/v2" + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + "github.com/oidc-mytoken/utils/httpclient" + "github.com/oidc-mytoken/utils/unixtime" + "github.com/oidc-mytoken/utils/utils/jwtutils" + "github.com/oidc-mytoken/utils/utils/ternary" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/db/dbrepo/accesstokenrepo" + "github.com/oidc-mytoken/server/internal/db/dbrepo/authcodeinforepo" + "github.com/oidc-mytoken/server/internal/db/dbrepo/authcodeinforepo/state" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/transfercoderepo" + "github.com/oidc-mytoken/server/internal/db/dbrepo/userrepo" + "github.com/oidc-mytoken/server/internal/db/notificationsrepo" + "github.com/oidc-mytoken/server/internal/db/profilerepo" + "github.com/oidc-mytoken/server/internal/model" + mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" + "github.com/oidc-mytoken/server/internal/mytoken/restrictions" + "github.com/oidc-mytoken/server/internal/oidc/oidcreqres" + provider2 "github.com/oidc-mytoken/server/internal/oidc/provider" + "github.com/oidc-mytoken/server/internal/oidc/userinfo" + "github.com/oidc-mytoken/server/internal/server/httpstatus" + "github.com/oidc-mytoken/server/internal/server/routes" + iutils "github.com/oidc-mytoken/server/internal/utils" + "github.com/oidc-mytoken/server/internal/utils/cookies" + "github.com/oidc-mytoken/server/internal/utils/errorfmt" +) + +// CodeExchange performs an OIDC code exchange, creates the mytoken, and stores it in the database. +func CodeExchange( + rlog log.Ext1FieldLogger, oState *state.State, code string, networkData api.ClientMetaData, +) (*model.Response, string) { + rlog.Debug("Handle code exchange") + authInfo, errRes := fetchAuthInfo(rlog, oState) + if errRes != nil { + return errRes, "" + } + p, errRes := fetchProvider(authInfo.Issuer) + if errRes != nil { + return errRes, "" + } + oidcTokenRes, errRes := exchangeCodeForToken(rlog, p, authInfo, code) + if errRes != nil { + return errRes, "" + } + + updateAuthInfoScopesAndAudiences(rlog, authInfo, oidcTokenRes) + userInfos, errRes := fetchUserInfos(rlog, p, oidcTokenRes) + if errRes != nil { + return errRes, "" + } + enforcedRestrictionsConfig := provider2.GetEnforcedRestrictionsByIssuer(p.Issuer()) + enforcedRestrictions, forbiddenByEnforced, errRes := getEnforcedRestrictionTemplate( + enforcedRestrictionsConfig, userInfos, oidcTokenRes.AccessToken, + ) + if errRes != nil { + var additionlErrHTML string + if forbiddenByEnforced { + additionlErrHTML = enforcedRestrictionsConfig.HelpHTMLText + } + return errRes, additionlErrHTML + } + ste, restrictionsWhereOK, errRes := storeTokenInDatabase( + rlog, oState, authInfo, enforcedRestrictions, oidcTokenRes, userInfos, networkData, + ) + if errRes != nil { + return errRes, "" + } + var additionlErrHTML string + if !restrictionsWhereOK { + additionlErrHTML = enforcedRestrictionsConfig.HelpHTMLText + } + return generateResponse(rlog, authInfo, ste, networkData), additionlErrHTML +} + +func fetchAuthInfo(rlog log.Ext1FieldLogger, oState *state.State) (*authcodeinforepo.AuthFlowInfoOut, *model.Response) { + authInfo, err := authcodeinforepo.GetAuthFlowInfoByState(rlog, nil, oState) + if err == nil { + return authInfo, nil + } + if errors.Is(err, sql.ErrNoRows) { + return nil, &model.Response{ + Status: fiber.StatusBadRequest, + Response: api.ErrorStateMismatch, + } + } + rlog.Errorf("%s", errorfmt.Full(err)) + return nil, model.ErrorToInternalServerErrorResponse(err) +} + +func fetchProvider(issuer string) (model.Provider, *model.Response) { + p := provider2.GetProvider(issuer) + if p == nil { + return nil, &model.Response{ + Status: fiber.StatusBadRequest, + Response: api.ErrorUnknownIssuer, + } + } + return p, nil +} + +func exchangeCodeForToken( + rlog log.Ext1FieldLogger, p model.Provider, authInfo *authcodeinforepo.AuthFlowInfoOut, + code string, +) (*oidcreqres.OIDCTokenResponse, *model.Response) { + params := url.Values{} + params.Set("grant_type", "authorization_code") + params.Set("code_verifier", authInfo.CodeVerifier) + params.Set("code", code) + params.Set("redirect_uri", routes.RedirectURI) + params.Set("client_id", p.ClientID()) + + httpRes, err := p.AddClientAuthentication(httpclient.Do().R(), p.Endpoints().Token). + SetFormDataFromValues(params). + SetResult(&oidcreqres.OIDCTokenResponse{}). + SetError(&oidcreqres.OIDCErrorResponse{}). + Post(p.Endpoints().Token) + if err != nil { + rlog.Errorf("%s", errorfmt.Full(err)) + return nil, model.ErrorToInternalServerErrorResponse(err) + } + + if errRes, ok := httpRes.Error().(*oidcreqres.OIDCErrorResponse); ok && errRes != nil && errRes.Error != "" { + return nil, &model.Response{ + Status: httpRes.RawResponse.StatusCode, + Response: model.OIDCError(errRes.Error, errRes.ErrorDescription), + } + } + + oidcTokenRes, ok := httpRes.Result().(*oidcreqres.OIDCTokenResponse) + if !ok { + return nil, &model.Response{ + Status: httpstatus.StatusOIDPError, + Response: model.ErrorWithoutDescription("could not unmarshal OP response"), + } + } + + if oidcTokenRes.RefreshToken == "" { + return nil, &model.Response{ + Status: httpstatus.StatusOIDPError, + Response: api.ErrorNoRefreshToken, + } + } + + return oidcTokenRes, nil +} + +func updateAuthInfoScopesAndAudiences( + rlog log.Ext1FieldLogger, authInfo *authcodeinforepo.AuthFlowInfoOut, oidcTokenRes *oidcreqres.OIDCTokenResponse, +) { + if scopesStr := oidcTokenRes.Scopes; scopesStr != "" { + scopes := iutils.SplitIgnoreEmpty(scopesStr, " ") + authInfo.Restrictions.SetMaxScopes(scopes) + } + + audiences := authInfo.Restrictions.GetAudiences() + if tmp, ok := jwtutils.GetAudiencesFromJWT(rlog, oidcTokenRes.AccessToken); ok { + audiences = tmp + } + authInfo.Restrictions.SetMaxAudiences(audiences) +} + +func fetchUserInfos(rlog log.Ext1FieldLogger, p model.Provider, oidcTokenRes *oidcreqres.OIDCTokenResponse) ( + map[string]any, *model.Response, +) { + attrs := []string{ + "sub", + "email", + "email_verified", + } + enforcedRestrictionsConf := provider2.GetEnforcedRestrictionsByIssuer(p.Issuer()) + if enforcedRestrictionsConf.Enabled { + for endpoint, claimName := range enforcedRestrictionsConf.ClaimSources { + if slices.Contains(enforcedRestrictionsClaimSourcesUserInfoKeys, endpoint) { + attrs = append(attrs, claimName) + } + } + } + + userInfos := userinfo.GetUserAttributes(rlog, oidcTokenRes, p, attrs...) + oidcSub := iutils.GetStringFromAnyMap(userInfos, "sub") + if oidcSub == "" { + return nil, &model.Response{ + Status: httpstatus.StatusOIDPError, + Response: model.ErrorWithoutDescription("could not get 'subject' from id token"), + } + } + + return userInfos, nil +} + +func storeTokenInDatabase( + rlog log.Ext1FieldLogger, oState *state.State, authInfo *authcodeinforepo.AuthFlowInfoOut, + enforcedRestrictions string, oidcTokenRes *oidcreqres.OIDCTokenResponse, userInfos map[string]any, + networkData api.ClientMetaData, +) (*mytokenrepo.MytokenEntry, bool, *model.Response) { + var ste *mytokenrepo.MytokenEntry + var restrictionsWhereOK bool + err := db.Transact( + rlog, func(tx *sqlx.Tx) error { + var err error + ste, restrictionsWhereOK, err = createMytokenEntry( + rlog, tx, authInfo, enforcedRestrictions, oidcTokenRes.RefreshToken, + iutils.GetStringFromAnyMap(userInfos, "sub"), networkData, + ) + if err != nil { + return err + } + if err = storeAccessToken( + rlog, tx, oidcTokenRes.AccessToken, networkData, ste, authInfo.Restrictions.GetScopes(), + authInfo.Restrictions.GetAudiences(), + ); err != nil { + return err + } + if authInfo.PollingCode { + if err = linkPollingCodeToMytoken(rlog, tx, oState, ste); err != nil { + return err + } + } + if err = updateUserMailInfo(rlog, tx, ste.ID, userInfos); err != nil { + return err + } + return authcodeinforepo.DeleteAuthFlowInfoByState(rlog, tx, oState) + }, + ) + if err != nil { + rlog.Errorf("%s", errorfmt.Full(err)) + return nil, restrictionsWhereOK, model.ErrorToInternalServerErrorResponse(err) + } + return ste, restrictionsWhereOK, nil +} + +func storeAccessToken( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, accessToken string, networkData api.ClientMetaData, + ste *mytokenrepo.MytokenEntry, scopes []string, audiences []string, +) error { + at := accesstokenrepo.AccessToken{ + Token: accessToken, + IP: networkData.IP, + Comment: "Initial Access Token from authorization code flow", + Mytoken: ste.Token, + Scopes: scopes, + Audiences: audiences, + } + return at.Store(rlog, tx) +} + +func linkPollingCodeToMytoken( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, oState *state.State, ste *mytokenrepo.MytokenEntry, +) error { + jwt, err := ste.Token.ToJWT() + if err != nil { + return err + } + return transfercoderepo.LinkPollingCodeToMT(rlog, tx, oState.PollingCode(rlog), jwt, ste.ID) +} + +func updateUserMailInfo(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mytokenID mtid.MTID, userInfos map[string]any) error { + mailInfo, err := userrepo.GetMail(rlog, tx, mytokenID) + if _, err = db.ParseError(err); err != nil { + return err + } + if mailInfo.Mail.Valid { + return nil + } + // no valid mail in db + mail := iutils.GetStringFromAnyMap(userInfos, "email") + mailVerified := iutils.GetBoolFromAnyMap(userInfos, "email_verified") + return userrepo.SetEmail(rlog, tx, mytokenID, mail, mailVerified) +} + +func generateResponse( + rlog log.Ext1FieldLogger, authInfo *authcodeinforepo.AuthFlowInfoOut, ste *mytokenrepo.MytokenEntry, + networkData api.ClientMetaData, +) *model.Response { + if authInfo.PollingCode { + uri := "/native" + if authInfo.ApplicationName != "" { + uri = fmt.Sprintf("%s?application=%s", uri, authInfo.ApplicationName) + } + return &model.Response{ + Status: fiber.StatusSeeOther, + Response: uri, + } + } + res, err := ste.Token.ToTokenResponse(rlog, authInfo.ResponseType, authInfo.MaxTokenLen, networkData, "") + if err != nil { + rlog.Errorf("%s", errorfmt.Full(err)) + return model.ErrorToInternalServerErrorResponse(err) + } + var cookie *fiber.Cookie + if authInfo.ResponseType == model.ResponseTypeTransferCode { + cookie = cookies.TransferCodeCookie(res.TransferCode, int(res.ExpiresIn)) + } else { + cookie = cookies.MytokenCookie(res.Mytoken) + } + return &model.Response{ + Status: fiber.StatusSeeOther, + Response: ternary.IfNotEmptyOr(authInfo.RedirectURI, "/home"), + Cookies: []*fiber.Cookie{cookie}, + } +} + +func createMytokenEntry( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, authFlowInfo *authcodeinforepo.AuthFlowInfoOut, + enforcedRestrictionsTemplate, rt, oidcSub string, networkData api.ClientMetaData, +) (*mytokenrepo.MytokenEntry, bool, error) { + var rot *api.Rotation + if authFlowInfo.Rotation != nil { + rot = &authFlowInfo.Rotation.Rotation + } + restr := authFlowInfo.Restrictions.Restrictions + restrictionsWhereOK := true + if enforcedRestrictionsTemplate != "" { + parser := profilerepo.NewDBProfileParser(rlog) + enforced, err := parser.ParseRestrictionsTemplate([]byte(enforcedRestrictionsTemplate)) + if err != nil { + return nil, false, err + } + restr, restrictionsWhereOK = restrictions.Tighten(rlog, restrictions.NewRestrictionsFromAPI(enforced), restr) + } + mt, err := mytoken.NewMytoken( + oidcSub, + authFlowInfo.Issuer, + authFlowInfo.Name, + restr, + authFlowInfo.Capabilities.Capabilities, + rot, + unixtime.Now(), + ) + if err != nil { + return nil, restrictionsWhereOK, err + } + mte := mytokenrepo.NewMytokenEntry(mt, authFlowInfo.Name, networkData) + mte.Token.AuthTime = unixtime.Now() + if err = mte.InitRefreshToken(rt); err != nil { + return nil, restrictionsWhereOK, err + } + if err = mte.Store(rlog, tx, "Used grant_type oidc_flow authorization_code"); err != nil { + return nil, restrictionsWhereOK, err + } + if err = notificationsrepo.ScheduleExpirationNotificationsIfNeeded( + rlog, tx, mte.ID, mte.Token.ExpiresAt, mte.Token.IssuedAt, + ); err != nil { + return nil, restrictionsWhereOK, err + } + return mte, restrictionsWhereOK, nil +} diff --git a/internal/oidc/authcode/enforced_restrictions.go b/internal/oidc/authcode/enforced_restrictions.go new file mode 100644 index 00000000..fed93b51 --- /dev/null +++ b/internal/oidc/authcode/enforced_restrictions.go @@ -0,0 +1,115 @@ +package authcode + +import ( + "slices" + + "github.com/gofiber/fiber/v2" + "github.com/oidc-mytoken/api/v0" + + "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/model" + "github.com/oidc-mytoken/server/internal/oidc/userinfo" + "github.com/oidc-mytoken/server/internal/server/httpstatus" +) + +var enforcedRestrictionsClaimSourcesUserInfoKeys = []string{ + "op", + "issuer", + "default", + "userinfo", +} + +func getEnforcedRestrictionTemplate( + conf config.EnforcedRestrictionsConf, + userInfos map[string]any, at string, +) ( + string, bool, *model.Response, +) { + if !conf.Enabled { + return "", false, nil + } + + var claimValue any + var found bool + for endpoint, claimName := range conf.ClaimSources { + if slices.Contains(enforcedRestrictionsClaimSourcesUserInfoKeys, endpoint) { + claimValue, found = userInfos[claimName] + } else { + userAttributes, errRes, err := userinfo.Get(endpoint, at) + if err != nil || errRes != nil || userAttributes == nil { + continue + } + claimValue, found = userAttributes[claimName] + } + if found { + break + } + } + if !found { + return handleDefaultEnforcedRestrictionsTemplate(conf) + } + + switch e := claimValue.(type) { + case string: + return matchEnforcedRestrictionsTemplate(conf, e) + case []any: + strEntitlements := make([]string, len(e)) + for i, entitlement := range e { + var ok bool + strEntitlements[i], ok = entitlement.(string) + if !ok { + return "", false, &model.Response{ + Status: httpstatus.StatusOIDPError, + Response: model.OIDCError( + "invalid_claim_source_response", + "cannot understand claim type", + ), + } + } + } + return matchAnyEnforcedRestrictionsTemplate(conf, strEntitlements) + case []string: + return matchAnyEnforcedRestrictionsTemplate(conf, e) + default: + return "", false, &model.Response{ + Status: httpstatus.StatusOIDPError, + Response: model.OIDCError( + "invalid_claim_source_response", + "cannot understand claim type", + ), + } + } +} + +func handleDefaultEnforcedRestrictionsTemplate(conf config.EnforcedRestrictionsConf) (string, bool, *model.Response) { + if conf.ForbidOnDefault { + return "", true, &model.Response{ + Status: fiber.StatusForbidden, + Response: api.Error{ + Error: api.ErrorStrAccessDenied, + ErrorDescription: "you do not have the required attributes to use this service", + }, + } + } + return conf.DefaultTemplate, false, nil +} + +func matchEnforcedRestrictionsTemplate(conf config.EnforcedRestrictionsConf, entitlement string) ( + string, bool, *model.Response, +) { + if template, ok := conf.Mapping[entitlement]; ok { + return template, false, nil + } + return handleDefaultEnforcedRestrictionsTemplate(conf) +} + +func matchAnyEnforcedRestrictionsTemplate(conf config.EnforcedRestrictionsConf, entitlements []string) ( + string, bool, *model.Response, +) { + for k, v := range conf.Mapping { + if slices.Contains(entitlements, k) { + return v, false, nil + } + } + return handleDefaultEnforcedRestrictionsTemplate(conf) +} diff --git a/internal/oidc/oidcfed/discovery.go b/internal/oidc/oidcfed/discovery.go index d72db0ec..69e40349 100644 --- a/internal/oidc/oidcfed/discovery.go +++ b/internal/oidc/oidcfed/discovery.go @@ -6,15 +6,15 @@ import ( "github.com/oidc-mytoken/api/v0" log "github.com/sirupsen/logrus" - oidcfed "github.com/zachmann/go-oidcfed/pkg" + oidfed "github.com/zachmann/go-oidfed/pkg" "github.com/oidc-mytoken/server/internal/config" ) -var discoverer = oidcfed.FilterableVerifiedChainsOPDiscoverer{ - Filters: []oidcfed.OPDiscoveryFilter{ - oidcfed.OPDiscoveryFilterSupportedGrantTypesIncludes("refresh_token"), - oidcfed.OPDiscoveryFilterSupportedScopesIncludes("offline_access"), +var discoverer = oidfed.FilterableVerifiedChainsOPDiscoverer{ + Filters: []oidfed.OPDiscoveryFilter{ + oidfed.OPDiscoveryFilterSupportedGrantTypesIncludes("refresh_token"), + oidfed.OPDiscoveryFilterSupportedScopesIncludes("offline_access"), }, } diff --git a/internal/oidc/oidcfed/federation.go b/internal/oidc/oidcfed/federation.go index dd8bc1bb..bcb79d1b 100644 --- a/internal/oidc/oidcfed/federation.go +++ b/internal/oidc/oidcfed/federation.go @@ -1,7 +1,7 @@ package oidcfed import ( - oidcfedcache "github.com/zachmann/go-oidcfed/pkg/cache" + oidfedcache "github.com/zachmann/go-oidfed/pkg/cache" "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/endpoints/federation" @@ -16,7 +16,7 @@ func Init() { } jws.LoadFederationKey() jws.LoadOIDCSigningKey() - oidcfedcache.SetCache(cache.SubCache(cache.FederationLib)) + oidfedcache.SetCache(cache.SubCache(cache.FederationLib)) Discovery() federation.InitEntityConfiguration() } diff --git a/internal/oidc/oidcfed/fedprovider.go b/internal/oidc/oidcfed/fedprovider.go index 152ab364..07e69975 100644 --- a/internal/oidc/oidcfed/fedprovider.go +++ b/internal/oidc/oidcfed/fedprovider.go @@ -5,7 +5,7 @@ import ( "github.com/go-resty/resty/v2" log "github.com/sirupsen/logrus" - fed "github.com/zachmann/go-oidcfed/pkg" + fed "github.com/zachmann/go-oidfed/pkg" "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/model" diff --git a/internal/oidc/oidcfed/trust.go b/internal/oidc/oidcfed/trust.go index 2c2937a9..d551c86c 100644 --- a/internal/oidc/oidcfed/trust.go +++ b/internal/oidc/oidcfed/trust.go @@ -1,7 +1,7 @@ package oidcfed import ( - fed "github.com/zachmann/go-oidcfed/pkg" + fed "github.com/zachmann/go-oidfed/pkg" ) // getOPMetadata returns the fed.OpenIDProviderMetadata for an oidcfed issuer diff --git a/internal/oidc/oidcreqres/request.go b/internal/oidc/oidcreqres/request.go index f6deb51f..3b6981f1 100644 --- a/internal/oidc/oidcreqres/request.go +++ b/internal/oidc/oidcreqres/request.go @@ -4,6 +4,8 @@ import ( "net/url" "strings" + "github.com/coreos/go-oidc/v3/oidc" + "github.com/oidc-mytoken/server/internal/model" iutils "github.com/oidc-mytoken/server/internal/utils" ) @@ -35,13 +37,17 @@ func NewRefreshRequest(rt string, aud *model.AudienceConf) *RefreshRequest { } } -// ToURLValues formats the RefreshRequest as a url.Values +// ToURLValues formats the RefreshRequest as an url.Values func (r *RefreshRequest) ToURLValues() url.Values { m := make(url.Values) m["grant_type"] = []string{r.GrantType} m["refresh_token"] = []string{r.RefreshToken} if r.Scopes != "" { - m["scope"] = []string{r.Scopes} + scopes := r.Scopes + if !strings.Contains(scopes, oidc.ScopeOfflineAccess) { + scopes += " " + oidc.ScopeOfflineAccess + } + m["scope"] = []string{scopes} } if len(r.Audiences) > 0 && r.Audiences[0] != "" { if r.spaceDelimited { diff --git a/internal/oidc/provider/providerrepo.go b/internal/oidc/provider/providerrepo.go index f37bb6f1..d9688204 100644 --- a/internal/oidc/provider/providerrepo.go +++ b/internal/oidc/provider/providerrepo.go @@ -30,3 +30,12 @@ func GetProvider(issuer string) model.Provider { } return nil } + +// GetEnforcedRestrictionsByIssuer returns the config.EnforcedRestrictionsConf for the passed issuer +func GetEnforcedRestrictionsByIssuer(issuer string) (c config.EnforcedRestrictionsConf) { + if p, ok := fileProviderByIssuer[issuer]; ok { + pp := p.(SimpleProvider) + c = pp.EnforcedRestrictions + } + return +} diff --git a/internal/oidc/provider/simple_provider.go b/internal/oidc/provider/simple_provider.go index 90edfa70..db80ac3e 100644 --- a/internal/oidc/provider/simple_provider.go +++ b/internal/oidc/provider/simple_provider.go @@ -20,7 +20,7 @@ import ( // SimpleProvider implements the Provider interface for normal OIDC providers with a registered client type SimpleProvider struct { - config.ProviderConf + *config.ProviderConf } // Name implements the Provider interface diff --git a/internal/oidc/refresh/refresh.go b/internal/oidc/refresh/refresh.go index 01cd272f..2f07be3a 100644 --- a/internal/oidc/refresh/refresh.go +++ b/internal/oidc/refresh/refresh.go @@ -1,6 +1,7 @@ package refresh import ( + "github.com/jmoiron/sqlx" "github.com/oidc-mytoken/utils/httpclient" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -12,20 +13,20 @@ import ( ) // UpdateChangedRT is a function that should update a refresh token, it takes the old value as well as the new one -type UpdateChangedRT func(rlog log.Ext1FieldLogger, tokenID mtid.MTID, newRT, mytoken string) error +type UpdateChangedRT func(rlog log.Ext1FieldLogger, tx *sqlx.Tx, tokenID mtid.MTID, newRT, mytoken string) error // DoFlowWithoutUpdate uses a refresh token to obtain a new access token; if the refresh token changes, this is ignored func DoFlowWithoutUpdate( rlog log.Ext1FieldLogger, provider model.Provider, tokenID mtid.MTID, mytoken, rt, scopes string, audiences []string, ) (*oidcreqres.OIDCTokenResponse, *oidcreqres.OIDCErrorResponse, error) { - return DoFlowAndUpdate(rlog, provider, tokenID, mytoken, rt, scopes, audiences, nil) + return DoFlowAndUpdate(rlog, nil, provider, tokenID, mytoken, rt, scopes, audiences, nil) } // DoFlowAndUpdate uses a refresh token to obtain a new access token; if the refresh token changes, the // UpdateChangedRT function is used to update the refresh token func DoFlowAndUpdate( - rlog log.Ext1FieldLogger, provider model.Provider, tokenID mtid.MTID, mytoken, rt, scopes string, + rlog log.Ext1FieldLogger, tx *sqlx.Tx, provider model.Provider, tokenID mtid.MTID, mytoken, rt, scopes string, audiences []string, updateFnc UpdateChangedRT, ) (*oidcreqres.OIDCTokenResponse, *oidcreqres.OIDCErrorResponse, error) { @@ -49,7 +50,7 @@ func DoFlowAndUpdate( return nil, nil, errors.New("could not unmarshal oidc response") } if res.RefreshToken != "" && res.RefreshToken != rt && updateFnc != nil { - if err = updateFnc(rlog, tokenID, res.RefreshToken, mytoken); err != nil { + if err = updateFnc(rlog, tx, tokenID, res.RefreshToken, mytoken); err != nil { return res, nil, err } } @@ -59,12 +60,12 @@ func DoFlowAndUpdate( // DoFlowAndUpdateDB uses a refresh token to obtain a new access token; if the refresh token changes, it is // updated in the database func DoFlowAndUpdateDB( - rlog log.Ext1FieldLogger, provider model.Provider, tokenID mtid.MTID, mytoken, rt, scopes string, + rlog log.Ext1FieldLogger, tx *sqlx.Tx, provider model.Provider, tokenID mtid.MTID, mytoken, rt, scopes string, audiences []string, ) (*oidcreqres.OIDCTokenResponse, *oidcreqres.OIDCErrorResponse, error) { - return DoFlowAndUpdate(rlog, provider, tokenID, mytoken, rt, scopes, audiences, updateChangedRTInDB) + return DoFlowAndUpdate(rlog, tx, provider, tokenID, mytoken, rt, scopes, audiences, updateChangedRTInDB) } -func updateChangedRTInDB(rlog log.Ext1FieldLogger, tokenID mtid.MTID, newRT, mytoken string) error { - return cryptstore.UpdateRefreshToken(rlog, nil, tokenID, newRT, mytoken) +func updateChangedRTInDB(rlog log.Ext1FieldLogger, tx *sqlx.Tx, tokenID mtid.MTID, newRT, mytoken string) error { + return cryptstore.UpdateRefreshToken(rlog, tx, tokenID, newRT, mytoken) } diff --git a/internal/oidc/userinfo/userinfo.go b/internal/oidc/userinfo/userinfo.go new file mode 100644 index 00000000..d6ff04f7 --- /dev/null +++ b/internal/oidc/userinfo/userinfo.go @@ -0,0 +1,93 @@ +package userinfo + +import ( + "github.com/oidc-mytoken/utils/httpclient" + "github.com/oidc-mytoken/utils/utils/jwtutils" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/model" + "github.com/oidc-mytoken/server/internal/oidc/oidcreqres" +) + +// Get obtains the userinfo response from the passed endpoint +func Get( + endpoint string, at string, +) (map[string]any, *oidcreqres.OIDCErrorResponse, error) { + + httpRes, err := httpclient.Do().R(). + SetAuthToken(at). + SetResult(make(map[string]any)). + SetError(&oidcreqres.OIDCErrorResponse{}). + Get(endpoint) + if err != nil { + return nil, nil, errors.WithStack(err) + } + if errRes, ok := httpRes.Error().(*oidcreqres.OIDCErrorResponse); ok && errRes != nil && errRes.Error != "" { + errRes.Status = httpRes.RawResponse.StatusCode + return nil, errRes, nil + } + res, ok := httpRes.Result().(map[string]any) + if !ok { + return nil, nil, errors.New("could not unmarshal userinfo response") + } + return res, nil, nil +} + +// GetFromProvider obtains the userinfo response from the model.Provider's +// userinfo endpoint +func GetFromProvider( + provider model.Provider, at string, +) (map[string]any, *oidcreqres.OIDCErrorResponse, error) { + return Get(provider.Endpoints().Userinfo, at) +} + +func getNonNilUserInfoMap(provider model.Provider, at string) map[string]any { + userinfoRes, errRes, err := GetFromProvider(provider, at) + if err == nil && errRes == nil { + return userinfoRes + } + return map[string]any{} +} + +func getNonNilJWTMap(rlog log.Ext1FieldLogger, token string) map[string]any { + attrs := jwtutils.GetFromJWT(rlog, token) + if attrs == nil { + attrs = map[string]any{} + } + return attrs +} + +// GetUserAttributes returns user attributes for the passed claim names by searching the id token, JWT AT, +// and userinfo endpoint +func GetUserAttributes( + rlog log.Ext1FieldLogger, oidcTokenRes *oidcreqres.OIDCTokenResponse, provider model.Provider, + attributes ...string, +) map[string]any { + var atTokenAttrs map[string]any + var userInfoAttrs map[string]any + idTokenAttrs := getNonNilJWTMap(rlog, oidcTokenRes.IDToken) + + finalAttrs := make(map[string]any, len(attributes)) + for _, attr := range attributes { + if v, ok := idTokenAttrs[attr]; ok { + finalAttrs[attr] = v + continue + } + if atTokenAttrs == nil { + atTokenAttrs = getNonNilJWTMap(rlog, oidcTokenRes.AccessToken) + } + if v, ok := atTokenAttrs[attr]; ok { + finalAttrs[attr] = v + continue + } + if userInfoAttrs == nil { + userInfoAttrs = getNonNilUserInfoMap(provider, oidcTokenRes.AccessToken) + } + if v, ok := userInfoAttrs[attr]; ok { + finalAttrs[attr] = v + // continue + } + } + return finalAttrs +} diff --git a/internal/server/api.go b/internal/server/api.go index 66bad08f..d965b092 100644 --- a/internal/server/api.go +++ b/internal/server/api.go @@ -6,9 +6,12 @@ import ( "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/endpoints/guestmode" + "github.com/oidc-mytoken/server/internal/endpoints/notification" + "github.com/oidc-mytoken/server/internal/endpoints/notification/calendar" "github.com/oidc-mytoken/server/internal/endpoints/profiles" "github.com/oidc-mytoken/server/internal/endpoints/revocation" "github.com/oidc-mytoken/server/internal/endpoints/settings" + "github.com/oidc-mytoken/server/internal/endpoints/settings/email" "github.com/oidc-mytoken/server/internal/endpoints/settings/grants" "github.com/oidc-mytoken/server/internal/endpoints/settings/grants/ssh" "github.com/oidc-mytoken/server/internal/endpoints/token/access" @@ -27,29 +30,68 @@ func addAPIRoutes(s fiber.Router) { func addAPIvXRoutes(s fiber.Router, version int) { apiPaths := paths.GetAPIPaths(version) - s.Post(apiPaths.MytokenEndpoint, mytoken.HandleMytokenEndpoint) - s.Post(apiPaths.AccessTokenEndpoint, access.HandleAccessTokenEndpoint) + s.Post(apiPaths.MytokenEndpoint, toFiberHandler(mytoken.HandleMytokenEndpoint)) + s.Post(apiPaths.AccessTokenEndpoint, toFiberHandler(access.HandleAccessTokenEndpoint)) if config.Get().Features.TokenRevocation.Enabled { - s.Post(apiPaths.RevocationEndpoint, revocation.HandleRevoke) + s.Post(apiPaths.RevocationEndpoint, toFiberHandler(revocation.HandleRevoke)) } if config.Get().Features.TransferCodes.Enabled { - s.Post(apiPaths.TokenTransferEndpoint, mytoken.HandleCreateTransferCodeForExistingMytoken) + s.Post(apiPaths.TokenTransferEndpoint, toFiberHandler(mytoken.HandleCreateTransferCodeForExistingMytoken)) } if config.Get().Features.TokenInfo.Enabled { - s.Post(apiPaths.TokenInfoEndpoint, tokeninfo.HandleTokenInfo) + s.Post(apiPaths.TokenInfoEndpoint, toFiberHandler(tokeninfo.HandleTokenInfo)) } - s.Get(apiPaths.UserSettingEndpoint, settings.HandleSettings) + s.Get(apiPaths.UserSettingEndpoint, toFiberHandler(settings.HandleSettings)) grantPath := utils.CombineURLPath(apiPaths.UserSettingEndpoint, "grants") - s.Get(grantPath, grants.HandleListGrants) - s.Post(grantPath, grants.HandleEnableGrant) - s.Delete(grantPath, grants.HandleDisableGrant) + s.Get(grantPath, toFiberHandler(grants.HandleListGrants)) + s.Post(grantPath, toFiberHandler(grants.HandleEnableGrant)) + s.Delete(grantPath, toFiberHandler(grants.HandleDisableGrant)) if config.Get().Features.SSH.Enabled { sshGrantPath := utils.CombineURLPath(grantPath, "ssh") - s.Get(sshGrantPath, ssh.HandleGetSSHInfo) - s.Post(sshGrantPath, ssh.HandlePost) - s.Delete(sshGrantPath, ssh.HandleDeleteSSHKey) + s.Get(sshGrantPath, toFiberHandler(ssh.HandleGetSSHInfo)) + s.Post(sshGrantPath, toFiberHandler(ssh.HandlePost)) + s.Delete(sshGrantPath, toFiberHandler(ssh.HandleDeleteSSHKey)) } addProfileEndpointRoutes(s, apiPaths) + if config.Get().Features.Notifications.AnyEnabled { + if config.Get().Features.Notifications.ICS.Enabled { + s.Get(apiPaths.CalendarEndpoint, toFiberHandler(calendar.HandleList)) + s.Post(apiPaths.CalendarEndpoint, toFiberHandler(calendar.HandleAdd)) + s.Get(utils.CombineURLPath(apiPaths.CalendarEndpoint, ":name"), calendar.HandleGet) + s.Post(utils.CombineURLPath(apiPaths.CalendarEndpoint, ":name"), toFiberHandler(calendar.HandleAddMytoken)) + s.Delete(utils.CombineURLPath(apiPaths.CalendarEndpoint, ":name"), toFiberHandler(calendar.HandleDelete)) + } + s.Post(apiPaths.NotificationEndpoint, toFiberHandler(notification.HandlePost)) + s.Get(apiPaths.NotificationEndpoint, toFiberHandler(notification.HandleGet)) + s.Get( + utils.CombineURLPath(apiPaths.NotificationEndpoint, ":code"), + toFiberHandler(notification.HandleGetByManagementCode), + ) + s.Delete( + utils.CombineURLPath(apiPaths.NotificationEndpoint, ":code"), + toFiberHandler(notification.HandleDeleteByManagementCode), + ) + s.Post( + utils.CombineURLPath(apiPaths.NotificationEndpoint, ":code", "nc"), + toFiberHandler(notification.HandleNotificationUpdateClasses), + ) + s.Put( + utils.CombineURLPath(apiPaths.NotificationEndpoint, ":code", "nc"), + toFiberHandler(notification.HandleNotificationUpdateClasses), + ) + s.Post( + utils.CombineURLPath(apiPaths.NotificationEndpoint, ":code", "token"), + toFiberHandler(notification.HandleNotificationAddToken), + ) + s.Delete( + utils.CombineURLPath(apiPaths.NotificationEndpoint, ":code", "token"), + toFiberHandler(notification.HandleNotificationRemoveToken), + ) + if config.Get().Features.Notifications.Mail.Enabled { + s.Get(utils.CombineURLPath(apiPaths.UserSettingEndpoint, "email"), toFiberHandler(email.HandleGet)) + s.Put(utils.CombineURLPath(apiPaths.UserSettingEndpoint, "email"), toFiberHandler(email.HandlePut)) + } + } } func addProfileEndpointRoutes(r fiber.Router, apiPaths paths.APIPaths) { diff --git a/internal/server/errorHandler.go b/internal/server/errorHandler.go index aaa9ec1f..fd6d4dab 100644 --- a/internal/server/errorHandler.go +++ b/internal/server/errorHandler.go @@ -1,13 +1,16 @@ package server import ( + "errors" "fmt" + "strings" "github.com/gofiber/fiber/v2" fiberUtils "github.com/gofiber/fiber/v2/utils" "github.com/oidc-mytoken/api/v0" "github.com/oidc-mytoken/server/internal/model" + "github.com/oidc-mytoken/server/internal/server/apipath" "github.com/oidc-mytoken/server/internal/utils/errorfmt" "github.com/oidc-mytoken/server/internal/utils/logger" ) @@ -18,7 +21,8 @@ func handleError(ctx *fiber.Ctx, err error) error { msg := errorfmt.Error(err) rlog := logger.GetRequestLogger(ctx) - if e, ok := err.(*fiber.Error); ok { + var e *fiber.Error + if errors.As(err, &e) { code = e.Code msg = e.Error() } @@ -26,7 +30,9 @@ func handleError(ctx *fiber.Ctx, err error) error { rlog.Errorf("%s", errorfmt.Full(err)) } - if ctx.Accepts(fiber.MIMETextHTML, fiber.MIMETextHTMLCharsetUTF8) != "" { + if ctx.Accepts(fiber.MIMETextHTML, fiber.MIMETextHTMLCharsetUTF8) != "" && !strings.HasPrefix( + ctx.Path(), apipath.Prefix, + ) { return handleErrorHTML(ctx, code, msg) } return handleErrorJSON(ctx, code, msg) diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 7af407b9..8535c769 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -16,7 +16,7 @@ import ( "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/db/profilerepo" configurationEndpoint "github.com/oidc-mytoken/server/internal/endpoints/configuration" - consent "github.com/oidc-mytoken/server/internal/endpoints/consent/pkg" + "github.com/oidc-mytoken/server/internal/endpoints/webentities" "github.com/oidc-mytoken/server/internal/utils/cache" "github.com/oidc-mytoken/server/internal/utils/cookies" "github.com/oidc-mytoken/server/internal/utils/templating" @@ -38,17 +38,21 @@ func homeBindingData() map[string]interface{} { pp["fed"] = p.OIDCFed providers = append(providers, pp) } - return map[string]interface{}{ + bindingData := map[string]interface{}{ templating.MustacheKeyLoggedIn: true, templating.MustacheKeyRestrictionsGUI: true, templating.MustacheKeyHome: true, - templating.MustacheKeyCapabilities: consent.AllWebCapabilities(), + templating.MustacheKeyCapabilities: webentities.AllWebCapabilities(), templating.MustacheSubTokeninfo: map[string]interface{}{ templating.MustacheKeyCollapse: templating.Collapsable{ CollapseRestr: true, }, - templating.MustacheKeyPrefix: "tokeninfo-", - templating.MustacheKeyReadOnly: true, + templating.MustacheKeyPrefix: "tokeninfo-", + templating.MustacheKeyReadOnly: true, + templating.MustacheKeyCalendarsEditable: false, + }, + templating.MustacheSubMTListing: map[string]interface{}{ + templating.MustacheKeySubscribeNotifications: true, }, templating.MustacheSubCreateMT: map[string]interface{}{ templating.MustacheKeyPrefix: "createMT-", @@ -57,6 +61,25 @@ func homeBindingData() map[string]interface{} { }, "providers": providers, } + if config.Get().Features.Notifications.ICS.Enabled || config.Get(). + Features.Notifications.Mail.Enabled { + bindingData[templating.MustacheSubNotifications] = map[string]interface{}{ + templating.MustacheKeyPrefix: "notifications-", + templating.MustacheKeyNotificationClasses: webentities.AllWebNotificationClass(), + "modify": map[string]any{ + templating.MustacheKeyPrefix: "notifications-modify-", + }, + templating.MustacheSubNotificationListing: map[string]any{ + templating.MustacheKeyPrefix: "notification-listing-", + templating.MustacheSubNewNotificationModal: map[string]any{ + templating.MustacheKeyPrefix: "new-notification-modal-", + }, + }, + } + bindingData[templating.MustacheKeyNotificationsMailEnabled] = config.Get().Features.Notifications.Mail.Enabled + bindingData[templating.MustacheKeyNotificationsCalendarEnabled] = config.Get().Features.Notifications.ICS.Enabled + } + return bindingData } type templateProfileData struct { @@ -65,31 +88,33 @@ type templateProfileData struct { } // getWebProfileData returns the cached profile data for one of the profile types -func getWebProfileData(t string) ([]templateProfileData, bool) { - data, found := cache.Get(cache.WebProfiles, t) - if !found { - return nil, found +func getWebProfileData(t string) (data []templateProfileData, ok bool) { + found, err := cache.Get(cache.WebProfiles, t, &data) + if err != nil { + log.WithError(err).Error("failed to fetch web profile data") + } + if err != nil || !found { + ok = false } - d, ok := data.([]templateProfileData) - return d, ok + return } func profilesBindingData() map[string]interface{} { - var ok bool - var err error var groups []string - g, groupsFound := cache.Get(cache.WebProfiles, "groups") - if groupsFound { - groups, ok = g.([]string) - } - if !groupsFound || !ok { + groupsFound, err := cache.Get(cache.WebProfiles, "groups", &groups) + if err != nil || !groupsFound { groups, err = profilerepo.GetGroups(log.StandardLogger(), nil) if err != nil { log.WithError(err).Error("error while retrieving profile groups for webinterface binding data") return nil } - cache.Set(cache.WebProfiles, "groups", groups, time.Hour) + if err = cache.Set( + cache.WebProfiles, "groups", groups, + time.Hour, + ); err != nil { + log.WithError(err).Error("error while setting profile groups for webinterface binding data") + } } profileTypes := map[string]func(log.Ext1FieldLogger, *sqlx.Tx, string) (profiles []api.Profile, err error){ @@ -148,6 +173,15 @@ func handleHome(ctx *fiber.Ctx) error { return ctx.Render("sites/home", homeBindingData(), templating.LayoutMain) } +func handleViewCalendar(ctx *fiber.Ctx) error { + return ctx.Render( + "sites/calendar", map[string]any{ + "calendar-view": true, + templating.MustacheKeyEmptyNavbar: true, + }, templating.LayoutMain, + ) +} + func handleSettings(ctx *fiber.Ctx) error { type bindData struct { DisplayName string @@ -167,8 +201,8 @@ func handleSettings(ctx *fiber.Ctx) error { partialName: "sites/settings-ssh", bindingData: map[string]interface{}{ templating.MustacheKeyRestrictionsGUI: true, - templating.MustacheKeyRestrictions: consent.WebRestrictions{}, - templating.MustacheKeyCapabilities: consent.AllWebCapabilities(), + templating.MustacheKeyRestrictions: webentities.WebRestrictions{}, + templating.MustacheKeyCapabilities: webentities.AllWebCapabilities(), }, }, } @@ -180,13 +214,21 @@ func handleSettings(ctx *fiber.Ctx) error { g.EmbedBody = embed.String() } binding := map[string]interface{}{ - templating.MustacheKeyGrants: grants, - templating.MustacheKeyLoggedIn: true, - templating.MustacheKeySettings: true, - templating.MustacheKeySettingsSSH: true, - templating.MustacheKeyRestrictionsGUI: true, - templating.MustacheKeyRestrictions: consent.WebRestrictions{}, - templating.MustacheKeyCapabilities: consent.AllWebCapabilities(), + templating.MustacheKeyGrants: grants, + templating.MustacheKeyLoggedIn: true, + templating.MustacheKeySettings: true, + templating.MustacheKeySettingsSSH: true, + templating.MustacheKeyRestrictionsGUI: true, + templating.MustacheKeyCalendarsEditable: true, + templating.MustacheKeyRestrictions: webentities.WebRestrictions{}, + templating.MustacheKeyCapabilities: webentities.AllWebCapabilities(), + templating.MustacheKeyPrefix: "settings-", + } + if config.Get().Features.Notifications.ICS.Enabled || config.Get(). + Features.Notifications.Mail.Enabled { + binding[templating.MustacheSubNotifications] = true + binding[templating.MustacheKeyNotificationsMailEnabled] = config.Get().Features.Notifications.Mail.Enabled + binding[templating.MustacheKeyNotificationsCalendarEnabled] = config.Get().Features.Notifications.ICS.Enabled } return ctx.Render("sites/settings", binding, templating.LayoutMain) } @@ -218,3 +260,17 @@ func handlePrivacy(ctx *fiber.Ctx) error { } return ctx.Render("sites/privacy", binding, templating.LayoutMain) } + +func handleNotificationManagement(ctx *fiber.Ctx) error { + return ctx.Render( + "sites/manage-notification", map[string]any{ + "notification-management": true, + "empty-navbar": true, + templating.MustacheSubNewNotificationModal: true, + templating.MustacheKeyNotificationClasses: webentities.AllWebNotificationClass(), + templating.MustacheKeyCollapse: map[string]bool{ + "NotificationManagement": true, + }, + }, templating.LayoutMain, + ) +} diff --git a/internal/server/handlewrap.go b/internal/server/handlewrap.go new file mode 100644 index 00000000..86e9ee23 --- /dev/null +++ b/internal/server/handlewrap.go @@ -0,0 +1,15 @@ +package server + +import ( + "github.com/gofiber/fiber/v2" + + "github.com/oidc-mytoken/server/internal/model" +) + +type handler func(ctx *fiber.Ctx) *model.Response + +func toFiberHandler(h handler) fiber.Handler { + return func(ctx *fiber.Ctx) error { + return h(ctx).Send(ctx) + } +} diff --git a/internal/server/healthcheck/healthcheck.go b/internal/server/healthcheck/healthcheck.go new file mode 100644 index 00000000..d2988c42 --- /dev/null +++ b/internal/server/healthcheck/healthcheck.go @@ -0,0 +1,131 @@ +package healthcheck + +import ( + "fmt" + "sync" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/oidc-mytoken/utils/httpclient" + "github.com/oidc-mytoken/utils/utils" + log "github.com/sirupsen/logrus" + "github.com/zachmann/go-oidfed/pkg" + "github.com/zachmann/go-oidfed/pkg/cache" + + "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/db/dbrepo/versionrepo" + "github.com/oidc-mytoken/server/internal/model/version" + "github.com/oidc-mytoken/server/internal/server/routes" +) + +// Start starts the healthcheck endpoint on the configured port if enabled +func Start() { + if !config.Get().Server.Healthcheck.Enabled { + return + } + httpServer := fiber.New() + httpServer.Get( + "", handleHealthCheck, + ) + addr := fmt.Sprintf(":%d", config.Get().Server.Healthcheck.Port) + log.Infof("Healthcheck endpoint started on %s", addr) + go func() { + log.WithError(httpServer.Listen(addr)).Fatal() + }() +} + +type status struct { + Healthy bool `json:"healthy"` + Operational bool `json:"operational"` + Components componentsStatus `json:"components"` + Version string `json:"version"` + Timestamp pkg.Unixtime `json:"timestamp"` +} + +type componentsStatus struct { + ServerUp bool `json:"server_up"` + ServerReachable bool `json:"server_reachable"` + Database bool `json:"database_up"` + Cache bool `json:"cache_up"` +} + +func (c componentsStatus) healthy() bool { + return c.ServerUp && c.ServerReachable && c.Database && c.Cache +} +func (c componentsStatus) operational() bool { + return c.ServerUp && c.ServerReachable && c.Database +} + +func handleHealthCheck(ctx *fiber.Ctx) error { + state := healthcheck() + if !state.Operational { + ctx.Status(fiber.StatusServiceUnavailable) + } + return ctx.JSON(state) +} + +func healthcheck() status { + components := componentsStatus{ + ServerUp: true, + ServerReachable: checkServer(), + Database: checkDB(), + Cache: checkCache(), + } + return status{ + Healthy: components.healthy(), + Operational: components.operational(), + Components: components, + Version: version.VERSION, + Timestamp: pkg.Unixtime{Time: time.Now()}, + } +} + +func checkServer() bool { + _, err := httpclient.Do().R().Get(routes.ConfigEndpoint) + if err != nil { + log.WithError(err).WithField("healthcheck", "server_reachable").Error("error server healthcheck") + return false + } + return true +} + +func checkDB() bool { + _, err := versionrepo.GetVersionState(log.StandardLogger(), nil) + if err != nil { + log.WithError(err).WithField("healthcheck", "db").Error("error db healthcheck") + return false + } + return true +} + +var cacheMutex sync.Mutex + +func checkCache() bool { + cacheMutex.Lock() + defer cacheMutex.Unlock() + k := "healthcheck" + v := utils.RandASCIIString(64) + if err := cache.Set(k, v, time.Second); err != nil { + log.WithError(err).WithField("healthcheck", "cache").Error("error caching healthcheck") + return false + } + var cached string + set, err := cache.Get(k, &cached) + if err != nil { + log.WithError(err).WithField("healthcheck", "cache"). + Error("error obtaining cached healthcheck") + return false + } + if !set { + log.WithField("healthcheck", "cache").Error("cached healthcheck not found") + return false + } + if cached != v { + log.WithField("healthcheck", "cache"). + WithField("cached", v). + WithField("obtained", cached). + Error("cached value does not match") + return false + } + return true +} diff --git a/internal/server/middlerwares.go b/internal/server/middlewares.go similarity index 81% rename from internal/server/middlerwares.go rename to internal/server/middlewares.go index 9f32df43..fde0cd0b 100644 --- a/internal/server/middlerwares.go +++ b/internal/server/middlewares.go @@ -13,16 +13,17 @@ import ( "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/favicon" "github.com/gofiber/fiber/v2/middleware/filesystem" + "github.com/gofiber/fiber/v2/middleware/helmet" "github.com/gofiber/fiber/v2/middleware/limiter" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/recover" "github.com/gofiber/fiber/v2/middleware/requestid" - "github.com/gofiber/helmet/v2" log "github.com/sirupsen/logrus" "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/server/apipath" "github.com/oidc-mytoken/server/internal/server/paths" + "github.com/oidc-mytoken/server/internal/utils/ctxutils" "github.com/oidc-mytoken/server/internal/utils/fileio" "github.com/oidc-mytoken/server/internal/utils/iputils" loggerUtils "github.com/oidc-mytoken/server/internal/utils/logger" @@ -36,6 +37,16 @@ var staticFS fs.FS var _faviconFS embed.FS var faviconFS fs.FS +var corsAllowedPaths = []string{ + paths.WellknownMytokenConfiguration, + paths.WellknownOpenIDConfiguration, + paths.GetGeneralPaths().JWKSEndpoint, +} +var corsAllowedPrefixes = []string{ + apipath.Prefix, + "/static", +} + func init() { var err error staticFS, err = fs.Sub(_staticFS, "web/static") @@ -129,40 +140,37 @@ func addRecoverMiddleware(s fiber.Router) { } func addHelmetMiddleware(s fiber.Router) { - s.Use(helmet.New()) + helmetConfig := helmet.ConfigDefault + helmetConfig.Next = func(ctx *fiber.Ctx) bool { + return !nextCors(ctx) + } + s.Use(helmet.New(helmetConfig)) } func addRequestIDMiddleware(s fiber.Router) { s.Use(requestid.New()) } -func addCorsMiddleware(s fiber.Router) { - allowedPaths := []string{ - paths.WellknownMytokenConfiguration, - paths.WellknownOpenIDConfiguration, - paths.GetGeneralPaths().JWKSEndpoint, +func nextCors(c *fiber.Ctx) bool { + p := c.Path() + for _, pre := range corsAllowedPrefixes { + if strings.HasPrefix(p, pre) { + return false + } } - allowedPrefixes := []string{ - apipath.Prefix, - "/static", + for _, pre := range corsAllowedPaths { + if p == pre { + return false + } } + return true +} + +func addCorsMiddleware(s fiber.Router) { s.Use( cors.New( cors.Config{ - Next: func(c *fiber.Ctx) bool { - p := c.Path() - for _, pre := range allowedPrefixes { - if strings.HasPrefix(p, pre) { - return false - } - } - for _, pre := range allowedPaths { - if p == pre { - return false - } - } - return true - }, + Next: nextCors, }, ), ) @@ -172,14 +180,14 @@ func userIsGroupMiddleware(c *fiber.Ctx) error { rlog := loggerUtils.GetRequestLogger(c) rlog.WithFields( log.Fields{ - "group": c.Params("group", "_"), + "group": ctxutils.Params(c, "group", "_"), "username": c.Locals(basicauth.ConfigDefault.ContextUsername), "path": c.Path(), "original_path": c.Route().Path, "params": c.Route().Params, }, ).Error() - if c.Params("group", "_") != c.Locals(basicauth.ConfigDefault.ContextUsername) { + if ctxutils.Params(c, "group", "_") != c.Locals(basicauth.ConfigDefault.ContextUsername) { return fiber.ErrForbidden } return c.Next() diff --git a/internal/server/paths/paths.go b/internal/server/paths/paths.go index c5b69057..4b8aced1 100644 --- a/internal/server/paths/paths.go +++ b/internal/server/paths/paths.go @@ -28,6 +28,8 @@ func defaultAPIPaths(api string) APIPaths { UserSettingEndpoint: utils.CombineURLPath(api, "/settings"), ProfilesEndpoint: utils.CombineURLPath(api, "/pt"), GuestModeOP: utils.CombineURLPath(api, "/guests"), + NotificationEndpoint: utils.CombineURLPath(api, "/notifications"), + CalendarEndpoint: utils.CombineURLPath(api, "/notifications/calendars"), } } @@ -38,12 +40,15 @@ func init() { 0: defaultAPIPaths(apipath.V0), }, other: GeneralPaths{ - ConfigurationEndpoint: WellknownMytokenConfiguration, - FederationEndpoint: WellknownOpenIDFederation, - OIDCRedirectEndpoint: "/redirect", - JWKSEndpoint: "/jwks", - ConsentEndpoint: "/c", - Privacy: "/privacy", + ConfigurationEndpoint: WellknownMytokenConfiguration, + FederationEndpoint: WellknownOpenIDFederation, + OIDCRedirectEndpoint: "/redirect", + JWKSEndpoint: "/jwks", + ConsentEndpoint: "/c", + Privacy: "/privacy", + CalendarEndpoint: "/calendars", + ActionsEndpoint: "/actions", + NotificationManagementEndpoint: "/notifications", }, } } @@ -55,12 +60,15 @@ type paths struct { // GeneralPaths holds all non-api route paths type GeneralPaths struct { - ConfigurationEndpoint string - FederationEndpoint string - OIDCRedirectEndpoint string - JWKSEndpoint string - ConsentEndpoint string - Privacy string + ConfigurationEndpoint string + FederationEndpoint string + OIDCRedirectEndpoint string + JWKSEndpoint string + ConsentEndpoint string + Privacy string + CalendarEndpoint string + ActionsEndpoint string + NotificationManagementEndpoint string } // APIPaths holds all api route paths @@ -73,6 +81,8 @@ type APIPaths struct { UserSettingEndpoint string ProfilesEndpoint string GuestModeOP string + NotificationEndpoint string + CalendarEndpoint string } // GetCurrentAPIPaths returns the api paths for the most recent major version diff --git a/internal/server/routes/routes.go b/internal/server/routes/routes.go index c44810fe..f41cd48f 100644 --- a/internal/server/routes/routes.go +++ b/internal/server/routes/routes.go @@ -1,18 +1,50 @@ package routes import ( + "fmt" + "net/url" + "github.com/oidc-mytoken/utils/utils" "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/endpoints/actions/pkg" "github.com/oidc-mytoken/server/internal/server/paths" ) -var RedirectURI string -var ConsentEndpoint string +// EndpointURIs +var ( + RedirectURI string + ConsentEndpoint string + CalendarDownloadEndpoint string + ActionsEndpoint string + NotificationManagementEndpoint string + ConfigEndpoint string +) // Init initializes the authcode component func Init() { generalPaths := paths.GetGeneralPaths() RedirectURI = utils.CombineURLPath(config.Get().IssuerURL, generalPaths.OIDCRedirectEndpoint) ConsentEndpoint = utils.CombineURLPath(config.Get().IssuerURL, generalPaths.ConsentEndpoint) + CalendarDownloadEndpoint = utils.CombineURLPath(config.Get().IssuerURL, generalPaths.CalendarEndpoint) + ActionsEndpoint = utils.CombineURLPath(config.Get().IssuerURL, generalPaths.ActionsEndpoint) + NotificationManagementEndpoint = utils.CombineURLPath( + config.Get().IssuerURL, + generalPaths.NotificationManagementEndpoint, + ) + ConfigEndpoint = utils.CombineURLPath(config.Get().IssuerURL, generalPaths.ConfigurationEndpoint) +} + +// ActionsURL builds an action url from a pkg.ActionInfo +func ActionsURL(actionCode pkg.ActionInfo) string { + params := url.Values{} + params.Set("action", url.QueryEscape(actionCode.Action)) + params.Set("code", url.QueryEscape(actionCode.Code)) + p := params.Encode() + return fmt.Sprintf("%s?%s", ActionsEndpoint, p) +} + +// NotificationManagementURL builds notification management url for the passes management code +func NotificationManagementURL(mc string) string { + return utils.CombineURLPath(NotificationManagementEndpoint, mc) } diff --git a/internal/server/server.go b/internal/server/server.go index c9f379ed..f72bee94 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -10,15 +10,18 @@ import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/template/mustache/v2" + "github.com/oidc-mytoken/utils/utils" log "github.com/sirupsen/logrus" "github.com/oidc-mytoken/api/v0" "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/endpoints" + "github.com/oidc-mytoken/server/internal/endpoints/actions" "github.com/oidc-mytoken/server/internal/endpoints/configuration" "github.com/oidc-mytoken/server/internal/endpoints/consent" "github.com/oidc-mytoken/server/internal/endpoints/federation" + "github.com/oidc-mytoken/server/internal/endpoints/notification/calendar" "github.com/oidc-mytoken/server/internal/endpoints/redirect" "github.com/oidc-mytoken/server/internal/model" "github.com/oidc-mytoken/server/internal/server/apipath" @@ -104,26 +107,32 @@ func Init() { func addRoutes(s fiber.Router) { addWebRoutes(s) - s.Get(paths.GetGeneralPaths().ConfigurationEndpoint, configuration.HandleConfiguration) - s.Get(paths.WellknownOpenIDConfiguration, configuration.HandleConfiguration) + generalPaths := paths.GetGeneralPaths() + s.Get(generalPaths.ConfigurationEndpoint, toFiberHandler(configuration.HandleConfiguration)) + s.Get(paths.WellknownOpenIDConfiguration, toFiberHandler(configuration.HandleConfiguration)) if config.Get().Features.Federation.Enabled { - s.Get(paths.GetGeneralPaths().FederationEndpoint, federation.HandleEntityConfiguration) + s.Get(generalPaths.FederationEndpoint, federation.HandleEntityConfiguration) } - s.Get(paths.GetGeneralPaths().JWKSEndpoint, endpoints.HandleJWKS) - s.Get(paths.GetGeneralPaths().OIDCRedirectEndpoint, redirect.HandleOIDCRedirect) + s.Get(generalPaths.JWKSEndpoint, endpoints.HandleJWKS) + s.Get(generalPaths.OIDCRedirectEndpoint, redirect.HandleOIDCRedirect) s.Get("/c/:consent_code", consent.HandleConsent) - s.Post("/c/:consent_code", consent.HandleConsentPost) + s.Post("/c/:consent_code", toFiberHandler(consent.HandleConsentPost)) s.Post("/c", consent.HandleCreateConsent) s.Get("/native", handleNativeCallback) s.Get("/native/abort", handleNativeConsentAbortCallback) - s.Get(paths.GetGeneralPaths().Privacy, handlePrivacy) - s.Get("/settings", handleSettings) + s.Get(generalPaths.Privacy, handlePrivacy) + s.Get(utils.CombineURLPath(generalPaths.CalendarEndpoint, ":id"), calendar.HandleGetICS) + s.Get(generalPaths.ActionsEndpoint, actions.HandleActions) addAPIRoutes(s) } func addWebRoutes(s fiber.Router) { + generalPaths := paths.GetGeneralPaths() s.Get("/", handleIndex) s.Get("/home", handleHome) + s.Get("/settings", handleSettings) + s.Get(utils.CombineURLPath(generalPaths.CalendarEndpoint, ":id", "view"), handleViewCalendar) + s.Get(utils.CombineURLPath(generalPaths.NotificationManagementEndpoint, ":mc"), handleNotificationManagement) } func start(s *fiber.App) { diff --git a/internal/server/ssh/at.go b/internal/server/ssh/at.go index ccbd6636..d0c96d09 100644 --- a/internal/server/ssh/at.go +++ b/internal/server/ssh/at.go @@ -26,7 +26,7 @@ func handleSSHAT(reqData []byte, s ssh.Session) error { } } mt := ctx.Value("mytoken").(*mytoken.Mytoken) - clientMetaData := api.ClientMetaData{ + clientMetaData := &api.ClientMetaData{ IP: ctx.Value("ip").(string), UserAgent: ctx.Value("user_agent").(string), } @@ -36,12 +36,12 @@ func handleSSHAT(reqData []byte, s ssh.Session) error { rlog.Debug("Handle AT from ssh") rlog.Trace("Parsed AT request") - errRes := auth.RequireMytokenNotRevoked(rlog, nil, mt) + errRes := auth.RequireMytokenNotRevoked(rlog, nil, mt, clientMetaData) if errRes != nil { return writeErrRes(s, errRes) } - usedRestriction, errRes := auth.CheckCapabilityAndRestriction( - rlog, nil, mt, clientMetaData.IP, + usedRestriction, errRes := auth.RequireCapabilityAndRestriction( + rlog, nil, mt, clientMetaData, utils.SplitIgnoreEmpty(req.Scope, " "), utils.SplitIgnoreEmpty(req.Audience, " "), api.CapabilityAT, @@ -53,7 +53,7 @@ func handleSSHAT(reqData []byte, s ssh.Session) error { if errRes != nil { return writeErrRes(s, errRes) } - res := access.HandleAccessTokenRefresh(rlog, mt, req, clientMetaData, provider, usedRestriction) + res := access.HandleAccessTokenRefresh(rlog, mt, req, *clientMetaData, provider, usedRestriction) if res.Status >= 400 { return writeErrRes(s, res) } diff --git a/internal/server/ssh/hostconfig.go b/internal/server/ssh/hostconfig.go index 47354ee0..06f9285f 100644 --- a/internal/server/ssh/hostconfig.go +++ b/internal/server/ssh/hostconfig.go @@ -22,7 +22,7 @@ func entryName(name string) string { return "mytoken-" + name } -// CreateHostConfigEntry creates an ssh config host entry for the passed ssh user name and name +// CreateHostConfigEntry creates an ssh config host entry for the passed ssh username and name func CreateHostConfigEntry(sshUser, name string) string { return fmt.Sprintf(hostEntryTemplate, entryName(name), config.Get().Host, 2222, sshUser) } diff --git a/internal/server/ssh/mytoken.go b/internal/server/ssh/mytoken.go index 90381134..62d6c13b 100644 --- a/internal/server/ssh/mytoken.go +++ b/internal/server/ssh/mytoken.go @@ -23,7 +23,7 @@ func handleSSHMytoken(reqData []byte, s ssh.Session) error { return err } } - clientMetaData := api.ClientMetaData{ + clientMetaData := &api.ClientMetaData{ IP: ctx.Value("ip").(string), UserAgent: ctx.Value("user_agent").(string), } @@ -31,11 +31,11 @@ func handleSSHMytoken(reqData []byte, s ssh.Session) error { rlog := logger.GetSSHRequestLogger(ctx.Value("session").(string)) rlog.Debug("Handle mytoken from ssh") - usedRestriction, mt, errRes := mytoken2.HandleMytokenFromMytokenReqChecks(rlog, req, clientMetaData.IP, nil) + usedRestriction, mt, errRes := mytoken2.HandleMytokenFromMytokenReqChecks(rlog, req, clientMetaData, nil) if errRes != nil { return writeErrRes(s, errRes) } - res := mytoken2.HandleMytokenFromMytokenReq(rlog, mt, req, &clientMetaData, usedRestriction) + res := mytoken2.HandleMytokenFromMytokenReq(rlog, mt, req, clientMetaData, usedRestriction) if res.Status >= 400 { return writeErrRes(s, res) } diff --git a/internal/server/ssh/tokeninfo.go b/internal/server/ssh/tokeninfo.go index 407cff81..91030f11 100644 --- a/internal/server/ssh/tokeninfo.go +++ b/internal/server/ssh/tokeninfo.go @@ -2,8 +2,11 @@ package ssh import ( "github.com/gliderlabs/ssh" + "github.com/jmoiron/sqlx" "github.com/oidc-mytoken/api/v0" + "github.com/pkg/errors" + "github.com/oidc-mytoken/server/internal/db" "github.com/oidc-mytoken/server/internal/endpoints/tokeninfo" "github.com/oidc-mytoken/server/internal/endpoints/tokeninfo/pkg" "github.com/oidc-mytoken/server/internal/model" @@ -15,83 +18,127 @@ import ( func handleIntrospect(s ssh.Session) error { ctx := s.Context() mt := ctx.Value("mytoken").(*mytoken.Mytoken) - clientMetaData := api.ClientMetaData{ + clientMetaData := &api.ClientMetaData{ IP: ctx.Value("ip").(string), UserAgent: ctx.Value("user_agent").(string), } rlog := logger.GetSSHRequestLogger(ctx.Value("session").(string)) rlog.Debug("Handle tokeninfo introspect from ssh") - errRes := auth.RequireMytokenNotRevoked(rlog, nil, mt) + var res *model.Response + var errRes *model.Response + _ = db.Transact( + rlog, func(tx *sqlx.Tx) error { + errRes = auth.RequireMytokenNotRevoked(rlog, tx, mt, clientMetaData) + if errRes != nil { + return errors.New("rollback") + } + res = tokeninfo.HandleTokenInfoIntrospect(rlog, tx, mt, model.ResponseTypeToken, clientMetaData) + if res.Status >= 400 { + errRes = res + return errors.New("rollback") + } + return nil + }, + ) if errRes != nil { return writeErrRes(s, errRes) } - res := tokeninfo.HandleTokenInfoIntrospect(rlog, mt, model.ResponseTypeToken, &clientMetaData) - if res.Status >= 400 { - return writeErrRes(s, &res) - } return writeJSON(s, res.Response) } func handleHistory(s ssh.Session) error { ctx := s.Context() mt := ctx.Value("mytoken").(*mytoken.Mytoken) - clientMetaData := api.ClientMetaData{ + clientMetaData := &api.ClientMetaData{ IP: ctx.Value("ip").(string), UserAgent: ctx.Value("user_agent").(string), } rlog := logger.GetSSHRequestLogger(ctx.Value("session").(string)) rlog.Debug("Handle tokeninfo history from ssh") - errRes := auth.RequireMytokenNotRevoked(rlog, nil, mt) + var res *model.Response + var errRes *model.Response + _ = db.Transact( + rlog, func(tx *sqlx.Tx) error { + errRes = auth.RequireMytokenNotRevoked(rlog, tx, mt, clientMetaData) + if errRes != nil { + return errors.New("rollback") + } + res = tokeninfo.HandleTokenInfoHistory(rlog, tx, &pkg.TokenInfoRequest{}, mt, clientMetaData) + if res.Status >= 400 { + errRes = res + return errors.New("rollback") + } + return nil + }, + ) if errRes != nil { return writeErrRes(s, errRes) } - res := tokeninfo.HandleTokenInfoHistory(rlog, &pkg.TokenInfoRequest{}, mt, &clientMetaData) - if res.Status >= 400 { - return writeErrRes(s, &res) - } return writeJSON(s, res.Response) } func handleSubtokens(s ssh.Session) error { ctx := s.Context() mt := ctx.Value("mytoken").(*mytoken.Mytoken) - clientMetaData := api.ClientMetaData{ + clientMetaData := &api.ClientMetaData{ IP: ctx.Value("ip").(string), UserAgent: ctx.Value("user_agent").(string), } rlog := logger.GetSSHRequestLogger(ctx.Value("session").(string)) rlog.Debug("Handle tokeninfo subtokens from ssh") - errRes := auth.RequireMytokenNotRevoked(rlog, nil, mt) + var res *model.Response + var errRes *model.Response + _ = db.Transact( + rlog, func(tx *sqlx.Tx) error { + errRes = auth.RequireMytokenNotRevoked(rlog, tx, mt, clientMetaData) + if errRes != nil { + return errors.New("rollback") + } + res = tokeninfo.HandleTokenInfoSubtokens(rlog, tx, &pkg.TokenInfoRequest{}, mt, clientMetaData) + if res.Status >= 400 { + errRes = res + return errors.New("rollback") + } + return nil + }, + ) if errRes != nil { return writeErrRes(s, errRes) } - res := tokeninfo.HandleTokenInfoSubtokens(rlog, &pkg.TokenInfoRequest{}, mt, &clientMetaData) - if res.Status >= 400 { - return writeErrRes(s, &res) - } return writeJSON(s, res.Response) } func handleListMytokens(s ssh.Session) error { ctx := s.Context() mt := ctx.Value("mytoken").(*mytoken.Mytoken) - clientMetaData := api.ClientMetaData{ + clientMetaData := &api.ClientMetaData{ IP: ctx.Value("ip").(string), UserAgent: ctx.Value("user_agent").(string), } rlog := logger.GetSSHRequestLogger(ctx.Value("session").(string)) rlog.Debug("Handle tokeninfo list mytokens from ssh") - errRes := auth.RequireMytokenNotRevoked(rlog, nil, mt) + var res *model.Response + var errRes *model.Response + _ = db.Transact( + rlog, func(tx *sqlx.Tx) error { + errRes = auth.RequireMytokenNotRevoked(rlog, tx, mt, clientMetaData) + if errRes != nil { + return errors.New("rollback") + } + res = tokeninfo.HandleTokenInfoList(rlog, tx, &pkg.TokenInfoRequest{}, mt, clientMetaData) + if res.Status >= 400 { + errRes = res + return errors.New("rollback") + } + return nil + }, + ) if errRes != nil { return writeErrRes(s, errRes) } - res := tokeninfo.HandleTokenInfoList(rlog, &pkg.TokenInfoRequest{}, mt, &clientMetaData) - if res.Status >= 400 { - return writeErrRes(s, &res) - } return writeJSON(s, res.Response) } diff --git a/internal/server/web/partials/about-web.mustache b/internal/server/web/partials/about-web.mustache index 448288e7..a5700062 100644 --- a/internal/server/web/partials/about-web.mustache +++ b/internal/server/web/partials/about-web.mustache @@ -57,6 +57,17 @@ {{/logged-in}} + {{#notifications-mail-enabled}} +
  • + {{#logged-in}} + + {{/logged-in}} +
  • + {{/notifications-mail-enabled}}
  • {{#logged-in}} +
    diff --git a/internal/server/web/partials/list-mts.mustache b/internal/server/web/partials/list-mts.mustache index 1a060b8f..089609b1 100644 --- a/internal/server/web/partials/list-mts.mustache +++ b/internal/server/web/partials/list-mts.mustache @@ -1,3 +1,4 @@ +{{#mt-listing}}

    All My Mytokens

    -{{>history-modal}} \ No newline at end of file + +{{#notifications}} +{{>notifications/info-subscribe-modal}} +{{/notifications}} +{{/mt-listing}} \ No newline at end of file diff --git a/internal/server/web/partials/navbar.mustache b/internal/server/web/partials/navbar.mustache index 6524d72d..fdd643db 100644 --- a/internal/server/web/partials/navbar.mustache +++ b/internal/server/web/partials/navbar.mustache @@ -1,6 +1,9 @@
  • {{/logged-in}} + {{#logged-in}} + {{#notifications-mail-enabled}} + + {{/notifications-mail-enabled}} + {{/logged-in}}
    @@ -56,11 +64,20 @@ {{> list-mts}}
    {{/logged-in}} + {{#logged-in}} + {{#notifications-mail-enabled}} +
    + {{> notifications/home-pane}} +
    + {{/notifications-mail-enabled}} + {{/logged-in}} {{>revocation-modal}} +{{>history-modal}} +{{> error-message }} {{^logged-in}} + + + +``` + +### 3. Direct Download? + +Managing dependencies by "directly downloading" them and placing them into your +source code is not recommended for a variety of reasons, including missing out +on feat/fix updates easily. Please use a versioning management system like a CDN +or npm/Yarn. + +## Usage + +The most straightforward way to get started is to import Popper from the `unpkg` +CDN, which includes all of its features. You can call the `Popper.createPopper` +constructor to create new popper instances. + +Here is a complete example: + +```html + +Popper example + + + + + + + + +``` + +Visit the [tutorial](https://popper.js.org/docs/v2/tutorial/) for an example of +how to build your own tooltip from scratch using Popper. + +### Module bundlers + +You can import the `createPopper` constructor from the fully-featured file: + +```js +import { createPopper } from '@popperjs/core'; + +const button = document.querySelector('#button'); +const tooltip = document.querySelector('#tooltip'); + +// Pass the button, the tooltip, and some options, and Popper will do the +// magic positioning for you: +createPopper(button, tooltip, { + placement: 'right', +}); +``` + +All the modifiers listed in the docs menu will be enabled and "just work", so +you don't need to think about setting Popper up. The size of Popper including +all of its features is about 5 kB minzipped, but it may grow a bit in the +future. + +#### Popper Lite (tree-shaking) + +If bundle size is important, you'll want to take advantage of tree-shaking. The +library is built in a modular way to allow to import only the parts you really +need. + +```js +import { createPopperLite as createPopper } from '@popperjs/core'; +``` + +The Lite version includes the most necessary modifiers that will compute the +offsets of the popper, compute and add the positioning styles, and add event +listeners. This is close in bundle size to pure CSS tooltip libraries, and +behaves somewhat similarly. + +However, this does not include the features that makes Popper truly useful. + +The two most useful modifiers not included in Lite are `preventOverflow` and +`flip`: + +```js +import { + createPopperLite as createPopper, + preventOverflow, + flip, +} from '@popperjs/core'; + +const button = document.querySelector('#button'); +const tooltip = document.querySelector('#tooltip'); + +createPopper(button, tooltip, { + modifiers: [preventOverflow, flip], +}); +``` + +As you make more poppers, you may be finding yourself needing other modifiers +provided by the library. + +See [tree-shaking](https://popper.js.org/docs/v2/performance/#tree-shaking) for more +information. + +## Distribution targets + +Popper is distributed in 3 different versions, in 3 different file formats. + +The 3 file formats are: + +- `esm` (works with `import` syntax — **recommended**) +- `umd` (works with ` + +``` + +The core CSS comes bundled with the default unpkg import. + +## Usage + +For detailed usage information, +[visit the docs](https://atomiks.github.io/tippyjs/v6/getting-started/). + +## Component Wrappers + +React: [@tippyjs/react](https://github.com/atomiks/tippyjs-react) + +## License + +MIT diff --git a/internal/server/web/static/lib/tippy/dist/backdrop.css b/internal/server/web/static/lib/tippy/dist/backdrop.css new file mode 100644 index 00000000..36c26c57 --- /dev/null +++ b/internal/server/web/static/lib/tippy/dist/backdrop.css @@ -0,0 +1 @@ +.tippy-box[data-placement^=top]>.tippy-backdrop{transform-origin:0 25%;border-radius:40% 40% 0 0}.tippy-box[data-placement^=top]>.tippy-backdrop[data-state=visible]{transform:scale(1) translate(-50%,-55%)}.tippy-box[data-placement^=top]>.tippy-backdrop[data-state=hidden]{transform:scale(.2) translate(-50%,-45%)}.tippy-box[data-placement^=bottom]>.tippy-backdrop{transform-origin:0 -50%;border-radius:0 0 30% 30%}.tippy-box[data-placement^=bottom]>.tippy-backdrop[data-state=visible]{transform:scale(1) translate(-50%,-45%)}.tippy-box[data-placement^=bottom]>.tippy-backdrop[data-state=hidden]{transform:scale(.2) translate(-50%)}.tippy-box[data-placement^=left]>.tippy-backdrop{transform-origin:50% 0;border-radius:50% 0 0 50%}.tippy-box[data-placement^=left]>.tippy-backdrop[data-state=visible]{transform:scale(1) translate(-50%,-50%)}.tippy-box[data-placement^=left]>.tippy-backdrop[data-state=hidden]{transform:scale(.2) translate(-75%,-50%)}.tippy-box[data-placement^=right]>.tippy-backdrop{transform-origin:-50% 0;border-radius:0 50% 50% 0}.tippy-box[data-placement^=right]>.tippy-backdrop[data-state=visible]{transform:scale(1) translate(-50%,-50%)}.tippy-box[data-placement^=right]>.tippy-backdrop[data-state=hidden]{transform:scale(.2) translate(-25%,-50%)}.tippy-box[data-animatefill]{background-color:transparent!important}.tippy-backdrop{position:absolute;background-color:#333;border-radius:50%;width:calc(110% + 32px);left:50%;top:50%;z-index:-1;transition:all cubic-bezier(.46,.1,.52,.98);-webkit-backface-visibility:hidden;backface-visibility:hidden}.tippy-backdrop[data-state=hidden]{opacity:0}.tippy-backdrop:after{content:"";float:left;padding-top:100%}.tippy-backdrop+.tippy-content{transition-property:opacity;will-change:opacity}.tippy-backdrop+.tippy-content[data-state=hidden]{opacity:0} \ No newline at end of file diff --git a/internal/server/web/static/lib/tippy/dist/border.css b/internal/server/web/static/lib/tippy/dist/border.css new file mode 100644 index 00000000..c2b8f805 --- /dev/null +++ b/internal/server/web/static/lib/tippy/dist/border.css @@ -0,0 +1 @@ +.tippy-box{border:1px transparent}.tippy-box[data-placement^=top]>.tippy-arrow:after{border-top-color:inherit;border-width:8px 8px 0;bottom:-8px;left:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:after{border-bottom-color:inherit;border-width:0 8px 8px;top:-8px;left:0}.tippy-box[data-placement^=left]>.tippy-arrow:after{border-left-color:inherit;border-width:8px 0 8px 8px;right:-8px;top:0}.tippy-box[data-placement^=right]>.tippy-arrow:after{border-width:8px 8px 8px 0;left:-8px;top:0;border-right-color:inherit}.tippy-box[data-placement^=top]>.tippy-svg-arrow>svg:first-child:not(:last-child){top:17px}.tippy-box[data-placement^=bottom]>.tippy-svg-arrow>svg:first-child:not(:last-child){bottom:17px}.tippy-box[data-placement^=left]>.tippy-svg-arrow>svg:first-child:not(:last-child){left:12px}.tippy-box[data-placement^=right]>.tippy-svg-arrow>svg:first-child:not(:last-child){right:12px}.tippy-arrow{border-color:inherit}.tippy-arrow:after{content:"";z-index:-1;position:absolute;border-color:transparent;border-style:solid} \ No newline at end of file diff --git a/internal/server/web/static/lib/tippy/dist/svg-arrow.css b/internal/server/web/static/lib/tippy/dist/svg-arrow.css new file mode 100644 index 00000000..c2a61ad7 --- /dev/null +++ b/internal/server/web/static/lib/tippy/dist/svg-arrow.css @@ -0,0 +1 @@ +.tippy-box[data-placement^=top]>.tippy-svg-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-svg-arrow:after,.tippy-box[data-placement^=top]>.tippy-svg-arrow>svg{top:16px;transform:rotate(180deg)}.tippy-box[data-placement^=bottom]>.tippy-svg-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-svg-arrow>svg{bottom:16px}.tippy-box[data-placement^=left]>.tippy-svg-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-svg-arrow:after,.tippy-box[data-placement^=left]>.tippy-svg-arrow>svg{transform:rotate(90deg);top:calc(50% - 3px);left:11px}.tippy-box[data-placement^=right]>.tippy-svg-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-svg-arrow:after,.tippy-box[data-placement^=right]>.tippy-svg-arrow>svg{transform:rotate(-90deg);top:calc(50% - 3px);right:11px}.tippy-svg-arrow{width:16px;height:16px;fill:#333;text-align:initial}.tippy-svg-arrow,.tippy-svg-arrow>svg{position:absolute} \ No newline at end of file diff --git a/internal/server/web/static/lib/tippy/dist/tippy.css b/internal/server/web/static/lib/tippy/dist/tippy.css new file mode 100644 index 00000000..e6ae635c --- /dev/null +++ b/internal/server/web/static/lib/tippy/dist/tippy.css @@ -0,0 +1 @@ +.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1} \ No newline at end of file diff --git a/internal/server/web/static/lib/tippy/dist/tippy.esm.js b/internal/server/web/static/lib/tippy/dist/tippy.esm.js new file mode 100644 index 00000000..0314c895 --- /dev/null +++ b/internal/server/web/static/lib/tippy/dist/tippy.esm.js @@ -0,0 +1,2544 @@ +/**! + * tippy.js v6.3.7 + * (c) 2017-2021 atomiks + * MIT License + */ +import {createPopper, applyStyles} from '../../../js/lib/popperjs/popper.js'; + +var ROUND_ARROW = ''; +var BOX_CLASS = "tippy-box"; +var CONTENT_CLASS = "tippy-content"; +var BACKDROP_CLASS = "tippy-backdrop"; +var ARROW_CLASS = "tippy-arrow"; +var SVG_ARROW_CLASS = "tippy-svg-arrow"; +var TOUCH_OPTIONS = { + passive: true, + capture: true +}; +var TIPPY_DEFAULT_APPEND_TO = function TIPPY_DEFAULT_APPEND_TO() { + return document.body; +}; + +function hasOwnProperty(obj, key) { + return {}.hasOwnProperty.call(obj, key); +} + +function getValueAtIndexOrReturn(value, index, defaultValue) { + if (Array.isArray(value)) { + var v = value[index]; + return v == null ? Array.isArray(defaultValue) ? defaultValue[index] : defaultValue : v; + } + + return value; +} + +function isType(value, type) { + var str = {}.toString.call(value); + return str.indexOf('[object') === 0 && str.indexOf(type + "]") > -1; +} + +function invokeWithArgsOrReturn(value, args) { + return typeof value === 'function' ? value.apply(void 0, args) : value; +} + +function debounce(fn, ms) { + // Avoid wrapping in `setTimeout` if ms is 0 anyway + if (ms === 0) { + return fn; + } + + var timeout; + return function (arg) { + clearTimeout(timeout); + timeout = setTimeout(function () { + fn(arg); + }, ms); + }; +} + +function removeProperties(obj, keys) { + var clone = Object.assign({}, obj); + keys.forEach(function (key) { + delete clone[key]; + }); + return clone; +} + +function splitBySpaces(value) { + return value.split(/\s+/).filter(Boolean); +} + +function normalizeToArray(value) { + return [].concat(value); +} + +function pushIfUnique(arr, value) { + if (arr.indexOf(value) === -1) { + arr.push(value); + } +} + +function unique(arr) { + return arr.filter(function (item, index) { + return arr.indexOf(item) === index; + }); +} + +function getBasePlacement(placement) { + return placement.split('-')[0]; +} + +function arrayFrom(value) { + return [].slice.call(value); +} + +function removeUndefinedProps(obj) { + return Object.keys(obj).reduce(function (acc, key) { + if (obj[key] !== undefined) { + acc[key] = obj[key]; + } + + return acc; + }, {}); +} + +function div() { + return document.createElement('div'); +} + +function isElement(value) { + return ['Element', 'Fragment'].some(function (type) { + return isType(value, type); + }); +} + +function isNodeList(value) { + return isType(value, 'NodeList'); +} + +function isMouseEvent(value) { + return isType(value, 'MouseEvent'); +} + +function isReferenceElement(value) { + return !!(value && value._tippy && value._tippy.reference === value); +} + +function getArrayOfElements(value) { + if (isElement(value)) { + return [value]; + } + + if (isNodeList(value)) { + return arrayFrom(value); + } + + if (Array.isArray(value)) { + return value; + } + + return arrayFrom(document.querySelectorAll(value)); +} + +function setTransitionDuration(els, value) { + els.forEach(function (el) { + if (el) { + el.style.transitionDuration = value + "ms"; + } + }); +} + +function setVisibilityState(els, state) { + els.forEach(function (el) { + if (el) { + el.setAttribute('data-state', state); + } + }); +} + +function getOwnerDocument(elementOrElements) { + var _element$ownerDocumen; + + var _normalizeToArray = normalizeToArray(elementOrElements), + element = _normalizeToArray[0]; // Elements created via a