Compare commits
46 commits
Author | SHA1 | Date | |
---|---|---|---|
792300ab9e | |||
d323d21c19 | |||
d8023a527e | |||
44c13d0185 | |||
e99fabafb9 | |||
157fbad122 | |||
67ce0aec79 | |||
f0011e183d | |||
92d014965b | |||
c741400583 | |||
8d805cefe6 | |||
93d7b13928 | |||
c71e7fa12f | |||
a0849388a7 | |||
0bfb02f960 | |||
9206c8e41e | |||
741e638c78 | |||
13f65707e7 | |||
b0e0a19c97 | |||
9ba477174d | |||
64e48a5689 | |||
f3060bee3b | |||
344589829b | |||
f53b67fa81 | |||
0fc6a4b093 | |||
94a5fbfc51 | |||
51f9be1486 | |||
a5f2d430e1 | |||
773a659c3e | |||
9719eefed4 | |||
143196d737 | |||
9cf1428517 | |||
db665a30fa | |||
088f4faf82 | |||
c8958a8f44 | |||
9d2d49425d | |||
69a07ce076 | |||
550622e512 | |||
e4497cad8c | |||
137789e2a1 | |||
2e34244fcf | |||
673eaeb10d | |||
d18c91da23 | |||
0083624204 | |||
fd8caf98a6 | |||
4909df4b04 |
72 changed files with 3923 additions and 916 deletions
18
.envrc
Normal file
18
.envrc
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Can be debug,info,warning,error
|
||||||
|
export POLYCULECONNECT_LOG_LEVEL=debug
|
||||||
|
|
||||||
|
# Can be net,unix
|
||||||
|
export POLYCULECONNECT_SERVER_MODE=net
|
||||||
|
export POLYCULECONNECT_SERVER_HOST="0.0.0.0"
|
||||||
|
export POLYCULECONNECT_SERVER_PORT="5000"
|
||||||
|
# POLYCULECONNECT_SERVER_SOCK_PATH = ""
|
||||||
|
|
||||||
|
export POLYCULECONNECT_STORAGE_TYPE="sqlite"
|
||||||
|
export POLYCULECONNECT_STORAGE_PATH="./build/polyculeconnect.db"
|
||||||
|
# POLYCULECONNECT_STORAGE_HOST = "127.0.0.1"
|
||||||
|
# POLYCULECONNECT_STORAGE_PORT = "5432"
|
||||||
|
# POLYCULECONNECT_STORAGE_DB = "polyculeconnect"
|
||||||
|
# POLYCULECONNECT_STORAGE_USER = "polyculeconnect"
|
||||||
|
# POLYCULECONNECT_STORAGE_PASSWORD = "polyculeconnect"
|
||||||
|
# POLYCULECONNECT_STORAGE_SSL_MODE = "disable"
|
||||||
|
# POLYCULECONNECT_STORAGE_SSL_CA_FILE = ""
|
21
.forgejo/workflows/docker-build-push-master.yml
Normal file
21
.forgejo/workflows/docker-build-push-master.yml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
|
jobs:
|
||||||
|
docker-build-push:
|
||||||
|
runs-on: cth-ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: login to repository
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.faercol.me
|
||||||
|
username: ${{ secrets.DOCKER_LOGIN }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: build and push image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
push: true
|
||||||
|
tags: git.faercol.me/polyculeconnect/polyculeconnect:latest
|
21
.forgejo/workflows/docker-build-push-tag.yml
Normal file
21
.forgejo/workflows/docker-build-push-tag.yml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "**"
|
||||||
|
jobs:
|
||||||
|
docker-build-push:
|
||||||
|
runs-on: cth-ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: login to repository
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.faercol.me
|
||||||
|
username: ${{ secrets.DOCKER_LOGIN }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: build and push image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
push: true
|
||||||
|
tags: git.faercol.me/polyculeconnect/polyculeconnect:${{ gitea.ref_name }}
|
16
.forgejo/workflows/docker-build.yml
Normal file
16
.forgejo/workflows/docker-build.yml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
- "!main"
|
||||||
|
jobs:
|
||||||
|
docker-build-only:
|
||||||
|
runs-on: cth-ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: build image (build only)
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
push: false
|
||||||
|
tags: git.faercol.me/polyculeconnect/polyculeconnect
|
17
.forgejo/workflows/go-test.yml
Normal file
17
.forgejo/workflows/go-test.yml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
jobs:
|
||||||
|
go-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: 1.22
|
||||||
|
- name: Run unit tests
|
||||||
|
run: make -C polyculeconnect test
|
||||||
|
- name: Build go package
|
||||||
|
run: make -C polyculeconnect build
|
|
@ -1,57 +0,0 @@
|
||||||
steps:
|
|
||||||
docker-build-only:
|
|
||||||
image: woodpeckerci/plugin-docker-buildx
|
|
||||||
privileged: true
|
|
||||||
settings:
|
|
||||||
repo: git.faercol.me/polyculeconnect/polyculeconnect
|
|
||||||
tags: latest
|
|
||||||
dry_run: true
|
|
||||||
platforms:
|
|
||||||
- linux/amd64
|
|
||||||
# - linux/arm64
|
|
||||||
when:
|
|
||||||
- event: pull_request
|
|
||||||
- event: push
|
|
||||||
branch:
|
|
||||||
exclude: [main]
|
|
||||||
|
|
||||||
docker-build-push:
|
|
||||||
image: woodpeckerci/plugin-docker-buildx
|
|
||||||
privileged: true
|
|
||||||
settings:
|
|
||||||
repo: git.faercol.me/polyculeconnect/polyculeconnect
|
|
||||||
registry: git.faercol.me
|
|
||||||
tags: latest
|
|
||||||
username:
|
|
||||||
from_secret: git_username
|
|
||||||
password:
|
|
||||||
from_secret: git_password
|
|
||||||
platforms:
|
|
||||||
- linux/amd64
|
|
||||||
# - linux/arm64
|
|
||||||
when:
|
|
||||||
- event: push
|
|
||||||
branch: main
|
|
||||||
|
|
||||||
docker-push-tag:
|
|
||||||
image: woodpeckerci/plugin-docker-buildx
|
|
||||||
privileged: true
|
|
||||||
settings:
|
|
||||||
registry: git.faercol.me
|
|
||||||
repo: git.faercol.me/polyculeconnect/polyculeconnect
|
|
||||||
auto_tag: true
|
|
||||||
platforms:
|
|
||||||
- linux/amd64
|
|
||||||
# - linux/arm64
|
|
||||||
username:
|
|
||||||
from_secret: git_username
|
|
||||||
password:
|
|
||||||
from_secret: git_password
|
|
||||||
when:
|
|
||||||
- event: tag
|
|
||||||
|
|
||||||
depends_on:
|
|
||||||
- test
|
|
||||||
|
|
||||||
when:
|
|
||||||
event: [push, tag]
|
|
|
@ -1,13 +0,0 @@
|
||||||
steps:
|
|
||||||
go-test:
|
|
||||||
image: golang
|
|
||||||
commands:
|
|
||||||
- make -C polyculeconnect test
|
|
||||||
|
|
||||||
go-build:
|
|
||||||
image: golang
|
|
||||||
commands:
|
|
||||||
- make -C polyculeconnect build
|
|
||||||
|
|
||||||
when:
|
|
||||||
event: [push, tag]
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM --platform=$TARGETPLATFORM golang:1.20 AS builder
|
FROM --platform=$TARGETPLATFORM golang:1.21 AS builder
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ARG BUILDPLATFORM
|
ARG BUILDPLATFORM
|
||||||
WORKDIR /go/src/git.faercol.me/polyculeconnect
|
WORKDIR /go/src/git.faercol.me/polyculeconnect
|
||||||
|
@ -6,7 +6,9 @@ COPY polyculeconnect ./
|
||||||
RUN make build
|
RUN make build
|
||||||
|
|
||||||
# Replace with from scratch later on
|
# Replace with from scratch later on
|
||||||
FROM --platform=$TARGETPLATFORM alpine:latest
|
FROM --platform=$TARGETPLATFORM debian:latest
|
||||||
|
RUN apt-get update && \
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get -qq install ca-certificates
|
||||||
WORKDIR /root
|
WORKDIR /root
|
||||||
COPY --from=builder go/src/git.faercol.me/polyculeconnect/build/polyculeconnect ./
|
COPY --from=builder go/src/git.faercol.me/polyculeconnect/build/polyculeconnect ./
|
||||||
ADD polyculeconnect/robots.txt /root/
|
ADD polyculeconnect/robots.txt /root/
|
||||||
|
@ -16,4 +18,4 @@ ADD polyculeconnect/templates /root/templates/
|
||||||
VOLUME [ "/config" ]
|
VOLUME [ "/config" ]
|
||||||
|
|
||||||
ENTRYPOINT [ "./polyculeconnect" ]
|
ENTRYPOINT [ "./polyculeconnect" ]
|
||||||
CMD [ "-config", "/config/config.json" ]
|
CMD [ "serve", "--config", "/config/config.json" ]
|
||||||
|
|
47
README.md
47
README.md
|
@ -1,6 +1,6 @@
|
||||||
# PolyculeConnect
|
# PolyculeConnect
|
||||||
|
|
||||||
[![status-badge](https://ci-polycule-connect.chapoline.me/api/badges/1/status.svg)](https://ci-polycule-connect.chapoline.me/repos/1)
|
[![status-badge](https://git.faercol.me/PolyculeConnect/polycule-connect/badges/workflows/go-test.yml/badge.svg?branch=main)](https://ci-polycule-connect.chapoline.me/repos/1)
|
||||||
|
|
||||||
![Project logo](./polyculeconnect/static/img/logo-text.png)
|
![Project logo](./polyculeconnect/static/img/logo-text.png)
|
||||||
|
|
||||||
|
@ -10,21 +10,13 @@ TODO
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Here is an example config file
|
As a temporary solution, the list of backends and applications, as well as the openconnect configuration
|
||||||
|
can only be handled through the JSON config file.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"log": {
|
|
||||||
"level": "debug" // debug,info,warn,error
|
|
||||||
},
|
|
||||||
"server": {
|
|
||||||
"port": 5000, // only used in net mode
|
|
||||||
"host": "0.0.0.0", // only used in net mode
|
|
||||||
// "sock": "/your/sock.sock" // path to your unix sock if "mode" is set to "unix"
|
|
||||||
"mode": "net" // net,unix
|
|
||||||
},
|
|
||||||
"openconnect": {
|
"openconnect": {
|
||||||
"issuer": "https://polyculeconnect.domain", // hostname of your polyculeconnect server
|
"issuer": "https://polyculeconnect.domain",
|
||||||
"clients": [
|
"clients": [
|
||||||
{
|
{
|
||||||
"name": "<name>",
|
"name": "<name>",
|
||||||
|
@ -40,19 +32,44 @@ Here is an example config file
|
||||||
"id": "<unique_id>",
|
"id": "<unique_id>",
|
||||||
"name": "<human_readable_name>",
|
"name": "<human_readable_name>",
|
||||||
"local": true,
|
"local": true,
|
||||||
"type": "oidc", // must be "oidc" for now
|
"type": "oidc",
|
||||||
"config": {
|
"config": {
|
||||||
"issuer": "https://polyculeconnect.domain", // must be the same as current issuer
|
"issuer": "https://polyculeconnect.domain",
|
||||||
"clientID": "<client_id>",
|
"clientID": "<client_id>",
|
||||||
"clientSecret": "<client_secret>",
|
"clientSecret": "<client_secret>",
|
||||||
"redirectURI": "<redirect_uri>"
|
"redirectURI": "<redirect_uri>"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The rest of the configuration is handled through environment variables
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# Can be debug,info,warning,error
|
||||||
|
LOG_LEVEL = "info"
|
||||||
|
|
||||||
|
ISSUER = "http://localhost"
|
||||||
|
|
||||||
|
# Can be net,unix
|
||||||
|
SERVER_MODE = "net"
|
||||||
|
SERVER_HOST = "0.0.0.0"
|
||||||
|
SERVER_PORT = "5000"
|
||||||
|
# SERVER_SOCK_PATH = ""
|
||||||
|
|
||||||
|
STORAGE_TYPE = "sqlite"
|
||||||
|
STORAGE_FILEPATH = "./build/polyculeconnect.db"
|
||||||
|
# STORAGE_HOST = "127.0.0.1"
|
||||||
|
# STORAGE_PORT = "5432"
|
||||||
|
# STORAGE_DB = "polyculeconnect"
|
||||||
|
# STORAGE_USER = "polyculeconnect"
|
||||||
|
# STORAGE_PASSWORD = "polyculeconnect"
|
||||||
|
# STORAGE_SSL_MODE = "disable"
|
||||||
|
# STORAGE_SSL_CA_FILE = ""
|
||||||
|
```
|
||||||
|
|
||||||
You can register multiple backend and multiple clients (applications)
|
You can register multiple backend and multiple clients (applications)
|
||||||
|
|
||||||
## Running the server
|
## Running the server
|
||||||
|
|
113
polyculeconnect/cmd/app/add.go
Normal file
113
polyculeconnect/cmd/app/add.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/utils"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/services"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
appID string
|
||||||
|
appClientID string
|
||||||
|
appClientSecret string
|
||||||
|
appName string
|
||||||
|
appRedirectURIs []string
|
||||||
|
appInteractive bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var appAddCmd = &cobra.Command{
|
||||||
|
Use: "add",
|
||||||
|
Short: "Add a new app to the storage",
|
||||||
|
Long: `Add a new app to the storage.
|
||||||
|
|
||||||
|
Parameters to provide:
|
||||||
|
- id: Unique ID to represent the app in the storage
|
||||||
|
- name: Human readable name to represent the app.
|
||||||
|
- redirect-uri: list of allowed redirection URIs for this app
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
- client-id: Client ID used by the OpenIDConnect protocol, automatically generated if not provided
|
||||||
|
- client-secret: Client secret used by the OpenIDConnect protocol, automatically generated if not provided
|
||||||
|
- interactive: Pass this parameter to use a prompt to pass unset parameters (client id and secret)`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
addNewApp()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateSecret(interactive bool, currentValue, valueName string) (string, error) {
|
||||||
|
if currentValue != "" {
|
||||||
|
return currentValue, nil
|
||||||
|
}
|
||||||
|
if !interactive {
|
||||||
|
val, err := services.GenerateRandomHex(services.IDSecretSize)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate %s: %w", valueName, err)
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
fmt.Printf("Enter value for %s, use an empty value to automatically generate it.\n", valueName)
|
||||||
|
var enteredVal string
|
||||||
|
fmt.Scanln(&enteredVal)
|
||||||
|
if enteredVal == "" {
|
||||||
|
return generateSecret(false, currentValue, valueName)
|
||||||
|
}
|
||||||
|
return enteredVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addNewApp() {
|
||||||
|
c := utils.InitConfig("")
|
||||||
|
logger.Init(c.LogLevel)
|
||||||
|
|
||||||
|
s, err := db.New(*c)
|
||||||
|
if err != nil {
|
||||||
|
utils.Failf("failed to init storage: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
clientID, err := generateSecret(appInteractive, appClientID, "client ID")
|
||||||
|
if err != nil {
|
||||||
|
utils.Fail(err.Error())
|
||||||
|
}
|
||||||
|
clientSecret, err := generateSecret(appInteractive, appClientSecret, "client secret")
|
||||||
|
if err != nil {
|
||||||
|
utils.Fail(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
appConf := model.ClientConfig{
|
||||||
|
ID: clientID,
|
||||||
|
Secret: clientSecret,
|
||||||
|
Name: appName,
|
||||||
|
RedirectURIs: appRedirectURIs,
|
||||||
|
}
|
||||||
|
clt := model.Client{
|
||||||
|
ClientConfig: appConf,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ClientStorage().AddClient(context.Background(), &clt); err != nil {
|
||||||
|
utils.Failf("failed to create app: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("New app %s added.\n", appName)
|
||||||
|
printProperty("Client ID", clientID, 1)
|
||||||
|
printProperty("Client secret", clientSecret, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
appCmd.AddCommand(appAddCmd)
|
||||||
|
|
||||||
|
appAddCmd.Flags().StringVarP(&appName, "name", "n", "", "Name to represent the app")
|
||||||
|
appAddCmd.Flags().StringVarP(&appID, "id", "i", "", "ID to identify the app in the storage")
|
||||||
|
appAddCmd.Flags().StringVarP(&appClientID, "client-id", "", "", "OpenIDConnect client secret")
|
||||||
|
appAddCmd.Flags().StringVarP(&appClientSecret, "client-secret", "", "", "OpenIDConnect client secret")
|
||||||
|
appAddCmd.Flags().StringSliceVarP(&appRedirectURIs, "redirect-uri", "r", []string{}, "Allowed redirect URI")
|
||||||
|
|
||||||
|
appAddCmd.Flags().BoolVar(&appInteractive, "interactive", false, "Set the client ID and secret in an interactive way")
|
||||||
|
|
||||||
|
appAddCmd.MarkFlagRequired("name")
|
||||||
|
appAddCmd.MarkFlagRequired("redirect-uri")
|
||||||
|
}
|
28
polyculeconnect/cmd/app/app.go
Normal file
28
polyculeconnect/cmd/app/app.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var appCmd = &cobra.Command{
|
||||||
|
Use: "app",
|
||||||
|
Short: "Handle client applications",
|
||||||
|
Long: `Add, Remove or Show currently installed client applications`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func printProperty(key, value string, indent int) {
|
||||||
|
prefix := strings.Repeat("\t", indent)
|
||||||
|
keyStr := ""
|
||||||
|
if key != "" {
|
||||||
|
keyStr = key + ": "
|
||||||
|
}
|
||||||
|
fmt.Printf("%s- %s%s\n", prefix, keyStr, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cmd.RootCmd.AddCommand(appCmd)
|
||||||
|
}
|
40
polyculeconnect/cmd/app/remove.go
Normal file
40
polyculeconnect/cmd/app/remove.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/utils"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var appRemoveCmd = &cobra.Command{
|
||||||
|
Use: "remove <app_client_id>",
|
||||||
|
Short: "Remove an app",
|
||||||
|
Long: `Remove the app with the given ID from the database.`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
removeApp(args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeApp(appID string) {
|
||||||
|
c := utils.InitConfig("")
|
||||||
|
logger.Init(c.LogLevel)
|
||||||
|
|
||||||
|
s, err := db.New(*c)
|
||||||
|
if err != nil {
|
||||||
|
utils.Failf("failed to init storage: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ClientStorage().DeleteClient(context.Background(), appID); err != nil {
|
||||||
|
utils.Failf("Failed to remove app: %s", err.Error())
|
||||||
|
}
|
||||||
|
fmt.Println("App deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
appCmd.AddCommand(appRemoveCmd)
|
||||||
|
}
|
76
polyculeconnect/cmd/app/show.go
Normal file
76
polyculeconnect/cmd/app/show.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/utils"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db/client"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var appShowCmd = &cobra.Command{
|
||||||
|
Use: "show [app_id]",
|
||||||
|
Short: "Display installed apps",
|
||||||
|
Long: `Display the configuration for the apps.
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
- app-id: id of the application to display. If empty, display the list of available apps instead`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
c := utils.InitConfig("")
|
||||||
|
logger.Init(c.LogLevel)
|
||||||
|
|
||||||
|
s, err := db.New(*c)
|
||||||
|
if err != nil {
|
||||||
|
utils.Failf("failed to init storage: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
showApp(args[0], s.ClientStorage())
|
||||||
|
} else {
|
||||||
|
listApps(s.ClientStorage())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func showApp(appID string, st client.ClientDB) {
|
||||||
|
appConfig, err := st.GetClientByID(context.Background(), appID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, client.ErrNotFound) {
|
||||||
|
utils.Failf("App with ID %s does not exist\n", appID)
|
||||||
|
}
|
||||||
|
utils.Failf("Failed to get config for app %s: %q\n", appID, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("App config:")
|
||||||
|
printProperty("Name", appConfig.Name, 1)
|
||||||
|
printProperty("ID", appConfig.ID, 1)
|
||||||
|
printProperty("Client secret", appConfig.Secret, 1)
|
||||||
|
printProperty("Redirect URIs", "", 1)
|
||||||
|
for _, uri := range appConfig.RedirectURIs() {
|
||||||
|
printProperty("", uri, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listApps(st client.ClientDB) {
|
||||||
|
apps, err := st.GetAllClients(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
utils.Failf("Failed to list apps: %q\n", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(apps) == 0 {
|
||||||
|
fmt.Println("No app configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, b := range apps {
|
||||||
|
fmt.Printf("\t - %s: (%s)\n", b.ID, b.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
appCmd.AddCommand(appShowCmd)
|
||||||
|
}
|
101
polyculeconnect/cmd/backend/add.go
Normal file
101
polyculeconnect/cmd/backend/add.go
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/utils"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
backendID string
|
||||||
|
backendName string
|
||||||
|
backendIssuer string
|
||||||
|
backendClientID string
|
||||||
|
backendClientSecret string
|
||||||
|
backendScopes []string
|
||||||
|
)
|
||||||
|
|
||||||
|
var backendAddCmd = &cobra.Command{
|
||||||
|
Use: "add",
|
||||||
|
Short: "Add a new backend to the storage",
|
||||||
|
Long: `Add a new backend to the storage.
|
||||||
|
|
||||||
|
Parameters to provide:
|
||||||
|
- id: Unique ID to represent the backend in the storage
|
||||||
|
- name: Human readable name to represent the backend. It will be used by
|
||||||
|
the user in the authentication page to select a backend during
|
||||||
|
authentication
|
||||||
|
- issuer: Full hostname of the OIDC provider, e.g. 'https://github.com'
|
||||||
|
- client-id: OIDC Client ID for the backend
|
||||||
|
- client-secret OIDC Client secret for the backend`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
addNewBackend()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func scopesValid(scopes []string) bool {
|
||||||
|
for _, s := range scopes {
|
||||||
|
if s == "openid" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func addNewBackend() {
|
||||||
|
c := utils.InitConfig("")
|
||||||
|
logger.Init(c.LogLevel)
|
||||||
|
|
||||||
|
s, err := db.New(*c)
|
||||||
|
if err != nil {
|
||||||
|
utils.Failf("failed to init storage: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if backendClientID == "" {
|
||||||
|
utils.Fail("Empty client ID")
|
||||||
|
}
|
||||||
|
if backendClientSecret == "" {
|
||||||
|
utils.Fail("Empty client secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !scopesValid(backendScopes) {
|
||||||
|
utils.Failf("Invalid list of scopes %s", strings.Join(backendScopes, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
backendIDUUID := uuid.New()
|
||||||
|
|
||||||
|
backendConf := model.Backend{
|
||||||
|
ID: backendIDUUID,
|
||||||
|
Name: backendName,
|
||||||
|
Config: model.BackendOIDCConfig{
|
||||||
|
ClientID: backendClientID,
|
||||||
|
ClientSecret: backendClientSecret,
|
||||||
|
Issuer: backendIssuer,
|
||||||
|
RedirectURI: c.RedirectURI(),
|
||||||
|
Scopes: backendScopes,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := s.BackendStorage().AddBackend(context.Background(), &backendConf); err != nil {
|
||||||
|
utils.Failf("Failed to add new backend to storage: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("New backend %s added.\n", backendName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
backendCmd.AddCommand(backendAddCmd)
|
||||||
|
|
||||||
|
backendAddCmd.Flags().StringVarP(&backendID, "id", "i", "", "ID to identify the backend in the storage")
|
||||||
|
backendAddCmd.Flags().StringVarP(&backendName, "name", "n", "", "Name to represent the backend")
|
||||||
|
backendAddCmd.Flags().StringVarP(&backendIssuer, "issuer", "d", "", "Full hostname of the backend")
|
||||||
|
backendAddCmd.Flags().StringVarP(&backendClientID, "client-id", "", "", "OIDC Client ID for the backend")
|
||||||
|
backendAddCmd.Flags().StringVarP(&backendClientSecret, "client-secret", "", "", "OIDC Client secret for the backend")
|
||||||
|
backendAddCmd.Flags().StringArrayVarP(&backendScopes, "scopes", "s", []string{"openid", "profile", "email"}, "OIDC Scopes asked to the backend")
|
||||||
|
}
|
22
polyculeconnect/cmd/backend/backend.go
Normal file
22
polyculeconnect/cmd/backend/backend.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var backendCmd = &cobra.Command{
|
||||||
|
Use: "backend",
|
||||||
|
Short: "Handle authentication backends",
|
||||||
|
Long: `Add, Remove or Show currently installed authentication backends`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func printProperty(key, value string) {
|
||||||
|
fmt.Printf("\t- %s: %s\n", key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cmd.RootCmd.AddCommand(backendCmd)
|
||||||
|
}
|
42
polyculeconnect/cmd/backend/remove.go
Normal file
42
polyculeconnect/cmd/backend/remove.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/utils"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var backendRemoveCmd = &cobra.Command{
|
||||||
|
Use: "remove <backend_id>",
|
||||||
|
Short: "Remove a backend",
|
||||||
|
Long: `Remove the backend with the given ID from the database.`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
removeBackend(args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeBackend(backendIDStr string) {
|
||||||
|
backendID, err := uuid.Parse(backendIDStr)
|
||||||
|
if err != nil {
|
||||||
|
utils.Failf("Invalid UUID format: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := db.New(*utils.InitConfig(""))
|
||||||
|
if err != nil {
|
||||||
|
utils.Failf("Failed to init storage: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.BackendStorage().DeleteBackend(context.Background(), backendID); err != nil {
|
||||||
|
utils.Failf("Failed to remove backend: %s", err.Error())
|
||||||
|
}
|
||||||
|
fmt.Println("Backend deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
backendCmd.AddCommand(backendRemoveCmd)
|
||||||
|
}
|
76
polyculeconnect/cmd/backend/show.go
Normal file
76
polyculeconnect/cmd/backend/show.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/utils"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db/backend"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var backendShowCmd = &cobra.Command{
|
||||||
|
Use: "show [backend_id]",
|
||||||
|
Short: "Display installed backends",
|
||||||
|
Long: `Display the configuration for the backends.
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
- app-id: id of the backend to display. If empty, display the list of available backends instead`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
conf := utils.InitConfig("")
|
||||||
|
s, err := db.New(*conf)
|
||||||
|
if err != nil {
|
||||||
|
utils.Failf("Failed to init storage: %s", err.Error())
|
||||||
|
}
|
||||||
|
logger.Init(conf.LogLevel)
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
showBackend(args[0], s.BackendStorage())
|
||||||
|
} else {
|
||||||
|
listBackends(s.BackendStorage())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func showBackend(backendName string, st backend.BackendDB) {
|
||||||
|
backendConfig, err := st.GetBackendByName(context.Background(), backendName)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, backend.ErrNotFound) {
|
||||||
|
utils.Failf("Backend with name %s does not exist\n", backendName)
|
||||||
|
}
|
||||||
|
utils.Failf("Failed to get config for backend %s: %q\n", backendName, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Backend config:")
|
||||||
|
printProperty("ID", backendConfig.ID.String())
|
||||||
|
printProperty("Name", backendConfig.Name)
|
||||||
|
printProperty("Issuer", backendConfig.Config.Issuer)
|
||||||
|
printProperty("Client ID", backendConfig.Config.ClientID)
|
||||||
|
printProperty("Client secret", backendConfig.Config.ClientSecret)
|
||||||
|
printProperty("Redirect URI", backendConfig.Config.RedirectURI)
|
||||||
|
printProperty("Scopes", strings.Join(backendConfig.Config.Scopes, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func listBackends(backendStorage backend.BackendDB) {
|
||||||
|
backends, err := backendStorage.GetAllBackends(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
utils.Failf("Failed to list backends: %q\n", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(backends) == 0 {
|
||||||
|
fmt.Println("No backend configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, b := range backends {
|
||||||
|
fmt.Printf("\t - %s: (%s) - %s\n", b.ID, b.Name, b.Config.Issuer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
backendCmd.AddCommand(backendShowCmd)
|
||||||
|
}
|
55
polyculeconnect/cmd/db/connect.go
Normal file
55
polyculeconnect/cmd/db/connect.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/utils"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// connectCmd represents the db connect command
|
||||||
|
var connectCmd = &cobra.Command{
|
||||||
|
Use: "connect",
|
||||||
|
Short: "Connect to the database",
|
||||||
|
Long: `Connect to the database.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
conf := utils.InitConfig("")
|
||||||
|
if err := connectToDB(conf); err != nil {
|
||||||
|
utils.Failf("Failed to connect to DB: %s", err.Error())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectSQLite(conf config.StorageConfig) error {
|
||||||
|
path, err := exec.LookPath("sqlite3")
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, exec.ErrNotFound) {
|
||||||
|
return errors.New("sqlite3 not installed")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to find sqlite3 executable: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := syscall.Exec(path, []string{path, conf.File}, nil); err != nil {
|
||||||
|
return fmt.Errorf("failed to run sqlite3 command: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectToDB(conf *config.AppConfig) error {
|
||||||
|
switch conf.StorageType {
|
||||||
|
case string(config.Memory):
|
||||||
|
return errors.New("no DB associated with memory storage")
|
||||||
|
case string(config.SQLite):
|
||||||
|
return connectSQLite(conf.StorageConfig)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported storage type %q", conf.StorageType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dbCmd.AddCommand(connectCmd)
|
||||||
|
}
|
17
polyculeconnect/cmd/db/db.go
Normal file
17
polyculeconnect/cmd/db/db.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dbCmd represents the db command
|
||||||
|
var dbCmd = &cobra.Command{
|
||||||
|
Use: "db",
|
||||||
|
Short: "Manage the database",
|
||||||
|
Long: `Manage the database.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cmd.RootCmd.AddCommand(dbCmd)
|
||||||
|
}
|
50
polyculeconnect/cmd/db/destroy.go
Normal file
50
polyculeconnect/cmd/db/destroy.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/utils"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// destroyCmd represents the db destroy command
|
||||||
|
var destroyCmd = &cobra.Command{
|
||||||
|
Use: "destroy",
|
||||||
|
Short: "Completely delete the current database",
|
||||||
|
Long: `Delete the current database.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
conf := utils.InitConfig("")
|
||||||
|
if err := deleteDB(conf); err != nil {
|
||||||
|
utils.Failf("Failed to connect to DB: %s", err.Error())
|
||||||
|
}
|
||||||
|
fmt.Println("DB deleted")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteSqliteDB(path string) error {
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) { // if the file has already been deleted we don't want to fail here
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to delete SQLite file: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteDB(conf *config.AppConfig) error {
|
||||||
|
switch conf.StorageType {
|
||||||
|
case string(config.Memory):
|
||||||
|
return errors.New("no DB to delete in memory mode")
|
||||||
|
case string(config.SQLite):
|
||||||
|
return deleteSqliteDB(conf.StorageConfig.File)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported storage type %q", conf.StorageType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dbCmd.AddCommand(destroyCmd)
|
||||||
|
}
|
96
polyculeconnect/cmd/db/migrate.go
Normal file
96
polyculeconnect/cmd/db/migrate.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/utils"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/config"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db"
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// migrateCmd represents the db migrate command
|
||||||
|
var migrateCmd = &cobra.Command{
|
||||||
|
Use: "migrate",
|
||||||
|
Short: "Run the database migrations",
|
||||||
|
Long: `Run the database migrations.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
conf := utils.InitConfig("")
|
||||||
|
up, nbSteps := parseArgs(args)
|
||||||
|
if err := runMigrations(conf, up, nbSteps); err != nil {
|
||||||
|
utils.Failf("Failed to run migrations: %s", err.Error())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseArgs(args []string) (bool, int) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return true, 0
|
||||||
|
}
|
||||||
|
var actionUp bool
|
||||||
|
switch args[0] {
|
||||||
|
case "up":
|
||||||
|
actionUp = true
|
||||||
|
case "down":
|
||||||
|
actionUp = false
|
||||||
|
default:
|
||||||
|
actionUp = true
|
||||||
|
}
|
||||||
|
|
||||||
|
nbSteps := 0
|
||||||
|
if len(args) > 1 {
|
||||||
|
var err error
|
||||||
|
nbSteps, err = strconv.Atoi(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return actionUp, 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionUp, nbSteps
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMigrations(conf *config.AppConfig, up bool, nbSteps int) error {
|
||||||
|
storage, err := db.New(*conf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to db: %w", err)
|
||||||
|
}
|
||||||
|
driver, err := sqlite3.WithInstance(storage.DB(), &sqlite3.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open sqlite3 driver: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := migrate.NewWithDatabaseInstance("file://migrations", "", driver)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to init migrator: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if nbSteps > 0 {
|
||||||
|
multiplier := 1
|
||||||
|
if !up {
|
||||||
|
multiplier = -1
|
||||||
|
}
|
||||||
|
return m.Steps(multiplier * nbSteps)
|
||||||
|
}
|
||||||
|
if up {
|
||||||
|
return m.Up()
|
||||||
|
}
|
||||||
|
return m.Down()
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dbCmd.AddCommand(migrateCmd)
|
||||||
|
|
||||||
|
// Here you will define your flags and configuration settings.
|
||||||
|
|
||||||
|
// Cobra supports Persistent Flags which will work for this command
|
||||||
|
// and all subcommands, e.g.:
|
||||||
|
// dbCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||||
|
|
||||||
|
// Cobra supports local flags which will only run when this command
|
||||||
|
// is called directly, e.g.:
|
||||||
|
// dbCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||||
|
}
|
28
polyculeconnect/cmd/root.go
Normal file
28
polyculeconnect/cmd/root.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RootCmd represents the base command when called without any subcommands
|
||||||
|
var RootCmd = &cobra.Command{
|
||||||
|
Use: "polyculeconnect",
|
||||||
|
Short: "You're in their DMs, I'm in their SSO",
|
||||||
|
Long: `PolyculeConnect is a SSO OpenIDConnect provider which allows multiple authentication backends,
|
||||||
|
and enables authentication federation among several infrastructures.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
|
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||||
|
func Execute() {
|
||||||
|
err := RootCmd.Execute()
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.Root().CompletionOptions.DisableDefaultCmd = true
|
||||||
|
}
|
126
polyculeconnect/cmd/serve/serve.go
Normal file
126
polyculeconnect/cmd/serve/serve.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/utils"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/client"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/middlewares"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/storage"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/server"
|
||||||
|
"github.com/go-jose/go-jose/v4"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/op"
|
||||||
|
"go.uber.org/zap/exp/zapslog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configPath string
|
||||||
|
|
||||||
|
const stopTimeout = 10 * time.Second
|
||||||
|
|
||||||
|
// serveCmd represents the serve command
|
||||||
|
var serveCmd = &cobra.Command{
|
||||||
|
Use: "serve",
|
||||||
|
Short: "Start the web server",
|
||||||
|
Long: `Start the PolyculeConnect web server using the configuration defined through environment
|
||||||
|
variables`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
serve()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func serve() {
|
||||||
|
mainCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
conf := utils.InitConfig(configPath)
|
||||||
|
logger.Init(conf.LogLevel)
|
||||||
|
logger.L.Infof("Initialized logger with level %v", conf.LogLevel)
|
||||||
|
|
||||||
|
userDB, err := db.New(*conf)
|
||||||
|
if err != nil {
|
||||||
|
utils.Failf("failed to init user DB: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
backends := map[uuid.UUID]*client.OIDCClient{}
|
||||||
|
key := sha256.Sum256([]byte("test"))
|
||||||
|
|
||||||
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
utils.Failf("Failed to generate private key: %s", err)
|
||||||
|
}
|
||||||
|
signingKey := model.Key{
|
||||||
|
PrivateKey: privateKey,
|
||||||
|
KeyID: uuid.New(),
|
||||||
|
SigningAlg: jose.RS256,
|
||||||
|
}
|
||||||
|
|
||||||
|
st := storage.Storage{LocalStorage: userDB, InitializedBackends: backends, Key: &signingKey}
|
||||||
|
opConf := op.Config{
|
||||||
|
CryptoKey: key,
|
||||||
|
CodeMethodS256: false,
|
||||||
|
GrantTypeRefreshToken: true,
|
||||||
|
}
|
||||||
|
slogger := slog.New(zapslog.NewHandler(logger.L.Desugar().Core(), nil))
|
||||||
|
// slogger :=
|
||||||
|
options := []op.Option{
|
||||||
|
op.WithAllowInsecure(),
|
||||||
|
op.WithLogger(slogger),
|
||||||
|
op.WithHttpInterceptors(middlewares.WithBackendFromRequestMiddleware),
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := op.NewProvider(&opConf, &st, op.StaticIssuer(conf.Issuer), options...)
|
||||||
|
if err != nil {
|
||||||
|
utils.Failf("failed to init OIDC provider: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L.Info("Initializing server")
|
||||||
|
s, err := server.New(conf, provider, &st, logger.L)
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Fatalf("Failed to initialize server: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.Run(mainCtx)
|
||||||
|
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, os.Interrupt)
|
||||||
|
|
||||||
|
logger.L.Info("Application successfully started")
|
||||||
|
|
||||||
|
logger.L.Debug("Waiting for stop signal")
|
||||||
|
select {
|
||||||
|
case <-s.Done():
|
||||||
|
logger.L.Fatal("Unexpected exit from server")
|
||||||
|
case <-c:
|
||||||
|
logger.L.Info("Stopping main application")
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L.Debugf("Waiting %v for all daemons to stop", stopTimeout)
|
||||||
|
select {
|
||||||
|
case <-time.After(stopTimeout):
|
||||||
|
logger.L.Fatalf("Failed to stop all daemons in the expected time")
|
||||||
|
case <-s.Done():
|
||||||
|
logger.L.Info("web server successfully stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L.Info("Application successfully stopped")
|
||||||
|
os.Exit(0)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cmd.RootCmd.AddCommand(serveCmd)
|
||||||
|
serveCmd.Flags().StringVarP(&configPath, "config", "c", "config.json", "Path to the JSON configuration file")
|
||||||
|
}
|
29
polyculeconnect/cmd/utils/utils.go
Normal file
29
polyculeconnect/cmd/utils/utils.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fail displays the given error to stderr and exits the program with a returncode 1
|
||||||
|
func Fail(errMsg string) {
|
||||||
|
fmt.Fprintln(os.Stderr, errMsg)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail displays the given formatted error to stderr and exits the program with a returncode 1
|
||||||
|
func Failf(msg string, args ...any) {
|
||||||
|
fmt.Fprintf(os.Stderr, msg+"\n", args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitConfig inits the configuration, and fails the program if an error occurs
|
||||||
|
func InitConfig(configPath string) *config.AppConfig {
|
||||||
|
conf, err := config.New(configPath)
|
||||||
|
if err != nil {
|
||||||
|
Failf("Failed to load the configuration: %s", err.Error())
|
||||||
|
}
|
||||||
|
return conf
|
||||||
|
}
|
|
@ -7,37 +7,20 @@ import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/dexidp/dex/connector/oidc"
|
"github.com/kelseyhightower/envconfig"
|
||||||
"github.com/dexidp/dex/storage"
|
"go.uber.org/zap"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListeningMode int64
|
const (
|
||||||
|
envConfigPrefix = "POLYCULECONNECT"
|
||||||
|
DefaultConfigPath = "/etc/polyculeconnect.json"
|
||||||
|
)
|
||||||
|
|
||||||
func (lm ListeningMode) String() string {
|
type ListeningMode string
|
||||||
mapping := map[ListeningMode]string{
|
|
||||||
ModeNet: "net",
|
|
||||||
ModeUnix: "unix",
|
|
||||||
}
|
|
||||||
val := mapping[lm]
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
func listeningModeFromString(rawVal string) (ListeningMode, error) {
|
|
||||||
mapping := map[string]ListeningMode{
|
|
||||||
"unix": ModeUnix,
|
|
||||||
"net": ModeNet,
|
|
||||||
}
|
|
||||||
if typedVal, ok := mapping[rawVal]; !ok {
|
|
||||||
return ModeNet, fmt.Errorf("invalid listening mode %s", rawVal)
|
|
||||||
} else {
|
|
||||||
return typedVal, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ModeUnix ListeningMode = iota
|
ModeUnix ListeningMode = "unix"
|
||||||
ModeNet
|
ModeNet ListeningMode = "net"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BackendConfigType string
|
type BackendConfigType string
|
||||||
|
@ -47,112 +30,194 @@ const (
|
||||||
SQLite BackendConfigType = "sqlite"
|
SQLite BackendConfigType = "sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultLogLevel = zap.InfoLevel
|
||||||
|
|
||||||
|
defaultServerMode = ModeNet
|
||||||
|
defaultServerHost = "0.0.0.0"
|
||||||
|
defaultServerPort = 5000
|
||||||
|
defaultServerSocket = ""
|
||||||
|
defaultServerStaticDir = "./"
|
||||||
|
|
||||||
|
defaultIssuer = "http://localhost:5000"
|
||||||
|
|
||||||
|
defaultStorageType = Memory
|
||||||
|
defaultStorageFile = "./polyculeconnect.db"
|
||||||
|
defaultStorageHost = "127.0.0.1"
|
||||||
|
defaultStoragePort = 5432
|
||||||
|
defaultStorageDB = "polyculeconnect"
|
||||||
|
defaultStorageUser = "polyculeconnect"
|
||||||
|
defaultStoragePassword = "polyculeconnect"
|
||||||
|
defaultStorageSSLMode = "disable"
|
||||||
|
defaultStorageSSLCaFile = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deprecated: remove when we finally drop the JSON config
|
||||||
type BackendConfig struct {
|
type BackendConfig struct {
|
||||||
Config *oidc.Config `json:"config"`
|
Name string `json:"name"`
|
||||||
Name string `json:"name"`
|
ID string `json:"ID"`
|
||||||
ID string `json:"ID"`
|
Local bool `json:"local"`
|
||||||
Type BackendConfigType `json:"type"`
|
|
||||||
Local bool `json:"local"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenConnectConfig struct {
|
type SSLStorageConfig struct {
|
||||||
ClientConfigs []*storage.Client `json:"clients"`
|
Mode string `json:"mode" envconfig:"STORAGE_SSL_MODE"`
|
||||||
BackendConfigs []*BackendConfig `json:"backends"`
|
CaFile string `json:"ca_file" envconfig:"STORAGE_SSL_CA_FILE"`
|
||||||
Issuer string `json:"issuer"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type StorageConfig struct {
|
type StorageConfig struct {
|
||||||
File string `json:"file"`
|
File string `envconfig:"STORAGE_PATH"`
|
||||||
Host string `json:"host"`
|
Host string `envconfig:"STORAGE_HOST"`
|
||||||
Port int `json:"port"`
|
Port int `envconfig:"STORAGE_PORT"`
|
||||||
Database string `json:"database"`
|
Database string `envconfig:"STORAGE_DATABASE"`
|
||||||
User string `json:"user"`
|
User string `envconfig:"STORAGE_USER"`
|
||||||
Password string `json:"password"`
|
Password string `envconfig:"STORAGE_PASSWORD"`
|
||||||
Ssl struct {
|
SSL SSLStorageConfig
|
||||||
Mode string `json:"mode"`
|
}
|
||||||
CaFile string `json:"caFile"`
|
|
||||||
} `json:"ssl"`
|
type logConfig struct {
|
||||||
|
Level string `json:"level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfig struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Sock string `json:"sock"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonStorageConfig struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
File string `json:"path"`
|
||||||
|
Host string `json:"host" `
|
||||||
|
Port int `json:"port"`
|
||||||
|
Database string `json:"database" `
|
||||||
|
User string `json:"user" `
|
||||||
|
Password string `json:"password" `
|
||||||
|
SSL SSLStorageConfig `json:"ssl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type jsonConf struct {
|
type jsonConf struct {
|
||||||
Log struct {
|
LogConfig logConfig `json:"log"`
|
||||||
Level string `json:"level"`
|
ServerConfig serverConfig `json:"server"`
|
||||||
} `json:"log"`
|
StorageConfig jsonStorageConfig `json:"storage"`
|
||||||
Server struct {
|
}
|
||||||
Host string `json:"host"`
|
|
||||||
Port int `json:"port"`
|
func (c *jsonConf) initValues(ac *AppConfig) {
|
||||||
Mode string `json:"mode"`
|
c.LogConfig = logConfig{Level: ac.LogLevel.String()}
|
||||||
SockPath string `json:"sock"`
|
c.ServerConfig = serverConfig{
|
||||||
} `json:"server"`
|
Mode: string(ac.ServerMode),
|
||||||
Storage struct {
|
Host: ac.Host,
|
||||||
StorageType string `json:"type"`
|
Port: ac.Port,
|
||||||
Config *StorageConfig `json:"config"`
|
Sock: ac.SockPath,
|
||||||
} `json:"storage"`
|
}
|
||||||
OpenConnectConfig *OpenConnectConfig `json:"openconnect"`
|
c.StorageConfig = jsonStorageConfig{
|
||||||
|
Type: ac.StorageType,
|
||||||
|
File: ac.StorageConfig.File,
|
||||||
|
Host: ac.StorageConfig.Host,
|
||||||
|
Port: ac.StorageConfig.Port,
|
||||||
|
Database: ac.StorageConfig.Database,
|
||||||
|
User: ac.StorageConfig.User,
|
||||||
|
Password: ac.StorageConfig.Password,
|
||||||
|
SSL: ac.StorageConfig.SSL,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
LogLevel logrus.Level
|
LogLevel zap.AtomicLevel `envconfig:"LOG_LEVEL"`
|
||||||
ServerMode ListeningMode
|
ServerMode ListeningMode `envconfig:"SERVER_MODE"`
|
||||||
Host string
|
Host string `envconfig:"SERVER_HOST"`
|
||||||
Port int
|
Port int `envconfig:"SERVER_PORT"`
|
||||||
SockPath string
|
SockPath string `envconfig:"SERVER_SOCK"`
|
||||||
StorageType string
|
StorageType string `envconfig:"STORAGE_TYPE"`
|
||||||
StorageConfig *StorageConfig
|
StorageConfig StorageConfig
|
||||||
OpenConnectConfig *OpenConnectConfig
|
Issuer string
|
||||||
|
StaticDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseLevel(lvlStr string) logrus.Level {
|
func defaultConfig() AppConfig {
|
||||||
for _, lvl := range logrus.AllLevels {
|
return AppConfig{
|
||||||
if lvl.String() == lvlStr {
|
LogLevel: zap.NewAtomicLevelAt(defaultLogLevel),
|
||||||
return lvl
|
ServerMode: defaultServerMode,
|
||||||
}
|
Host: defaultServerHost,
|
||||||
|
Port: defaultServerPort,
|
||||||
|
SockPath: defaultServerSocket,
|
||||||
|
StorageType: string(defaultStorageType),
|
||||||
|
StorageConfig: StorageConfig{
|
||||||
|
File: defaultStorageFile,
|
||||||
|
Host: defaultStorageHost,
|
||||||
|
Port: defaultServerPort,
|
||||||
|
Database: defaultStorageDB,
|
||||||
|
User: defaultStorageUser,
|
||||||
|
Password: defaultStoragePassword,
|
||||||
|
SSL: SSLStorageConfig{
|
||||||
|
Mode: defaultStorageSSLMode,
|
||||||
|
CaFile: defaultStorageSSLCaFile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Issuer: defaultIssuer,
|
||||||
|
StaticDir: defaultServerStaticDir,
|
||||||
}
|
}
|
||||||
return logrus.InfoLevel
|
}
|
||||||
|
|
||||||
|
func parseLevel(lvlStr string) zap.AtomicLevel {
|
||||||
|
var res zap.AtomicLevel
|
||||||
|
if err := res.UnmarshalText([]byte(lvlStr)); err != nil {
|
||||||
|
return zap.NewAtomicLevelAt(zap.InfoLevel)
|
||||||
|
}
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *AppConfig) UnmarshalJSON(data []byte) error {
|
func (ac *AppConfig) UnmarshalJSON(data []byte) error {
|
||||||
var jsonConf jsonConf
|
var jsonConf jsonConf
|
||||||
|
jsonConf.initValues(ac)
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &jsonConf); err != nil {
|
if err := json.Unmarshal(data, &jsonConf); err != nil {
|
||||||
return fmt.Errorf("failed to read JSON: %w", err)
|
return fmt.Errorf("failed to read JSON: %w", err)
|
||||||
}
|
}
|
||||||
ac.LogLevel = parseLevel(jsonConf.Log.Level)
|
|
||||||
|
|
||||||
lm, err := listeningModeFromString(jsonConf.Server.Mode)
|
ac.LogLevel = parseLevel(jsonConf.LogConfig.Level)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse server listening mode: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ac.ServerMode = lm
|
ac.ServerMode = ListeningMode(jsonConf.ServerConfig.Mode)
|
||||||
ac.SockPath = jsonConf.Server.SockPath
|
ac.Host = jsonConf.ServerConfig.Host
|
||||||
ac.Host = jsonConf.Server.Host
|
ac.Port = jsonConf.ServerConfig.Port
|
||||||
ac.Port = jsonConf.Server.Port
|
ac.SockPath = jsonConf.ServerConfig.Sock
|
||||||
ac.OpenConnectConfig = jsonConf.OpenConnectConfig
|
|
||||||
ac.StorageType = jsonConf.Storage.StorageType
|
ac.StorageType = jsonConf.StorageConfig.Type
|
||||||
ac.StorageConfig = jsonConf.Storage.Config
|
ac.StorageConfig.File = jsonConf.StorageConfig.File
|
||||||
|
ac.StorageConfig.Host = jsonConf.StorageConfig.Host
|
||||||
|
ac.StorageConfig.Port = jsonConf.StorageConfig.Port
|
||||||
|
ac.StorageConfig.Database = jsonConf.StorageConfig.Database
|
||||||
|
ac.StorageConfig.User = jsonConf.StorageConfig.User
|
||||||
|
ac.StorageConfig.Password = jsonConf.StorageConfig.Password
|
||||||
|
ac.StorageConfig.SSL = jsonConf.StorageConfig.SSL
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultConfig AppConfig = AppConfig{
|
func (ac *AppConfig) RedirectURI() string {
|
||||||
LogLevel: logrus.InfoLevel,
|
return ac.Issuer + "/callback"
|
||||||
ServerMode: ModeNet,
|
|
||||||
Host: "0.0.0.0",
|
|
||||||
Port: 5000,
|
|
||||||
StorageType: "memory",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(filepath string) (*AppConfig, error) {
|
func New(filepath string) (*AppConfig, error) {
|
||||||
|
conf := defaultConfig()
|
||||||
content, err := os.ReadFile(filepath)
|
content, err := os.ReadFile(filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
conf := defaultConfig
|
return nil, fmt.Errorf("failed to read config file %q: %w", filepath, err)
|
||||||
return &conf, nil
|
}
|
||||||
|
} else {
|
||||||
|
if err := json.Unmarshal(content, &conf); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("failed to read config file %q: %w", filepath, err)
|
|
||||||
}
|
}
|
||||||
var conf AppConfig
|
if err := envconfig.Process(envConfigPrefix, &conf); err != nil {
|
||||||
if err := json.Unmarshal(content, &conf); err != nil {
|
return nil, err
|
||||||
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
|
||||||
}
|
}
|
||||||
|
if err := envconfig.Process(envConfigPrefix, &conf.StorageConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := envconfig.Process(envConfigPrefix, &conf.StorageConfig.SSL); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &conf, nil
|
return &conf, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,135 +3,218 @@ package config
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestListeningModeString(t *testing.T) {
|
func initJson(t *testing.T, content string) string {
|
||||||
assert.Equal(t, "net", ModeNet.String(), "Unexpected string value")
|
tmpPath := t.TempDir()
|
||||||
assert.Equal(t, "unix", ModeUnix.String(), "Unexpected string value")
|
confPath := path.Join(tmpPath, "config.json")
|
||||||
|
err := os.WriteFile(confPath, []byte(content), 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return confPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func setEnv(t *testing.T, envVars map[string]string) {
|
||||||
|
for key, val := range envVars {
|
||||||
|
t.Setenv(key, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove all polyculeconnect environment variables from the environment and put them back after the test
|
||||||
|
func clearEnv(t *testing.T) {
|
||||||
|
for _, envvar := range os.Environ() {
|
||||||
|
if strings.HasPrefix(envvar, "POLYCULECONNECT") {
|
||||||
|
splitVar := strings.SplitN(envvar, "=", 2)
|
||||||
|
require.True(t, len(splitVar) >= 2, "invalid format for envvar %q", envvar)
|
||||||
|
require.NoError(t, os.Unsetenv(splitVar[0]), "failed to unset var %q", splitVar[0])
|
||||||
|
t.Cleanup(func() {
|
||||||
|
os.Setenv(splitVar[0], splitVar[1])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test returning a default config when providing a path that does not exist
|
|
||||||
func TestDefault(t *testing.T) {
|
func TestDefault(t *testing.T) {
|
||||||
conf, err := New("/this/path/does/not/exist")
|
clearEnv(t)
|
||||||
if assert.Nil(t, err, "Unexpected error") {
|
|
||||||
assert.Equal(t, defaultConfig, *conf, "Unexpected config")
|
t.Run("no file", func(t *testing.T) {
|
||||||
}
|
conf, err := New("/this/path/does/not/exist")
|
||||||
}
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, defaultConfig(), *conf)
|
||||||
// Test creating a valid config (net mode)
|
})
|
||||||
func TestOKNet(t *testing.T) {
|
|
||||||
tmpPath := t.TempDir()
|
t.Run("empty config", func(t *testing.T) {
|
||||||
content := `{
|
confPath := initJson(t, `{}`)
|
||||||
"log": {
|
conf, err := New(confPath)
|
||||||
"level": "error"
|
require.NoError(t, err)
|
||||||
},
|
assert.Equal(t, defaultConfig(), *conf)
|
||||||
"server": {
|
})
|
||||||
"mode": "net",
|
|
||||||
"host": "127.0.0.1",
|
|
||||||
"port": 8888
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
confPath := path.Join(tmpPath, "config.json")
|
|
||||||
require.Nil(t, os.WriteFile(confPath, []byte(content), 0o644), "Failed to write config")
|
|
||||||
|
|
||||||
expectedConf := AppConfig{
|
|
||||||
LogLevel: logrus.ErrorLevel,
|
|
||||||
ServerMode: ModeNet,
|
|
||||||
Host: "127.0.0.1",
|
|
||||||
Port: 8888,
|
|
||||||
}
|
|
||||||
conf, err := New(confPath)
|
|
||||||
if assert.Nil(t, err, "Unexpected error") {
|
|
||||||
assert.Equal(t, expectedConf, *conf, "Unexpected config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test creating a valid config (unix mode)
|
|
||||||
func TestOKUnix(t *testing.T) {
|
|
||||||
tmpPath := t.TempDir()
|
|
||||||
content := `{
|
|
||||||
"log": {
|
|
||||||
"level": "error"
|
|
||||||
},
|
|
||||||
"server": {
|
|
||||||
"mode": "unix",
|
|
||||||
"sock": "/run/toto.sock"
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
confPath := path.Join(tmpPath, "config.json")
|
|
||||||
require.Nil(t, os.WriteFile(confPath, []byte(content), 0o644), "Failed to write config")
|
|
||||||
|
|
||||||
expectedConf := AppConfig{
|
|
||||||
LogLevel: logrus.ErrorLevel,
|
|
||||||
ServerMode: ModeUnix,
|
|
||||||
SockPath: "/run/toto.sock",
|
|
||||||
}
|
|
||||||
conf, err := New(confPath)
|
|
||||||
if assert.Nil(t, err, "Unexpected error") {
|
|
||||||
assert.Equal(t, expectedConf, *conf, "Unexpected config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test creating a valid config, no log level provided, should be info
|
|
||||||
func TestOKNoLogLevel(t *testing.T) {
|
|
||||||
tmpPath := t.TempDir()
|
|
||||||
content := `{
|
|
||||||
"server": {
|
|
||||||
"mode": "net",
|
|
||||||
"host": "127.0.0.1",
|
|
||||||
"port": 8888
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
confPath := path.Join(tmpPath, "config.json")
|
|
||||||
require.Nil(t, os.WriteFile(confPath, []byte(content), 0o644), "Failed to write config")
|
|
||||||
|
|
||||||
expectedConf := AppConfig{
|
|
||||||
LogLevel: logrus.InfoLevel,
|
|
||||||
ServerMode: ModeNet,
|
|
||||||
Host: "127.0.0.1",
|
|
||||||
Port: 8888,
|
|
||||||
}
|
|
||||||
conf, err := New(confPath)
|
|
||||||
if assert.Nil(t, err, "Unexpected error") {
|
|
||||||
assert.Equal(t, expectedConf, *conf, "Unexpected config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test giving an invalid server mode
|
|
||||||
func TestErrMode(t *testing.T) {
|
|
||||||
tmpPath := t.TempDir()
|
|
||||||
content := `{
|
|
||||||
"log": {
|
|
||||||
"level": "error"
|
|
||||||
},
|
|
||||||
"server": {
|
|
||||||
"mode": "toto",
|
|
||||||
"sock": "/run/toto.sock"
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
confPath := path.Join(tmpPath, "config.json")
|
|
||||||
require.Nil(t, os.WriteFile(confPath, []byte(content), 0o644), "Failed to write config")
|
|
||||||
|
|
||||||
_, err := New(confPath)
|
|
||||||
if assert.Error(t, err, "Unexpected nil error") {
|
|
||||||
errMsg := "failed to parse config file: failed to parse server listening mode: invalid listening mode toto"
|
|
||||||
assert.Equal(t, errMsg, err.Error(), "Unexpected error message")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Since we still use a JSON conf for the OIDC config, we still need to check this for now
|
||||||
|
// But as soon as the config file is not necessary, this will probably disappear
|
||||||
func TestInvalidJSON(t *testing.T) {
|
func TestInvalidJSON(t *testing.T) {
|
||||||
tmpPath := t.TempDir()
|
clearEnv(t)
|
||||||
content := "toto"
|
|
||||||
confPath := path.Join(tmpPath, "config.json")
|
confPath := initJson(t, "toto")
|
||||||
require.Nil(t, os.WriteFile(confPath, []byte(content), 0o644), "Failed to write config")
|
errMsg := "failed to parse config file: invalid character 'o' in literal true (expecting 'r')"
|
||||||
_, err := New(confPath)
|
_, err := New(confPath)
|
||||||
if assert.Error(t, err, "Unexpected nil error") {
|
assert.ErrorContains(t, err, errMsg)
|
||||||
errMsg := "failed to parse config file: invalid character 'o' in literal true (expecting 'r')"
|
}
|
||||||
assert.Equal(t, errMsg, err.Error(), "Unexpected error message")
|
|
||||||
}
|
func TestJSONConfig(t *testing.T) {
|
||||||
|
clearEnv(t)
|
||||||
|
|
||||||
|
confPath := initJson(t, `{
|
||||||
|
"log": {"level":"info"},
|
||||||
|
"server": {
|
||||||
|
"mode": "net",
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 1337
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
conf, err := New(confPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, ModeNet, conf.ServerMode)
|
||||||
|
assert.Equal(t, "0.0.0.0", conf.Host)
|
||||||
|
assert.Equal(t, 1337, conf.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONConfigOverriden(t *testing.T) {
|
||||||
|
clearEnv(t)
|
||||||
|
|
||||||
|
confPath := initJson(t, `{
|
||||||
|
"log": {"level":"info"},
|
||||||
|
"server": {
|
||||||
|
"mode": "net",
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 1337
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
envVars := map[string]string{
|
||||||
|
string("POLYCULECONNECT_SERVER_MODE"): string(ModeUnix),
|
||||||
|
string("POLYCULECONNECT_SERVER_SOCK"): "/run/polyculeconnect.sock",
|
||||||
|
}
|
||||||
|
setEnv(t, envVars)
|
||||||
|
|
||||||
|
conf, err := New(confPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, ModeUnix, conf.ServerMode)
|
||||||
|
assert.Equal(t, "0.0.0.0", conf.Host)
|
||||||
|
assert.Equal(t, 1337, conf.Port)
|
||||||
|
assert.Equal(t, "/run/polyculeconnect.sock", conf.SockPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostNetMode(t *testing.T) {
|
||||||
|
clearEnv(t)
|
||||||
|
|
||||||
|
envVars := map[string]string{
|
||||||
|
string("POLYCULECONNECT_SERVER_MODE"): string(ModeNet),
|
||||||
|
string("POLYCULECONNECT_SERVER_HOST"): "127.0.0.1",
|
||||||
|
string("POLYCULECONNECT_SERVER_PORT"): "8888",
|
||||||
|
}
|
||||||
|
setEnv(t, envVars)
|
||||||
|
|
||||||
|
conf, err := New("")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, ModeNet, conf.ServerMode)
|
||||||
|
assert.Equal(t, "127.0.0.1", conf.Host)
|
||||||
|
assert.Equal(t, 8888, conf.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostSocketMode(t *testing.T) {
|
||||||
|
clearEnv(t)
|
||||||
|
|
||||||
|
envVars := map[string]string{
|
||||||
|
string("POLYCULECONNECT_SERVER_MODE"): string(ModeUnix),
|
||||||
|
string("POLYCULECONNECT_SERVER_SOCK"): "/run/polyculeconnect.sock",
|
||||||
|
}
|
||||||
|
setEnv(t, envVars)
|
||||||
|
|
||||||
|
conf, err := New("")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, ModeUnix, conf.ServerMode)
|
||||||
|
assert.Equal(t, "/run/polyculeconnect.sock", conf.SockPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogLevel(t *testing.T) {
|
||||||
|
clearEnv(t)
|
||||||
|
|
||||||
|
envVars := map[string]string{
|
||||||
|
string("POLYCULECONNECT_LOG_LEVEL"): "error",
|
||||||
|
}
|
||||||
|
setEnv(t, envVars)
|
||||||
|
|
||||||
|
conf, err := New("")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, zap.NewAtomicLevelAt(zap.ErrorLevel), conf.LogLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqliteConfig(t *testing.T) {
|
||||||
|
clearEnv(t)
|
||||||
|
|
||||||
|
envVars := map[string]string{
|
||||||
|
string("POLYCULECONNECT_STORAGE_TYPE"): "sqlite",
|
||||||
|
string("POLYCULECONNECT_STORAGE_PATH"): "/data/polyculeconnect.db",
|
||||||
|
}
|
||||||
|
setEnv(t, envVars)
|
||||||
|
|
||||||
|
conf, err := New("")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, string(SQLite), conf.StorageType)
|
||||||
|
assert.Equal(t, "/data/polyculeconnect.db", conf.StorageConfig.File)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqliteConfigJSON(t *testing.T) {
|
||||||
|
clearEnv(t)
|
||||||
|
|
||||||
|
confPath := initJson(t, `{
|
||||||
|
"log": {"level":"info"},
|
||||||
|
"storage": {
|
||||||
|
"type": "sqlite",
|
||||||
|
"path": "/data/polyculeconnect.db"
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
conf, err := New(confPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, string(SQLite), conf.StorageType)
|
||||||
|
assert.Equal(t, "/data/polyculeconnect.db", conf.StorageConfig.File)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqliteConfigJSONOverriden(t *testing.T) {
|
||||||
|
clearEnv(t)
|
||||||
|
|
||||||
|
confPath := initJson(t, `{
|
||||||
|
"log": {"level":"info"},
|
||||||
|
"storage": {
|
||||||
|
"type": "sqlite",
|
||||||
|
"path": "/data/polyculeconnect.db"
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
envVars := map[string]string{
|
||||||
|
string("POLYCULECONNECT_STORAGE_PATH"): "/tmp/polyculeconnect.db",
|
||||||
|
}
|
||||||
|
setEnv(t, envVars)
|
||||||
|
|
||||||
|
conf, err := New(confPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, string(SQLite), conf.StorageType)
|
||||||
|
assert.Equal(t, "/tmp/polyculeconnect.db", conf.StorageConfig.File)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
package connector
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/dexidp/dex/connector"
|
|
||||||
"github.com/dexidp/dex/pkg/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RefuseAllConfig struct{}
|
|
||||||
|
|
||||||
func (c *RefuseAllConfig) Open(id string, logger log.Logger) (connector.Connector, error) {
|
|
||||||
return &RefuseAllConnector{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type RefuseAllConnector struct{}
|
|
||||||
|
|
||||||
func (m *RefuseAllConnector) LoginURL(s connector.Scopes, callbackURL, state string) (string, error) {
|
|
||||||
return "", fmt.Errorf("you shouldn't use this function")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *RefuseAllConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) {
|
|
||||||
return connector.Identity{}, fmt.Errorf("you shouldn't use this function")
|
|
||||||
}
|
|
144
polyculeconnect/controller/auth/approval.go
Normal file
144
polyculeconnect/controller/auth/approval.go
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/helpers"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ApprovalRoute = "/approval"
|
||||||
|
|
||||||
|
var scopeDescriptions = map[string]string{
|
||||||
|
"offline_access": "Have offline access",
|
||||||
|
"profile": "View basic profile information",
|
||||||
|
"email": "View your email address",
|
||||||
|
"groups": "View your groups",
|
||||||
|
}
|
||||||
|
|
||||||
|
func scopeDescription(rawScope string) string {
|
||||||
|
if desc, ok := scopeDescriptions[rawScope]; ok {
|
||||||
|
return desc
|
||||||
|
}
|
||||||
|
return rawScope
|
||||||
|
}
|
||||||
|
|
||||||
|
type approvalData struct {
|
||||||
|
Scopes []string
|
||||||
|
Client string
|
||||||
|
AuthReqID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalController struct {
|
||||||
|
l *zap.SugaredLogger
|
||||||
|
st db.Storage
|
||||||
|
baseDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApprovalController(l *zap.SugaredLogger, st db.Storage, baseDir string) *ApprovalController {
|
||||||
|
return &ApprovalController{
|
||||||
|
l: l,
|
||||||
|
st: st,
|
||||||
|
baseDir: baseDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ApprovalController) handleFormResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
reqID, err := uuid.Parse(r.Form.Get("req"))
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorf("Invalid request ID: %s", err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusBadRequest, []byte("invalid query format"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Form.Get("approval") != "approve" {
|
||||||
|
c.l.Debug("Approval rejected")
|
||||||
|
helpers.HandleResponse(w, r, http.StatusBadRequest, []byte("approval rejected"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.st.AuthRequestStorage().GiveConsent(r.Context(), reqID); err != nil {
|
||||||
|
c.l.Errorf("Failed to approve request: %s", err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, fmt.Sprintf("/callback?code=%s&state=%s", r.Form.Get("code"), reqID.String()), http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ApprovalController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
c.l.Errorf("Failed to parse query: %s", err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusBadRequest, []byte("invalid query format"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
c.handleFormResponse(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state := r.Form.Get("state")
|
||||||
|
reqID, err := uuid.Parse(state)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorf("Invalid state %q: %s", state, err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusBadRequest, []byte("unexpected state"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := c.st.AuthRequestStorage().GetAuthRequestByID(r.Context(), reqID)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorf("Failed to get auth request from DB: %s", err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app, err := c.st.ClientStorage().GetClientByID(r.Context(), req.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorf("Failed to get client details from DB: %s", err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := approvalData{
|
||||||
|
Scopes: []string{},
|
||||||
|
Client: app.Name,
|
||||||
|
AuthReqID: reqID.String(),
|
||||||
|
}
|
||||||
|
for _, s := range req.Scopes {
|
||||||
|
if s == "openid" { // it's implied we want that, no consent is really important there
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data.Scopes = append(data.Scopes, scopeDescription(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
lp := filepath.Join(c.baseDir, "templates", "approval.html")
|
||||||
|
hdrTpl := filepath.Join(c.baseDir, "templates", "header.html")
|
||||||
|
footTpl := filepath.Join(c.baseDir, "templates", "footer.html")
|
||||||
|
tmpl, err := template.New("approval.html").ParseFiles(hdrTpl, footTpl, lp)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorf("Failed to parse templates: %s", err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
if err := tmpl.Execute(buf, data); err != nil {
|
||||||
|
c.l.Errorf("Failed to execute template: %s", err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = io.Copy(w, buf)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorf("Failed to write response: %s", err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
116
polyculeconnect/controller/auth/authcallback.go
Normal file
116
polyculeconnect/controller/auth/authcallback.go
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/helpers"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/storage"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const AuthCallbackRoute = "/callback"
|
||||||
|
|
||||||
|
type AuthCallbackController struct {
|
||||||
|
l *zap.SugaredLogger
|
||||||
|
st *storage.Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthCallbackController(l *zap.SugaredLogger, st *storage.Storage) *AuthCallbackController {
|
||||||
|
return &AuthCallbackController{
|
||||||
|
l: l,
|
||||||
|
st: st,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AuthCallbackController) HandleUserInfoCallback(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
|
||||||
|
requestID, err := uuid.Parse(state)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorf("Invalid state, should be a request UUID, but got %s: %s", state, err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusInternalServerError, []byte("failed to perform authentication"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.l.Infof("Successful login from %s", info.Email)
|
||||||
|
user := model.User{
|
||||||
|
Subject: info.Subject,
|
||||||
|
Name: info.Name,
|
||||||
|
FamilyName: info.FamilyName,
|
||||||
|
GivenName: info.GivenName,
|
||||||
|
Picture: info.Picture,
|
||||||
|
UpdatedAt: info.UpdatedAt.AsTime(),
|
||||||
|
Email: info.Email,
|
||||||
|
EmailVerified: bool(info.EmailVerified),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.st.LocalStorage.AuthRequestStorage().ValidateAuthRequest(r.Context(), requestID, user.Subject)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorf("Failed to validate auth request from storage: %s", err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusInternalServerError, []byte("failed to perform authentication"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := c.st.LocalStorage.UserStorage().AddUser(r.Context(), &user); err != nil {
|
||||||
|
c.l.Errorf("Failed to add related user to storageL %w", err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusInternalServerError, []byte("failed to perform authentication"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/authorize/callback?id="+state, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallbackDispatchController struct {
|
||||||
|
l *zap.SugaredLogger
|
||||||
|
st *storage.Storage
|
||||||
|
callbackHandlers map[uuid.UUID]http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCallbackDispatchController(l *zap.SugaredLogger, st *storage.Storage, handlers map[uuid.UUID]http.Handler) *CallbackDispatchController {
|
||||||
|
return &CallbackDispatchController{
|
||||||
|
l: l,
|
||||||
|
st: st,
|
||||||
|
callbackHandlers: handlers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CallbackDispatchController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
errMsg := r.URL.Query().Get("error")
|
||||||
|
if errMsg != "" {
|
||||||
|
errorDesc := r.URL.Query().Get("error_description")
|
||||||
|
c.l.Errorf("Failed to perform authentication: %s (%s)", errMsg, errorDesc)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusInternalServerError, []byte("failed to perform authentication"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state := r.URL.Query().Get("state")
|
||||||
|
requestID, err := uuid.Parse(state)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorf("Invalid state, should be a request UUID, but got %s: %s", state, err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusInternalServerError, []byte("failed to perform authentication"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := c.st.LocalStorage.AuthRequestStorage().GetAuthRequestByID(r.Context(), requestID)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorf("Failed to get auth request from DB: %s", err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusBadRequest, []byte("unknown request id"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !req.Consent {
|
||||||
|
c.l.Debug("Redirecting to consent endpoint")
|
||||||
|
http.Redirect(w, r, fmt.Sprintf("/approval?state=%s&code=%s", state, r.URL.Query().Get("code")), http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
callbackHandler, ok := c.callbackHandlers[req.BackendID]
|
||||||
|
if !ok {
|
||||||
|
c.l.Errorf("Backend %s does not exist for request %s", req.ID, req.BackendID)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusNotFound, []byte("unknown backend"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callbackHandler.ServeHTTP(w, r)
|
||||||
|
}
|
54
polyculeconnect/controller/auth/authdispatch.go
Normal file
54
polyculeconnect/controller/auth/authdispatch.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/helpers"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/storage"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthDispatchController struct {
|
||||||
|
l *zap.SugaredLogger
|
||||||
|
st *storage.Storage
|
||||||
|
redirectHandlers map[uuid.UUID]http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthDispatchController(l *zap.SugaredLogger, storage *storage.Storage, redirectHandlers map[uuid.UUID]http.Handler) *AuthDispatchController {
|
||||||
|
return &AuthDispatchController{
|
||||||
|
l: l,
|
||||||
|
st: storage,
|
||||||
|
redirectHandlers: redirectHandlers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AuthDispatchController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requestIDStr := r.URL.Query().Get("request_id")
|
||||||
|
if requestIDStr == "" {
|
||||||
|
helpers.HandleResponse(w, r, http.StatusBadRequest, []byte("no request ID in request"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestID, err := uuid.Parse(requestIDStr)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorf("Invalid UUID format for request ID: %s", err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusBadRequest, []byte("invalid request id"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := c.st.LocalStorage.AuthRequestStorage().GetAuthRequestByID(r.Context(), requestID)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorf("Failed to get auth request from DB: %s", err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusBadRequest, []byte("unknown request id"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loginHandler, ok := c.redirectHandlers[req.BackendID]
|
||||||
|
if !ok {
|
||||||
|
c.l.Errorf("Backend %s does not exist for request %s", req.ID, req.BackendID)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusNotFound, []byte("unknown backend"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loginHandler.ServeHTTP(w, r)
|
||||||
|
}
|
50
polyculeconnect/controller/auth/authredirect.go
Normal file
50
polyculeconnect/controller/auth/authredirect.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/helpers"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/storage"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const AuthRedirectRoute = "/perform_auth"
|
||||||
|
|
||||||
|
type AuthRedirectController struct {
|
||||||
|
provider rp.RelyingParty
|
||||||
|
l *zap.SugaredLogger
|
||||||
|
st *storage.Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthRedirectController(l *zap.SugaredLogger, provider rp.RelyingParty, storage *storage.Storage) *AuthRedirectController {
|
||||||
|
return &AuthRedirectController{
|
||||||
|
l: l,
|
||||||
|
st: storage,
|
||||||
|
provider: provider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AuthRedirectController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requestIDStr := r.URL.Query().Get("request_id")
|
||||||
|
if requestIDStr == "" {
|
||||||
|
helpers.HandleResponse(w, r, http.StatusBadRequest, []byte("no request ID in request"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestID, err := uuid.Parse(requestIDStr)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorf("Invalid UUID format for request ID: %s", err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusBadRequest, []byte("invalid request id"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.st.LocalStorage.AuthRequestStorage().GetAuthRequestByID(r.Context(), requestID)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorf("Failed to get auth request from DB: %s", err)
|
||||||
|
helpers.HandleResponse(w, r, http.StatusBadRequest, []byte("unknown request id"), c.l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rp.AuthURLHandler(func() string { return requestIDStr }, c.provider).ServeHTTP(w, r)
|
||||||
|
}
|
|
@ -9,28 +9,37 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/helpers"
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/helpers"
|
||||||
"github.com/sirupsen/logrus"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
const StaticRoute = "/static/"
|
const StaticRoute = "/static/"
|
||||||
|
|
||||||
type StaticController struct {
|
type StaticController struct {
|
||||||
|
baseDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStaticController(baseDir string) *StaticController {
|
||||||
|
return &StaticController{
|
||||||
|
baseDir: baseDir,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *StaticController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (sc *StaticController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
fs := http.FileServer(http.Dir("./static"))
|
fs := http.FileServer(http.Dir(sc.baseDir + "/static"))
|
||||||
http.StripPrefix(StaticRoute, fs).ServeHTTP(w, r)
|
http.StripPrefix(StaticRoute, fs).ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
type IndexController struct {
|
type IndexController struct {
|
||||||
l *logrus.Logger
|
l *zap.SugaredLogger
|
||||||
downstreamConstroller http.Handler
|
downstreamConstroller http.Handler
|
||||||
|
baseDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIndexController(l *logrus.Logger, downstream http.Handler) *IndexController {
|
func NewIndexController(l *zap.SugaredLogger, downstream http.Handler, baseDir string) *IndexController {
|
||||||
return &IndexController{
|
return &IndexController{
|
||||||
l: l,
|
l: l,
|
||||||
downstreamConstroller: downstream,
|
downstreamConstroller: downstream,
|
||||||
|
baseDir: baseDir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,9 +48,9 @@ func (ic IndexController) serveUI(w http.ResponseWriter, r *http.Request) (int,
|
||||||
"issuer": func() string { return "toto" },
|
"issuer": func() string { return "toto" },
|
||||||
}
|
}
|
||||||
|
|
||||||
lp := filepath.Join("templates", "index.html")
|
lp := filepath.Join(ic.baseDir, "templates", "index.html")
|
||||||
hdrTpl := filepath.Join("templates", "header.html")
|
hdrTpl := filepath.Join(ic.baseDir, "templates", "header.html")
|
||||||
footTpl := filepath.Join("templates", "footer.html")
|
footTpl := filepath.Join(ic.baseDir, "templates", "footer.html")
|
||||||
tmpl, err := template.New("index.html").Funcs(funcs).ParseFiles(hdrTpl, footTpl, lp)
|
tmpl, err := template.New("index.html").Funcs(funcs).ParseFiles(hdrTpl, footTpl, lp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, -1, fmt.Errorf("failed to init template: %w", err)
|
return http.StatusInternalServerError, -1, fmt.Errorf("failed to init template: %w", err)
|
||||||
|
@ -61,7 +70,7 @@ func (ic IndexController) serveUI(w http.ResponseWriter, r *http.Request) (int,
|
||||||
|
|
||||||
func (ic *IndexController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (ic *IndexController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.RequestURI != "/" {
|
if r.RequestURI != "/" {
|
||||||
ic.l.Debugf("Serving URI %q to dex handler", r.RequestURI)
|
ic.l.Debugf("Serving URI %q to oidc handler", r.RequestURI)
|
||||||
ic.downstreamConstroller.ServeHTTP(w, r)
|
ic.downstreamConstroller.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,70 +1,51 @@
|
||||||
module git.faercol.me/faercol/polyculeconnect/polyculeconnect
|
module git.faercol.me/faercol/polyculeconnect/polyculeconnect
|
||||||
|
|
||||||
go 1.20
|
go 1.21
|
||||||
|
|
||||||
|
toolchain go1.22.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dexidp/dex v0.0.0-20231014000322-089f374d4f3e
|
github.com/go-jose/go-jose/v4 v4.0.4
|
||||||
github.com/prometheus/client_golang v1.17.0
|
github.com/golang-migrate/migrate/v4 v4.17.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/spf13/cobra v1.7.0
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
|
github.com/zitadel/oidc/v3 v3.30.1
|
||||||
|
go.uber.org/zap v1.24.0
|
||||||
|
go.uber.org/zap/exp v0.2.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/compute v1.23.0 // indirect
|
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
|
||||||
github.com/AppsFlyer/go-sundheit v0.5.0 // indirect
|
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
|
||||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
|
||||||
github.com/beevik/etree v1.2.0 // indirect
|
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
|
||||||
github.com/coreos/go-oidc/v3 v3.6.0 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dexidp/dex/api/v2 v2.1.1-0.20231014000322-089f374d4f3e // indirect
|
github.com/go-chi/chi/v5 v5.1.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/go-ldap/ldap/v3 v3.4.6 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
github.com/google/s2a-go v0.1.7 // indirect
|
github.com/muhlemmer/gu v0.3.1 // indirect
|
||||||
github.com/google/uuid v1.3.1 // indirect
|
github.com/muhlemmer/httpforwarded v0.1.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect
|
|
||||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
|
||||||
github.com/gorilla/handlers v1.5.1 // indirect
|
|
||||||
github.com/gorilla/mux v1.8.0 // indirect
|
|
||||||
github.com/huandu/xstrings v1.3.3 // indirect
|
|
||||||
github.com/imdario/mergo v0.3.11 // indirect
|
|
||||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
|
||||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
|
||||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
|
||||||
github.com/mitchellh/reflectwalk v1.0.0 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
|
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||||
github.com/prometheus/common v0.44.0 // indirect
|
github.com/rs/cors v1.11.1 // indirect
|
||||||
github.com/prometheus/procfs v0.11.1 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/russellhaering/goxmldsig v1.4.0 // indirect
|
github.com/zitadel/logging v0.6.1 // indirect
|
||||||
github.com/shopspring/decimal v1.2.0 // indirect
|
github.com/zitadel/schema v1.3.0 // indirect
|
||||||
github.com/spf13/cast v1.4.1 // indirect
|
go.opentelemetry.io/otel v1.29.0 // indirect
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opentelemetry.io/otel/metric v1.29.0 // indirect
|
||||||
golang.org/x/crypto v0.14.0 // indirect
|
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741 // indirect
|
go.uber.org/atomic v1.10.0 // indirect
|
||||||
golang.org/x/net v0.17.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/oauth2 v0.13.0 // indirect
|
golang.org/x/crypto v0.25.0 // indirect
|
||||||
golang.org/x/sys v0.13.0 // indirect
|
golang.org/x/oauth2 v0.23.0 // indirect
|
||||||
golang.org/x/text v0.13.0 // indirect
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
google.golang.org/api v0.147.0 // indirect
|
golang.org/x/text v0.18.0 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231009173412-8bfb1ae86b6c // indirect
|
|
||||||
google.golang.org/grpc v1.58.3 // indirect
|
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,297 +1,115 @@
|
||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||||
cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
|
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
|
||||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/AppsFlyer/go-sundheit v0.5.0 h1:/VxpyigCfJrq1r97mn9HPiAB2qrhcTFHwNIIDr15CZM=
|
|
||||||
github.com/AppsFlyer/go-sundheit v0.5.0/go.mod h1:2ZM0BnfqT/mljBQO224VbL5XH06TgWuQ6Cn+cTtCpTY=
|
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
|
||||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
|
||||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
|
||||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
|
||||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
|
||||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
|
|
||||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
|
||||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
|
||||||
github.com/beevik/etree v1.2.0 h1:l7WETslUG/T+xOPs47dtd6jov2Ii/8/OjCldk5fYfQw=
|
|
||||||
github.com/beevik/etree v1.2.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc=
|
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
|
||||||
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/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/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o=
|
|
||||||
github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc=
|
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dexidp/dex v0.0.0-20231014000322-089f374d4f3e h1:d/wCtR05IxnKZN9cg8YsLXPzYeDZoQsJpGoBBw2oQn4=
|
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||||
github.com/dexidp/dex v0.0.0-20231014000322-089f374d4f3e/go.mod h1:xFYDBaDJhmCE6xrovgHeUaFyslqhfocm3/xIJRkUYdk=
|
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/dexidp/dex/api/v2 v2.1.1-0.20231014000322-089f374d4f3e h1:46E2eP+vALmVQQz68L21EusFbwjJDCZVZT5hI1al4lE=
|
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
|
||||||
github.com/dexidp/dex/api/v2 v2.1.1-0.20231014000322-089f374d4f3e/go.mod h1:MwZ7k1lmdibhSptV8z+eKgZDOyAamm3M332EyQHycxA=
|
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
|
||||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
|
||||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||||
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.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
|
||||||
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/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.5.0/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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
|
||||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
|
||||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
|
||||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.1 h1:SBWmZhjUDRorQxrN0nwzf+AHBxnbFjViHQS4P0yVpmQ=
|
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.1/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
|
||||||
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
|
|
||||||
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
|
||||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
|
||||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
|
||||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
|
||||||
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
|
|
||||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
|
||||||
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
|
||||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
|
||||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
|
||||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
|
|
||||||
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
|
||||||
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY=
|
||||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0=
|
||||||
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
|
|
||||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
|
||||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
|
|
||||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
|
||||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
|
||||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
|
||||||
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
|
||||||
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
|
||||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
|
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||||
github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
|
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||||
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
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.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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/zitadel/logging v0.6.1/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/zitadel/oidc/v3 v3.30.1 h1:CCi9qDjleuRYbECfUoVKrrN97KdheNCHAs33X8XnIRg=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/zitadel/oidc/v3 v3.30.1/go.mod h1:N5p02vx+mLUwf+WFNpDsNp+8DS8+jlgFBwpz7NIQjrg=
|
||||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0=
|
||||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
||||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
|
||||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
||||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||||
golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741 h1:fGZugkZk2UgYBxtpKmvub51Yno1LJDeEsRp2xGD+0gY=
|
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||||
golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
|
||||||
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 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
|
||||||
golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
|
|
||||||
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
|
|
||||||
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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
|
||||||
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-20190412213103-97732733099d/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-20210615035016-665e8c7367d1/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-20220715151400-c0bba94af5f8/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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
|
||||||
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.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
|
||||||
golang.org/x/text v0.3.0/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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.4.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 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/api v0.147.0 h1:Can3FaQo9LlVqxJCodNmeZW/ib3/qKAY3rFeXiHo5gc=
|
|
||||||
google.golang.org/api v0.147.0/go.mod h1:pQ/9j83DcmPd/5C9e2nFOdjjNkDZ1G+zkbK2uvdkJMs=
|
|
||||||
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.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
|
||||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
|
||||||
google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 h1:SeZZZx0cP0fqUyA+oRzP9k7cSwJlvDFiROO72uwD6i0=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 h1:W18sezcAYs+3tDZX4F80yctqa12jcP1PUS2gQu1zTPU=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231009173412-8bfb1ae86b6c h1:jHkCUWkseRf+W+edG5hMzr/Uh1xkDREY4caybAq4dpY=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231009173412-8bfb1ae86b6c/go.mod h1:4cYg8o5yUbm77w8ZX00LhMVNl/YVBFJRYWDc0uYWMs0=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
|
||||||
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.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
|
||||||
google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
|
|
||||||
google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
|
|
||||||
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.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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
|
||||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
|
||||||
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/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContextKey string
|
type ContextKey string
|
||||||
|
@ -16,7 +16,7 @@ type ResponseInfo struct {
|
||||||
ContentLength int
|
ContentLength int
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleResponse(w http.ResponseWriter, r *http.Request, returncode int, content []byte, l *logrus.Logger) {
|
func HandleResponse(w http.ResponseWriter, r *http.Request, returncode int, content []byte, l *zap.SugaredLogger) {
|
||||||
w.WriteHeader(returncode)
|
w.WriteHeader(returncode)
|
||||||
n, err := w.Write(content)
|
n, err := w.Write(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
52
polyculeconnect/internal/client/client.go
Normal file
52
polyculeconnect/internal/client/client.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/exp/zapslog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BackendOIDCConfig struct {
|
||||||
|
Issuer string
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
RedirectURI string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Backend struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Name string
|
||||||
|
Config BackendOIDCConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// OIDCClient is an OIDC client which is the client used to access a registered backend
|
||||||
|
type OIDCClient struct {
|
||||||
|
backend *Backend
|
||||||
|
|
||||||
|
provider rp.RelyingParty
|
||||||
|
ctx context.Context
|
||||||
|
l *zap.SugaredLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, conf *Backend, l *zap.SugaredLogger) (*OIDCClient, error) {
|
||||||
|
options := []rp.Option{
|
||||||
|
rp.WithLogger(slog.New(zapslog.NewHandler(logger.L.Desugar().Core(), nil))),
|
||||||
|
}
|
||||||
|
pr, err := rp.NewRelyingPartyOIDC(ctx, conf.Config.Issuer, conf.Config.ClientID, conf.Config.ClientSecret, conf.Config.RedirectURI, []string{}, options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to init relying party provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OIDCClient{ctx: ctx, backend: conf, provider: pr, l: l}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OIDCClient) AuthorizationEndpoint() string {
|
||||||
|
url := rp.AuthURL(uuid.NewString(), c.provider)
|
||||||
|
return url
|
||||||
|
}
|
62
polyculeconnect/internal/db/authcode/authcode.go
Normal file
62
polyculeconnect/internal/db/authcode/authcode.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package authcode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("auth code not found")
|
||||||
|
|
||||||
|
type AuthCodeDB interface {
|
||||||
|
GetAuthCodeByCode(ctx context.Context, code string) (*model.AuthCode, error)
|
||||||
|
CreateAuthCode(ctx context.Context, code model.AuthCode) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type sqlAuthCodeDB struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlAuthCodeDB) CreateAuthCode(ctx context.Context, code model.AuthCode) error {
|
||||||
|
logger.L.Debugf("Creating auth code for request %s", code.RequestID)
|
||||||
|
tx, err := db.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start transaction: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
query := `INSERT INTO "auth_code" ("id", "auth_request_id", "code") VALUES ($1, $2, $3)`
|
||||||
|
_, err = tx.ExecContext(ctx, query, code.CodeID, code.RequestID, code.Code)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to insert in DB: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlAuthCodeDB) GetAuthCodeByCode(ctx context.Context, code string) (*model.AuthCode, error) {
|
||||||
|
logger.L.Debugf("Getting auth code %s from DB", code)
|
||||||
|
query := `SELECT "id", "auth_request_id", "code" FROM "auth_code" WHERE "code" = ?`
|
||||||
|
row := db.db.QueryRowContext(ctx, query, code)
|
||||||
|
|
||||||
|
var res model.AuthCode
|
||||||
|
if err := row.Scan(&res.CodeID, &res.RequestID, &res.Code); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to read row from DB: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *sql.DB) AuthCodeDB {
|
||||||
|
return &sqlAuthCodeDB{db: db}
|
||||||
|
}
|
159
polyculeconnect/internal/db/authrequest/authrequest.go
Normal file
159
polyculeconnect/internal/db/authrequest/authrequest.go
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
package authrequest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("backend not found")
|
||||||
|
|
||||||
|
const authRequestRows = `"id", "client_id", "backend_id", "scopes", "redirect_uri", "state", "nonce", "response_type", "creation_time", "done", "code_challenge", "code_challenge_method", "auth_time", "user_id", "consent"`
|
||||||
|
|
||||||
|
type AuthRequestDB interface {
|
||||||
|
GetAuthRequestByID(ctx context.Context, id uuid.UUID) (*model.AuthRequest, error)
|
||||||
|
CreateAuthRequest(ctx context.Context, req model.AuthRequest) error
|
||||||
|
ValidateAuthRequest(ctx context.Context, reqID uuid.UUID, userID string) error
|
||||||
|
DeleteAuthRequest(ctx context.Context, reqID uuid.UUID) error
|
||||||
|
GiveConsent(ctx context.Context, reqID uuid.UUID) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type sqlAuthRequestDB struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlAuthRequestDB) GetAuthRequestByID(ctx context.Context, id uuid.UUID) (*model.AuthRequest, error) {
|
||||||
|
logger.L.Debugf("Getting auth request with id %s", id)
|
||||||
|
query := fmt.Sprintf(`SELECT %s FROM "auth_request" WHERE "id" = ?`, authRequestRows)
|
||||||
|
row := db.db.QueryRowContext(ctx, query, id)
|
||||||
|
|
||||||
|
var res model.AuthRequest
|
||||||
|
var scopesStr []byte
|
||||||
|
|
||||||
|
var timestamp *time.Time
|
||||||
|
|
||||||
|
if err := row.Scan(&res.ID, &res.ClientID, &res.BackendID, &scopesStr, &res.RedirectURI, &res.State, &res.Nonce, &res.ResponseType, &res.CreationDate, &res.DoneVal, &res.CodeChallenge, &res.CodeChallengeMethod, ×tamp, &res.UserID, &res.Consent); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get auth request from DB: %w", err)
|
||||||
|
}
|
||||||
|
if timestamp != nil {
|
||||||
|
res.AuthTime = *timestamp
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(scopesStr, &res.Scopes); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid format for scopes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlAuthRequestDB) CreateAuthRequest(ctx context.Context, req model.AuthRequest) error {
|
||||||
|
logger.L.Debugf("Creating a new auth request between client app %s and backend %s", req.ClientID, req.BackendID)
|
||||||
|
tx, err := db.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start transaction: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
scopesStr, err := json.Marshal(req.Scopes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to serialize scopes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := fmt.Sprintf(`INSERT INTO "auth_request" (%s) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NULL, '', 0)`, authRequestRows)
|
||||||
|
_, err = tx.ExecContext(ctx, query,
|
||||||
|
req.ID, req.ClientID, req.BackendID,
|
||||||
|
scopesStr, req.RedirectURI, req.State,
|
||||||
|
req.Nonce, req.ResponseType, req.CreationDate, false,
|
||||||
|
req.CodeChallenge, req.CodeChallengeMethod,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to insert in DB: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlAuthRequestDB) ValidateAuthRequest(ctx context.Context, reqID uuid.UUID, userID string) error {
|
||||||
|
logger.L.Debugf("Validating auth request %s", reqID)
|
||||||
|
tx, err := db.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start transaction: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
res, err := tx.ExecContext(ctx, `UPDATE "auth_request" SET done = true, auth_time = $1, user_id = $2 WHERE id = $3`, time.Now().UTC(), userID, reqID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update in DB: %w", err)
|
||||||
|
}
|
||||||
|
affectedRows, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check number of affected rows: %w", err)
|
||||||
|
}
|
||||||
|
if affectedRows != 1 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlAuthRequestDB) GiveConsent(ctx context.Context, reqID uuid.UUID) error {
|
||||||
|
tx, err := db.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start transaction: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
res, err := tx.ExecContext(ctx, `UPDATE "auth_request" SET consent = true WHERE id = $1`, reqID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update in DB: %w", err)
|
||||||
|
}
|
||||||
|
affectedRows, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check number of affected rows: %w", err)
|
||||||
|
}
|
||||||
|
if affectedRows != 1 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlAuthRequestDB) DeleteAuthRequest(ctx context.Context, reqID uuid.UUID) error {
|
||||||
|
logger.L.Debugf("Deleting auth request: %s", reqID)
|
||||||
|
tx, err := db.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start transaction: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx, `DELETE FROM "auth_request" WHERE id = $1`, reqID.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete auth request: %w", err)
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *sql.DB) *sqlAuthRequestDB {
|
||||||
|
return &sqlAuthRequestDB{db: db}
|
||||||
|
}
|
137
polyculeconnect/internal/db/backend/backend.go
Normal file
137
polyculeconnect/internal/db/backend/backend.go
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("backend not found")
|
||||||
|
|
||||||
|
const backendRows = `"id", "name", "oidc_issuer", "oidc_client_id", "oidc_client_secret", "oidc_redirect_uri", "oidc_scopes"`
|
||||||
|
|
||||||
|
type scannable interface {
|
||||||
|
Scan(dest ...any) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type BackendDB interface {
|
||||||
|
GetAllBackends(ctx context.Context) ([]*model.Backend, error)
|
||||||
|
|
||||||
|
GetBackendByID(ctx context.Context, id uuid.UUID) (*model.Backend, error)
|
||||||
|
GetBackendByName(ctx context.Context, name string) (*model.Backend, error)
|
||||||
|
|
||||||
|
AddBackend(ctx context.Context, newBackend *model.Backend) error
|
||||||
|
|
||||||
|
DeleteBackend(ctx context.Context, id uuid.UUID) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type sqlBackendDB struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func backendFromRow(row scannable) (*model.Backend, error) {
|
||||||
|
var res model.Backend
|
||||||
|
var scopesStr []byte
|
||||||
|
|
||||||
|
fmt.Println(string(scopesStr))
|
||||||
|
|
||||||
|
if err := row.Scan(&res.ID, &res.Name, &res.Config.Issuer, &res.Config.ClientID, &res.Config.ClientSecret, &res.Config.RedirectURI, &scopesStr); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid format for backend: %w", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(scopesStr, &res.Config.Scopes); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid value for scopes: %w", err)
|
||||||
|
}
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlBackendDB) GetBackendByName(ctx context.Context, name string) (*model.Backend, error) {
|
||||||
|
logger.L.Debugf("Getting backend with name %s from DB", name)
|
||||||
|
query := fmt.Sprintf(`SELECT %s FROM "backend" WHERE "name" = ?`, backendRows)
|
||||||
|
row := db.db.QueryRowContext(ctx, query, name)
|
||||||
|
return backendFromRow(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlBackendDB) GetBackendByID(ctx context.Context, id uuid.UUID) (*model.Backend, error) {
|
||||||
|
logger.L.Debugf("Getting backend with ID %s from DB", id)
|
||||||
|
query := fmt.Sprintf(`SELECT %s FROM "backend" WHERE "id" = ?`, backendRows)
|
||||||
|
row := db.db.QueryRowContext(ctx, query, id)
|
||||||
|
return backendFromRow(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlBackendDB) GetAllBackends(ctx context.Context) ([]*model.Backend, error) {
|
||||||
|
logger.L.Debug("Getting all backends from DB")
|
||||||
|
rows, err := db.db.QueryContext(ctx, fmt.Sprintf(`SELECT %s FROM "backend"`, backendRows))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res []*model.Backend
|
||||||
|
for rows.Next() {
|
||||||
|
b, err := backendFromRow(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res = append(res, b)
|
||||||
|
}
|
||||||
|
return res, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlBackendDB) AddBackend(ctx context.Context, newBackend *model.Backend) error {
|
||||||
|
tx, err := db.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start transaction: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
scopesStr, err := json.Marshal(newBackend.Config.Scopes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to serialize scopes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := fmt.Sprintf(`INSERT INTO "backend" (%s) VALUES ($1, $2, $3, $4, $5, $6, $7)`, backendRows)
|
||||||
|
_, err = tx.ExecContext(
|
||||||
|
ctx, query,
|
||||||
|
newBackend.ID, newBackend.Name,
|
||||||
|
newBackend.Config.Issuer, newBackend.Config.ClientID,
|
||||||
|
newBackend.Config.ClientSecret, newBackend.Config.RedirectURI,
|
||||||
|
scopesStr,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to insert in DB: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlBackendDB) DeleteBackend(ctx context.Context, id uuid.UUID) error {
|
||||||
|
tx, err := db.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start transaction: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
if _, err := tx.ExecContext(ctx, `DELETE FROM "backend" WHERE id = $1`, id.String()); err != nil {
|
||||||
|
return fmt.Errorf("failed to run query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *sql.DB) *sqlBackendDB {
|
||||||
|
return &sqlBackendDB{db: db}
|
||||||
|
}
|
64
polyculeconnect/internal/db/base.go
Normal file
64
polyculeconnect/internal/db/base.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/config"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db/authcode"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db/authrequest"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db/backend"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db/client"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db/token"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Storage interface {
|
||||||
|
DB() *sql.DB
|
||||||
|
ClientStorage() client.ClientDB
|
||||||
|
BackendStorage() backend.BackendDB
|
||||||
|
AuthRequestStorage() authrequest.AuthRequestDB
|
||||||
|
AuthCodeStorage() authcode.AuthCodeDB
|
||||||
|
UserStorage() user.UserDB
|
||||||
|
TokenStorage() token.TokenDB
|
||||||
|
}
|
||||||
|
|
||||||
|
type sqlStorage struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sqlStorage) DB() *sql.DB {
|
||||||
|
return s.db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sqlStorage) ClientStorage() client.ClientDB {
|
||||||
|
return client.New(s.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sqlStorage) BackendStorage() backend.BackendDB {
|
||||||
|
return backend.New(s.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sqlStorage) AuthRequestStorage() authrequest.AuthRequestDB {
|
||||||
|
return authrequest.New(s.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sqlStorage) AuthCodeStorage() authcode.AuthCodeDB {
|
||||||
|
return authcode.New(s.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sqlStorage) UserStorage() user.UserDB {
|
||||||
|
return user.New(s.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sqlStorage) TokenStorage() token.TokenDB {
|
||||||
|
return token.New(s.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(conf config.AppConfig) (Storage, error) {
|
||||||
|
db, err := sql.Open("sqlite3", conf.StorageConfig.File)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open DB: %w", err)
|
||||||
|
}
|
||||||
|
return &sqlStorage{db: db}, nil
|
||||||
|
}
|
135
polyculeconnect/internal/db/client/client.go
Normal file
135
polyculeconnect/internal/db/client/client.go
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("not found")
|
||||||
|
|
||||||
|
const clientRows = `"client"."id", "client"."secret", "client"."redirect_uris", "client"."trusted_peers", "client"."name"`
|
||||||
|
|
||||||
|
type ClientDB interface {
|
||||||
|
GetClientByID(ctx context.Context, id string) (*model.Client, error)
|
||||||
|
GetAllClients(ctx context.Context) ([]*model.Client, error)
|
||||||
|
AddClient(ctx context.Context, client *model.Client) error
|
||||||
|
DeleteClient(ctx context.Context, id string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type sqlClientDB struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func strArrayToSlice(rawVal string) []string {
|
||||||
|
var res []string
|
||||||
|
if err := json.Unmarshal([]byte(rawVal), &res); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func sliceToStrArray(rawVal []string) string {
|
||||||
|
res, err := json.Marshal(rawVal)
|
||||||
|
if err != nil {
|
||||||
|
return "[]"
|
||||||
|
}
|
||||||
|
return string(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
type scannable interface {
|
||||||
|
Scan(dest ...any) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientFromRow(row scannable) (*model.Client, error) {
|
||||||
|
var res model.Client
|
||||||
|
redirectURIsStr := ""
|
||||||
|
trustedPeersStr := ""
|
||||||
|
|
||||||
|
if err := row.Scan(&res.ID, &res.Secret, &redirectURIsStr, &trustedPeersStr, &res.Name); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid format for client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.ClientConfig.RedirectURIs = strArrayToSlice(redirectURIsStr)
|
||||||
|
res.ClientConfig.TrustedPeers = strArrayToSlice(trustedPeersStr)
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlClientDB) GetClientByID(ctx context.Context, id string) (*model.Client, error) {
|
||||||
|
logger.L.Debugf("Getting client app with ID %s from DB", id)
|
||||||
|
query := fmt.Sprintf(`SELECT %s FROM "client" WHERE "id" = ?`, clientRows)
|
||||||
|
row := db.db.QueryRowContext(ctx, query, id)
|
||||||
|
return clientFromRow(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlClientDB) GetAllClients(ctx context.Context) ([]*model.Client, error) {
|
||||||
|
rows, err := db.db.QueryContext(ctx, fmt.Sprintf(`SELECT %s FROM "client"`, clientRows))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query clients from DB: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var res []*model.Client
|
||||||
|
for rows.Next() {
|
||||||
|
clt, err := clientFromRow(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to scan row: %w", err)
|
||||||
|
}
|
||||||
|
res = append(res, clt)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read all rows %w", err)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlClientDB) AddClient(ctx context.Context, client *model.Client) error {
|
||||||
|
logger.L.Debugf("Creating client %s", client.Name)
|
||||||
|
query := `INSERT INTO "client" ("id", "secret", "redirect_uris", "trusted_peers", "name") VALUES ($1, $2, $3, $4, $5)`
|
||||||
|
|
||||||
|
tx, err := db.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start transaction: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
if affectedRows, err := tx.ExecContext(ctx, query, client.ID, client.Secret, sliceToStrArray(client.RedirectURIs()), sliceToStrArray(client.TrustedPeers), client.Name); err != nil {
|
||||||
|
return fmt.Errorf("failed to insert in DB: %w", err)
|
||||||
|
} else if nbAffected, err := affectedRows.RowsAffected(); err != nil {
|
||||||
|
return fmt.Errorf("failed to check number of affected rows: %w", err)
|
||||||
|
} else if nbAffected != 1 {
|
||||||
|
return fmt.Errorf("unexpected number of affected rows: %d", nbAffected)
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlClientDB) DeleteClient(ctx context.Context, id string) error {
|
||||||
|
tx, err := db.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tx.ExecContext(ctx, `DELETE FROM "client" WHERE "id" = ?`, id); err != nil {
|
||||||
|
return fmt.Errorf("failed to exec query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *sql.DB) *sqlClientDB {
|
||||||
|
return &sqlClientDB{db: db}
|
||||||
|
}
|
68
polyculeconnect/internal/db/token/token.go
Normal file
68
polyculeconnect/internal/db/token/token.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func strArrayToSlice(rawVal string) []string {
|
||||||
|
var res []string
|
||||||
|
if err := json.Unmarshal([]byte(rawVal), &res); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func sliceToStrArray(rawVal []string) string {
|
||||||
|
res, err := json.Marshal(rawVal)
|
||||||
|
if err != nil {
|
||||||
|
return "[]"
|
||||||
|
}
|
||||||
|
return string(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenDB interface {
|
||||||
|
AddRefreshToken(ctx context.Context, refreshToken *model.RefreshToken) error
|
||||||
|
GetRefreshTokenByID(ctx context.Context, id uuid.UUID) (*model.RefreshToken, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type sqlTokenDB struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlTokenDB) AddRefreshToken(ctx context.Context, refreshToken *model.RefreshToken) error {
|
||||||
|
tx, err := db.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start transaction: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
if _, err := tx.ExecContext(ctx, `INSERT INTO "refresh_token" ("id", "client_id", "user_id", "scopes", "auth_time") VALUES ($1, $2, $3, $4, $5)`, refreshToken.ID, refreshToken.ClientID, refreshToken.UserID, sliceToStrArray(refreshToken.Scopes), refreshToken.AuthTime); err != nil {
|
||||||
|
return fmt.Errorf("failed to exec query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlTokenDB) GetRefreshTokenByID(ctx context.Context, id uuid.UUID) (*model.RefreshToken, error) {
|
||||||
|
row := db.db.QueryRowContext(ctx, `SELECT "id", "client_id", "user_id", "scopes", "auth_time" FROM "refresh_token" WHERE "id" = ?`, id)
|
||||||
|
var res model.RefreshToken
|
||||||
|
var strScopes string
|
||||||
|
if err := row.Scan(&res.ID, &res.ClientID, &res.UserID, &strScopes, &res.AuthTime); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query DB: %w", err)
|
||||||
|
}
|
||||||
|
res.Scopes = strArrayToSlice(strScopes)
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *sql.DB) TokenDB {
|
||||||
|
return &sqlTokenDB{db: db}
|
||||||
|
}
|
63
polyculeconnect/internal/db/user/user.go
Normal file
63
polyculeconnect/internal/db/user/user.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserDB interface {
|
||||||
|
AddUser(ctx context.Context, user *model.User) error
|
||||||
|
GetUserBySubject(ctx context.Context, subject string) (*model.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("not found")
|
||||||
|
|
||||||
|
const getUserQuery = `
|
||||||
|
SELECT id, name, family_name, given_name, nickname, picture, updated_at, email, email_verified
|
||||||
|
FROM user
|
||||||
|
WHERE id = ?
|
||||||
|
`
|
||||||
|
const insertUserQuery = `
|
||||||
|
INSERT OR REPLACE INTO user (id, name, family_name, given_name, nickname, picture, updated_at, email, email_verified)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
`
|
||||||
|
|
||||||
|
type sqlUserDB struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlUserDB) GetUserBySubject(ctx context.Context, subject string) (*model.User, error) {
|
||||||
|
row := db.db.QueryRowContext(ctx, getUserQuery, subject)
|
||||||
|
var res model.User
|
||||||
|
if err := row.Scan(&res.Subject, &res.Name, &res.FamilyName, &res.GivenName, &res.Nickname, &res.Picture, &res.UpdatedAt, &res.Email, &res.EmailVerified); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to read result from DB: %w", err)
|
||||||
|
}
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *sqlUserDB) AddUser(ctx context.Context, user *model.User) error {
|
||||||
|
tx, err := db.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start transaction: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
if _, err := tx.ExecContext(ctx, insertUserQuery, user.Subject, user.Name, user.FamilyName, user.GivenName, user.Nickname, user.Picture, user.UpdatedAt, user.Email, user.EmailVerified); err != nil {
|
||||||
|
return fmt.Errorf("failed to insert in DB: %w", err)
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *sql.DB) *sqlUserDB {
|
||||||
|
return &sqlUserDB{db: db}
|
||||||
|
}
|
75
polyculeconnect/internal/middlewares/connectordispatcher.go
Normal file
75
polyculeconnect/internal/middlewares/connectordispatcher.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
backendNameQueryParam = "connector_id"
|
||||||
|
backendCtxKeyName = "backendName"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BackendFromRequestMiddleware struct {
|
||||||
|
l *zap.SugaredLogger
|
||||||
|
h http.Handler
|
||||||
|
baseDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *BackendFromRequestMiddleware) serveBackendSelector(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
lp := filepath.Join(m.baseDir, "templates", "login.html")
|
||||||
|
hdrTpl := filepath.Join(m.baseDir, "templates", "header.html")
|
||||||
|
footTpl := filepath.Join(m.baseDir, "templates", "footer.html")
|
||||||
|
tmpl, err := template.New("login.html").ParseFiles(hdrTpl, footTpl, lp)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, fmt.Errorf("failed to init template: %w", err)
|
||||||
|
}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
if err := tmpl.Execute(buf, nil); err != nil {
|
||||||
|
return http.StatusInternalServerError, fmt.Errorf("failed to execute template: %w", err)
|
||||||
|
}
|
||||||
|
_, err = io.Copy(w, buf)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, fmt.Errorf("failed to write response; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *BackendFromRequestMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/authorize" {
|
||||||
|
m.h.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
// TODO: handle this better
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
backendName := r.Form.Get(backendNameQueryParam)
|
||||||
|
if backendName == "" {
|
||||||
|
statusCode, err := m.serveBackendSelector(w, r)
|
||||||
|
if err != nil {
|
||||||
|
m.l.Errorf("Failed to serve backend selector page: %s", err)
|
||||||
|
}
|
||||||
|
w.WriteHeader(statusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(r.Context(), backendCtxKeyName, backendName)
|
||||||
|
m.h.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithBackendFromRequestMiddleware(input http.Handler) http.Handler {
|
||||||
|
return &BackendFromRequestMiddleware{h: input}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type loggedResponseWriter struct {
|
type loggedResponseWriter struct {
|
||||||
|
@ -33,7 +33,7 @@ func (lr *loggedResponseWriter) WriteHeader(statusCode int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoggerMiddleware struct {
|
type LoggerMiddleware struct {
|
||||||
l *logrus.Logger
|
l *zap.SugaredLogger
|
||||||
h http.Handler
|
h http.Handler
|
||||||
}
|
}
|
||||||
|
|
14
polyculeconnect/internal/middlewares/middlewarechain.go
Normal file
14
polyculeconnect/internal/middlewares/middlewarechain.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WithLogger(handler http.Handler, l *zap.SugaredLogger) http.Handler {
|
||||||
|
return &LoggerMiddleware{
|
||||||
|
l: l,
|
||||||
|
h: handler,
|
||||||
|
}
|
||||||
|
}
|
9
polyculeconnect/internal/model/authcode.go
Normal file
9
polyculeconnect/internal/model/authcode.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
|
type AuthCode struct {
|
||||||
|
CodeID uuid.UUID
|
||||||
|
RequestID uuid.UUID
|
||||||
|
Code string
|
||||||
|
}
|
118
polyculeconnect/internal/model/authrequest.go
Normal file
118
polyculeconnect/internal/model/authrequest.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthRequest also implements the op.AuthRequest interface
|
||||||
|
type AuthRequest struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
ClientID string
|
||||||
|
Scopes []string
|
||||||
|
RedirectURI string
|
||||||
|
State string
|
||||||
|
Nonce string
|
||||||
|
|
||||||
|
ResponseType string
|
||||||
|
|
||||||
|
CreationDate time.Time
|
||||||
|
AuthTime time.Time
|
||||||
|
|
||||||
|
// TODO mapping to claims to be added I guess
|
||||||
|
|
||||||
|
CodeChallenge string
|
||||||
|
CodeChallengeMethod string
|
||||||
|
|
||||||
|
BackendID uuid.UUID
|
||||||
|
Backend *Backend
|
||||||
|
|
||||||
|
UserID string
|
||||||
|
User *User
|
||||||
|
|
||||||
|
DoneVal bool
|
||||||
|
Consent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AuthRequest) GetID() string {
|
||||||
|
return a.ID.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AuthRequest) GetACR() string {
|
||||||
|
return "" // TODO: the hell is ACR???
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AuthRequest) GetAMR() []string {
|
||||||
|
return []string{} // TODO: the hell is this???
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AuthRequest) GetAudience() []string {
|
||||||
|
return []string{a.ID.String()} // TODO: check if we need to return something else
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AuthRequest) GetAuthTime() time.Time {
|
||||||
|
return a.AuthTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AuthRequest) GetClientID() string {
|
||||||
|
return a.ClientID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AuthRequest) GetCodeChallenge() *oidc.CodeChallenge {
|
||||||
|
return &oidc.CodeChallenge{
|
||||||
|
Challenge: a.CodeChallenge,
|
||||||
|
Method: oidc.CodeChallengeMethod(a.CodeChallengeMethod),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AuthRequest) GetNonce() string {
|
||||||
|
return a.Nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AuthRequest) GetRedirectURI() string {
|
||||||
|
return a.RedirectURI
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AuthRequest) GetResponseType() oidc.ResponseType {
|
||||||
|
return oidc.ResponseType(a.ResponseType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AuthRequest) GetResponseMode() oidc.ResponseMode {
|
||||||
|
return oidc.ResponseModeQuery // TODO: check if this is good
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AuthRequest) GetScopes() []string {
|
||||||
|
return a.Scopes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AuthRequest) GetState() string {
|
||||||
|
return a.State
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AuthRequest) GetSubject() string {
|
||||||
|
if a.User == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return a.User.Subject
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AuthRequest) Done() bool {
|
||||||
|
return a.DoneVal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthRequest) FromOIDCAuthRequest(req *oidc.AuthRequest, backendID uuid.UUID) {
|
||||||
|
a.ID = uuid.New()
|
||||||
|
a.ClientID = req.ClientID
|
||||||
|
a.Scopes = strings.Split(req.Scopes.String(), " ")
|
||||||
|
a.RedirectURI = req.RedirectURI
|
||||||
|
a.State = req.State
|
||||||
|
a.Nonce = req.Nonce
|
||||||
|
a.ResponseType = string(req.ResponseType)
|
||||||
|
a.CreationDate = time.Now().UTC()
|
||||||
|
a.CodeChallenge = req.CodeChallenge
|
||||||
|
a.CodeChallengeMethod = string(req.CodeChallengeMethod)
|
||||||
|
a.BackendID = backendID
|
||||||
|
}
|
17
polyculeconnect/internal/model/backend.go
Normal file
17
polyculeconnect/internal/model/backend.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
|
type BackendOIDCConfig struct {
|
||||||
|
Issuer string
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
RedirectURI string
|
||||||
|
Scopes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Backend struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Name string
|
||||||
|
Config BackendOIDCConfig
|
||||||
|
}
|
97
polyculeconnect/internal/model/client.go
Normal file
97
polyculeconnect/internal/model/client.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClientConfig represents the configuration for a OIDC client app
|
||||||
|
type ClientConfig struct {
|
||||||
|
ID string
|
||||||
|
Secret string
|
||||||
|
RedirectURIs []string
|
||||||
|
TrustedPeers []string
|
||||||
|
Name string
|
||||||
|
AuthRequest *AuthRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client represents an OIDC client app
|
||||||
|
type Client struct {
|
||||||
|
ClientConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) GetID() string {
|
||||||
|
return c.ClientConfig.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) RedirectURIs() []string {
|
||||||
|
return c.ClientConfig.RedirectURIs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) PostLogoutRedirectURIs() []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) ApplicationType() op.ApplicationType {
|
||||||
|
return op.ApplicationTypeWeb // TODO: should we support more?
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) AuthMethod() oidc.AuthMethod {
|
||||||
|
return oidc.AuthMethodBasic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) ResponseTypes() []oidc.ResponseType {
|
||||||
|
return []oidc.ResponseType{oidc.ResponseTypeCode}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) GrantTypes() []oidc.GrantType {
|
||||||
|
return []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken, oidc.GrantTypeTokenExchange}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginURL returns the login URL for a given client app and auth request.
|
||||||
|
// This login url should be the authorization URL for the selected OIDC backend
|
||||||
|
func (c Client) LoginURL(authRequestID string) string {
|
||||||
|
if authRequestID == "" {
|
||||||
|
return "" // we don't have a request, let's return nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
return "/perform_auth?request_id=" + authRequestID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) AccessTokenType() op.AccessTokenType {
|
||||||
|
return op.AccessTokenTypeJWT
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) IDTokenLifetime() time.Duration {
|
||||||
|
return 1 * time.Hour
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) DevMode() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []string {
|
||||||
|
return func(scopes []string) []string {
|
||||||
|
return scopes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string {
|
||||||
|
return func(scopes []string) []string {
|
||||||
|
return scopes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) IsScopeAllowed(scope string) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) IDTokenUserinfoClaimsAssertion() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) ClockSkew() time.Duration {
|
||||||
|
return 0
|
||||||
|
}
|
57
polyculeconnect/internal/model/signingkey.go
Normal file
57
polyculeconnect/internal/model/signingkey.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-jose/go-jose/v4"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SigningKey struct {
|
||||||
|
PrivateKey *rsa.PrivateKey
|
||||||
|
KeyID uuid.UUID
|
||||||
|
Algorithm jose.SignatureAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *SigningKey) ID() string {
|
||||||
|
return strings.ReplaceAll(k.KeyID.String(), "-", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *SigningKey) SignatureAlgorithm() jose.SignatureAlgorithm {
|
||||||
|
return k.Algorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *SigningKey) Key() any {
|
||||||
|
return k.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
type Key struct {
|
||||||
|
PrivateKey *rsa.PrivateKey
|
||||||
|
KeyID uuid.UUID
|
||||||
|
SigningAlg jose.SignatureAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Key) SigningKey() *SigningKey {
|
||||||
|
return &SigningKey{
|
||||||
|
PrivateKey: k.PrivateKey,
|
||||||
|
KeyID: k.KeyID,
|
||||||
|
Algorithm: k.SigningAlg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Key) ID() string {
|
||||||
|
return strings.ReplaceAll(k.KeyID.String(), "-", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Key) Algorithm() jose.SignatureAlgorithm {
|
||||||
|
return k.SigningAlg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Key) Key() any {
|
||||||
|
return &k.PrivateKey.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Key) Use() string {
|
||||||
|
return "sig"
|
||||||
|
}
|
68
polyculeconnect/internal/model/token.go
Normal file
68
polyculeconnect/internal/model/token.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
RefreshTokenID uuid.UUID
|
||||||
|
Expiration time.Time
|
||||||
|
Subjet string
|
||||||
|
Audiences []string
|
||||||
|
Scopes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshToken struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
ClientID string
|
||||||
|
UserID string
|
||||||
|
Scopes []string
|
||||||
|
AuthTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t RefreshToken) Request() *RefreshTokenRequest {
|
||||||
|
return &RefreshTokenRequest{
|
||||||
|
userID: t.UserID,
|
||||||
|
clientID: t.ClientID,
|
||||||
|
scopes: t.Scopes,
|
||||||
|
authTime: t.AuthTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshTokenRequest struct {
|
||||||
|
clientID string
|
||||||
|
authTime time.Time
|
||||||
|
userID string
|
||||||
|
scopes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RefreshTokenRequest) GetAMR() []string {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RefreshTokenRequest) GetAudience() []string {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RefreshTokenRequest) GetAuthTime() time.Time {
|
||||||
|
return r.authTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RefreshTokenRequest) GetClientID() string {
|
||||||
|
return r.clientID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RefreshTokenRequest) GetScopes() []string {
|
||||||
|
return r.scopes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RefreshTokenRequest) GetSubject() string {
|
||||||
|
return r.userID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RefreshTokenRequest) SetCurrentScopes(scopes []string) {
|
||||||
|
r.scopes = scopes
|
||||||
|
}
|
22
polyculeconnect/internal/model/user.go
Normal file
22
polyculeconnect/internal/model/user.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
// Part of openid scope
|
||||||
|
Subject string
|
||||||
|
|
||||||
|
// Part of profile scope
|
||||||
|
Name string
|
||||||
|
FamilyName string
|
||||||
|
GivenName string
|
||||||
|
Nickname string
|
||||||
|
Picture string
|
||||||
|
UpdatedAt time.Time
|
||||||
|
|
||||||
|
// part of email scope
|
||||||
|
Email string
|
||||||
|
EmailVerified bool
|
||||||
|
}
|
4
polyculeconnect/internal/storage/local.go
Normal file
4
polyculeconnect/internal/storage/local.go
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
type LocalStorage struct {
|
||||||
|
}
|
377
polyculeconnect/internal/storage/storage.go
Normal file
377
polyculeconnect/internal/storage/storage.go
Normal file
|
@ -0,0 +1,377 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/client"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger"
|
||||||
|
"github.com/go-jose/go-jose/v4"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ErrNotImplemented(name string) error {
|
||||||
|
return fmt.Errorf("%s is not implemented", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage implements the Storage interface from zitadel/oidc/op
|
||||||
|
type Storage struct {
|
||||||
|
LocalStorage db.Storage
|
||||||
|
InitializedBackends map[uuid.UUID]*client.OIDCClient
|
||||||
|
Key *model.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Auth storage interface
|
||||||
|
*/
|
||||||
|
func (s *Storage) CreateAuthRequest(ctx context.Context, req *oidc.AuthRequest, userID string) (op.AuthRequest, error) {
|
||||||
|
|
||||||
|
// userID should normally be an empty string (to verify), we don't get it in our workflow from what I saw
|
||||||
|
// TODO: check this is indeed not needed / never present
|
||||||
|
logger.L.Debugf("Creating a new auth request")
|
||||||
|
|
||||||
|
// validate that the connector is correct
|
||||||
|
backendName, ok := stringFromCtx(ctx, "backendName")
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("no backend name provided")
|
||||||
|
}
|
||||||
|
selectedBackend, err := s.LocalStorage.BackendStorage().GetBackendByName(ctx, backendName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get backend: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var opReq model.AuthRequest
|
||||||
|
opReq.FromOIDCAuthRequest(req, selectedBackend.ID)
|
||||||
|
|
||||||
|
if err := s.LocalStorage.AuthRequestStorage().CreateAuthRequest(ctx, opReq); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save auth request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L.Debugf("Created a new auth request for backend %s", backendName)
|
||||||
|
|
||||||
|
return opReq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) AuthRequestByID(ctx context.Context, requestID string) (op.AuthRequest, error) {
|
||||||
|
logger.L.Debugf("Getting auth request with ID %s", requestID)
|
||||||
|
|
||||||
|
id, err := uuid.Parse(requestID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid format for uuid: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := s.LocalStorage.AuthRequestStorage().GetAuthRequestByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get auth request from DB: %w", err)
|
||||||
|
}
|
||||||
|
if req.UserID == "" {
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.LocalStorage.UserStorage().GetUserBySubject(ctx, req.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user information from DB: %w", err)
|
||||||
|
}
|
||||||
|
req.User = user
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) AuthRequestByCode(ctx context.Context, requestCode string) (op.AuthRequest, error) {
|
||||||
|
logger.L.Debugf("Getting auth request from code %s", requestCode)
|
||||||
|
|
||||||
|
authCode, err := s.LocalStorage.AuthCodeStorage().GetAuthCodeByCode(ctx, requestCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get auth code from DB: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := s.LocalStorage.AuthRequestStorage().GetAuthRequestByID(ctx, authCode.RequestID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get auth request from DB: %w", err)
|
||||||
|
}
|
||||||
|
if req.UserID == "" {
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.LocalStorage.UserStorage().GetUserBySubject(ctx, req.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user information from DB: %w", err)
|
||||||
|
}
|
||||||
|
req.User = user
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) SaveAuthCode(ctx context.Context, id string, code string) error {
|
||||||
|
logger.L.Debugf("Saving auth code %s for request %s", code, id)
|
||||||
|
|
||||||
|
requestID, err := uuid.Parse(id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid requestID %s: %w", requestID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
codeID := uuid.New()
|
||||||
|
|
||||||
|
savedCode := model.AuthCode{
|
||||||
|
CodeID: codeID,
|
||||||
|
RequestID: requestID,
|
||||||
|
Code: code,
|
||||||
|
}
|
||||||
|
return s.LocalStorage.AuthCodeStorage().CreateAuthCode(ctx, savedCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) DeleteAuthRequest(ctx context.Context, id string) error {
|
||||||
|
reqID, err := uuid.Parse(id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid id format: %w", err)
|
||||||
|
}
|
||||||
|
return s.LocalStorage.AuthRequestStorage().DeleteAuthRequest(ctx, reqID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) CreateAccessToken(ctx context.Context, req op.TokenRequest) (accessTokenID string, expiration time.Time, err error) {
|
||||||
|
accessTokenUUID := uuid.New()
|
||||||
|
var authTime time.Time
|
||||||
|
|
||||||
|
switch typedReq := req.(type) {
|
||||||
|
case *model.AuthRequest:
|
||||||
|
logger.L.Debug("Creating access token for new authentication")
|
||||||
|
authTime = typedReq.AuthTime
|
||||||
|
case *model.RefreshTokenRequest:
|
||||||
|
logger.L.Debug("Handling refresh token request")
|
||||||
|
authTime = typedReq.GetAuthTime()
|
||||||
|
default:
|
||||||
|
logger.L.Errorf("Unexpected type for request %v", err)
|
||||||
|
return "", time.Time{}, errors.New("failed to parse auth request")
|
||||||
|
}
|
||||||
|
|
||||||
|
expiration = authTime.Add(5 * time.Minute)
|
||||||
|
|
||||||
|
// token := model.Token{
|
||||||
|
// ID: accessTokenUUID,
|
||||||
|
// RefreshTokenID: refreshTokenUUID,
|
||||||
|
// Expiration: authTime.Add(5 * time.Minute),
|
||||||
|
// Subjet: request.GetSubject(),
|
||||||
|
// Audiences: request.GetAudience(),
|
||||||
|
// Scopes: request.GetScopes(),
|
||||||
|
// }
|
||||||
|
|
||||||
|
return accessTokenUUID.String(), expiration, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshTokenID string, expiration time.Time, err error) {
|
||||||
|
accessTokenUUID := uuid.New()
|
||||||
|
refreshTokenUUID := uuid.New()
|
||||||
|
var authTime time.Time
|
||||||
|
var clientID string
|
||||||
|
|
||||||
|
switch typedReq := request.(type) {
|
||||||
|
case *model.AuthRequest:
|
||||||
|
logger.L.Debug("Creating access token for new authentication")
|
||||||
|
clientID = typedReq.ClientID
|
||||||
|
authTime = typedReq.AuthTime
|
||||||
|
case *model.RefreshTokenRequest:
|
||||||
|
logger.L.Debug("Handling refresh token request")
|
||||||
|
clientID = typedReq.GetClientID()
|
||||||
|
authTime = typedReq.GetAuthTime()
|
||||||
|
default:
|
||||||
|
logger.L.Errorf("Unexpected type for request %v", err)
|
||||||
|
return "", "", time.Time{}, errors.New("failed to parse auth request")
|
||||||
|
}
|
||||||
|
|
||||||
|
expiration = authTime.Add(5 * time.Minute)
|
||||||
|
|
||||||
|
// token := model.Token{
|
||||||
|
// ID: accessTokenUUID,
|
||||||
|
// RefreshTokenID: refreshTokenUUID,
|
||||||
|
// Expiration: authTime.Add(5 * time.Minute),
|
||||||
|
// Subjet: request.GetSubject(),
|
||||||
|
// Audiences: request.GetAudience(),
|
||||||
|
// Scopes: request.GetScopes(),
|
||||||
|
// }
|
||||||
|
refreshToken := model.RefreshToken{
|
||||||
|
ID: refreshTokenUUID,
|
||||||
|
ClientID: clientID,
|
||||||
|
UserID: request.GetSubject(),
|
||||||
|
Scopes: request.GetScopes(),
|
||||||
|
AuthTime: authTime,
|
||||||
|
}
|
||||||
|
if err := s.LocalStorage.TokenStorage().AddRefreshToken(ctx, &refreshToken); err != nil {
|
||||||
|
return "", "", time.Time{}, fmt.Errorf("failed to insert token in DB: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessTokenUUID.String(), refreshTokenUUID.String(), expiration, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) TokenRequestByRefreshToken(ctx context.Context, refreshTokenID string) (op.RefreshTokenRequest, error) {
|
||||||
|
parsedID, err := uuid.Parse(refreshTokenID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid format for refresh token id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken, err := s.LocalStorage.TokenStorage().GetRefreshTokenByID(ctx, parsedID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, op.ErrInvalidRefreshToken
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to get refresh token: %w", err)
|
||||||
|
}
|
||||||
|
return refreshToken.Request(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) TerminateSession(ctx context.Context, userID string, clientID string) error {
|
||||||
|
return ErrNotImplemented("TerminateSession")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) RevokeToken(ctx context.Context, tokenOrTokenID string, userID string, clientID string) *oidc.Error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) GetRefreshTokenInfo(ctx context.Context, clientID string, stoken string) (string, string, error) {
|
||||||
|
return "", "", ErrNotImplemented("GetRefreshTokenInfo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) SigningKey(ctx context.Context) (op.SigningKey, error) {
|
||||||
|
return s.Key.SigningKey(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) SignatureAlgorithms(ctx context.Context) ([]jose.SignatureAlgorithm, error) {
|
||||||
|
return nil, ErrNotImplemented("SignatureAlgorithms")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) KeySet(ctx context.Context) ([]op.Key, error) {
|
||||||
|
return []op.Key{s.Key}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
OP storage
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (s *Storage) getClientWithDetails(ctx context.Context, authRequestID uuid.UUID) (op.Client, error) {
|
||||||
|
logger.L.Debug("Trying to get client details from auth request")
|
||||||
|
|
||||||
|
authRequest, err := s.LocalStorage.AuthRequestStorage().GetAuthRequestByID(ctx, authRequestID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get authRequest from local storage: %w", err)
|
||||||
|
}
|
||||||
|
backend, err := s.LocalStorage.BackendStorage().GetBackendByID(ctx, authRequest.BackendID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get associated backend from local storage: %w", err)
|
||||||
|
}
|
||||||
|
client, err := s.LocalStorage.ClientStorage().GetClientByID(ctx, authRequest.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get associated client from local storage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// oidcClient, ok := s.InitializedBackends[backend.ID]
|
||||||
|
// if !ok {
|
||||||
|
// return nil, fmt.Errorf("no initialized backend for ID %s", backend.ID)
|
||||||
|
// }
|
||||||
|
|
||||||
|
authRequest.Backend = backend
|
||||||
|
client.AuthRequest = authRequest
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're cheating a bit here since we're using the authrequest to get its associated client
|
||||||
|
// but a request is always associated to a backend, and we really need both, so we have no
|
||||||
|
// choice here. I'll maybe need to have a more elegant solution later, but not choice for now
|
||||||
|
func (s *Storage) GetClientByClientID(ctx context.Context, id string) (op.Client, error) {
|
||||||
|
logger.L.Debugf("Selecting client app with ID %s", id)
|
||||||
|
|
||||||
|
authRequestID, err := uuid.Parse(id)
|
||||||
|
if err != nil {
|
||||||
|
// it's not a UUID, it means this was called using client_id, we just return the client without details
|
||||||
|
client, err := s.LocalStorage.ClientStorage().GetClientByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get client %s from local storage: %w", id, err)
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have a UUID, it means we got a requestID, so we can get all details here
|
||||||
|
return s.getClientWithDetails(ctx, authRequestID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error {
|
||||||
|
logger.L.Debugf("Validating client secret %s for client %s", clientSecret, clientID)
|
||||||
|
|
||||||
|
client, err := s.LocalStorage.ClientStorage().GetClientByID(ctx, clientID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.Secret != clientSecret {
|
||||||
|
return errors.New("invalid secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error {
|
||||||
|
logger.L.Debugf("Setting user info for user %s", userID)
|
||||||
|
|
||||||
|
user, err := s.LocalStorage.UserStorage().GetUserBySubject(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get user from DB: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scopes {
|
||||||
|
switch s {
|
||||||
|
case "openid":
|
||||||
|
userinfo.Subject = user.Subject
|
||||||
|
case "profile":
|
||||||
|
userinfo.Name = user.Name
|
||||||
|
userinfo.FamilyName = user.FamilyName
|
||||||
|
userinfo.GivenName = user.GivenName
|
||||||
|
userinfo.Nickname = user.Nickname
|
||||||
|
userinfo.Picture = user.Picture
|
||||||
|
userinfo.UpdatedAt = oidc.FromTime(user.UpdatedAt)
|
||||||
|
case "email":
|
||||||
|
userinfo.Email = user.Email
|
||||||
|
userinfo.EmailVerified = oidc.Bool(user.EmailVerified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error {
|
||||||
|
return ErrNotImplemented("SetUserinfoFromToken")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) SetIntrospectionFromToken(ctx context.Context, userinfo *oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
|
||||||
|
return ErrNotImplemented("SetIntrospectionFromToken")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) {
|
||||||
|
// For now, let's just return nothing, we don't want to add any private scope
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) GetKeyByIDAndClientID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) {
|
||||||
|
return nil, ErrNotImplemented("GetKeyByIDAndClientID")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) {
|
||||||
|
return nil, ErrNotImplemented("ValidateJWTProfileScopes")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Health(ctx context.Context) error {
|
||||||
|
return ErrNotImplemented("Health")
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringFromCtx(ctx context.Context, key string) (string, bool) {
|
||||||
|
rawVal := ctx.Value(key)
|
||||||
|
if rawVal == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
val, ok := rawVal.(string)
|
||||||
|
return val, ok
|
||||||
|
}
|
|
@ -1,10 +1,28 @@
|
||||||
package logger
|
package logger
|
||||||
|
|
||||||
import "github.com/sirupsen/logrus"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
var L *logrus.Logger
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
var L *zap.SugaredLogger
|
||||||
|
|
||||||
|
func Init(level zap.AtomicLevel) {
|
||||||
|
conf := zap.Config{
|
||||||
|
Level: level,
|
||||||
|
Encoding: "console",
|
||||||
|
OutputPaths: []string{"stdout"},
|
||||||
|
ErrorOutputPaths: []string{"stderr"},
|
||||||
|
EncoderConfig: zap.NewDevelopmentEncoderConfig(),
|
||||||
|
}
|
||||||
|
conf.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||||
|
|
||||||
|
if l, err := conf.Build(); err != nil {
|
||||||
|
panic(fmt.Errorf("failed to init logger: %w", err))
|
||||||
|
} else {
|
||||||
|
L = l.Sugar()
|
||||||
|
}
|
||||||
|
|
||||||
func Init(level logrus.Level) {
|
|
||||||
L = logrus.New()
|
|
||||||
L.SetLevel(level)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,185 +1,13 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd"
|
||||||
"encoding/json"
|
_ "git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/app"
|
||||||
"flag"
|
_ "git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/backend"
|
||||||
"fmt"
|
_ "git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/db"
|
||||||
"os"
|
_ "git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/serve"
|
||||||
"os/signal"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/config"
|
|
||||||
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/connector"
|
|
||||||
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger"
|
|
||||||
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/server"
|
|
||||||
"github.com/dexidp/dex/connector/oidc"
|
|
||||||
dex_server "github.com/dexidp/dex/server"
|
|
||||||
"github.com/dexidp/dex/storage"
|
|
||||||
"github.com/dexidp/dex/storage/memory"
|
|
||||||
"github.com/dexidp/dex/storage/sql"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const stopTimeout = 10 * time.Second
|
|
||||||
|
|
||||||
type cliArgs struct {
|
|
||||||
configPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseArgs() *cliArgs {
|
|
||||||
configPath := flag.String("config", "", "Path to the JSON configuration file")
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
return &cliArgs{
|
|
||||||
configPath: *configPath,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initStorage(conf *config.AppConfig) (storage.Storage, error) {
|
|
||||||
var storageType storage.Storage
|
|
||||||
var err error
|
|
||||||
switch conf.StorageType {
|
|
||||||
case "memory":
|
|
||||||
storageType = memory.New(logger.L)
|
|
||||||
case "sqlite":
|
|
||||||
sqlconfig := sql.SQLite3{
|
|
||||||
File: conf.StorageConfig.File,
|
|
||||||
}
|
|
||||||
storageType, err = sqlconfig.Open(logger.L)
|
|
||||||
if err != nil {
|
|
||||||
logger.L.Fatalf("Failed to initialize sqlite backend: %s", err.Error())
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return storageType, fmt.Errorf("unsupported storage backend type: %s", conf.StorageType)
|
|
||||||
}
|
|
||||||
return storageType, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createConnector(backend *config.BackendConfig, dexConf *dex_server.Config, connectorIDs []string) error {
|
|
||||||
for _, id := range connectorIDs {
|
|
||||||
if id == backend.ID {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
backendConfJson, err := json.Marshal(backend.Config)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to serialize oidc config for backend %q: %s", backend.Name, err.Error())
|
|
||||||
}
|
|
||||||
return dexConf.Storage.CreateConnector(storage.Connector{
|
|
||||||
ID: backend.ID,
|
|
||||||
Name: backend.Name,
|
|
||||||
Type: string(backend.Type),
|
|
||||||
Config: backendConfJson,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
args := parseArgs()
|
cmd.Execute()
|
||||||
|
|
||||||
mainCtx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
conf, err := config.New(args.configPath)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Init(conf.LogLevel)
|
|
||||||
logger.L.Infof("Initialized logger with level %v", conf.LogLevel)
|
|
||||||
|
|
||||||
storageType, err := initStorage(conf)
|
|
||||||
if err != nil {
|
|
||||||
logger.L.Fatalf("Failed to initialize storage backend: %s", err.Error())
|
|
||||||
}
|
|
||||||
logger.L.Infof("Initialized storage backend %q", conf.StorageType)
|
|
||||||
dexConf := dex_server.Config{
|
|
||||||
Web: dex_server.WebConfig{
|
|
||||||
Dir: "./",
|
|
||||||
Theme: "default",
|
|
||||||
},
|
|
||||||
Storage: storageType,
|
|
||||||
Issuer: conf.OpenConnectConfig.Issuer,
|
|
||||||
SupportedResponseTypes: []string{"code"},
|
|
||||||
SkipApprovalScreen: false,
|
|
||||||
AllowedOrigins: []string{"*"},
|
|
||||||
Logger: logger.L,
|
|
||||||
PrometheusRegistry: prometheus.NewRegistry(),
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.L.Info("Initializing authentication backends")
|
|
||||||
|
|
||||||
dex_server.ConnectorsConfig["refuseAll"] = func() dex_server.ConnectorConfig { return new(connector.RefuseAllConfig) }
|
|
||||||
connectors, err := dexConf.Storage.ListConnectors()
|
|
||||||
if err != nil {
|
|
||||||
logger.L.Fatalf("Failed to get existing connectors: %s", err.Error())
|
|
||||||
}
|
|
||||||
var connectorIDs []string
|
|
||||||
for _, conn := range connectors {
|
|
||||||
connectorIDs = append(connectorIDs, conn.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
backend := config.BackendConfig{
|
|
||||||
Config: &oidc.Config{},
|
|
||||||
Name: "RefuseAll",
|
|
||||||
ID: "null",
|
|
||||||
Type: "refuseAll",
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := createConnector(&backend, &dexConf, connectorIDs); err != nil {
|
|
||||||
logger.L.Errorf("Failed to add connector for backend RefuseAll to stage: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, backend := range conf.OpenConnectConfig.BackendConfigs {
|
|
||||||
if err := createConnector(backend, &dexConf, connectorIDs); err != nil {
|
|
||||||
logger.L.Errorf("Failed to add connector for backend %q to stage: %s", backend.Name, err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.L.Info("Initializing clients")
|
|
||||||
for _, client := range conf.OpenConnectConfig.ClientConfigs {
|
|
||||||
if err := dexConf.Storage.CreateClient(*client); err != nil {
|
|
||||||
logger.L.Errorf("Failed to add client to storage: %s", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dexSrv, err := dex_server.NewServer(mainCtx, dexConf)
|
|
||||||
if err != nil {
|
|
||||||
logger.L.Fatalf("Failed to init dex server: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.L.Info("Initializing server")
|
|
||||||
s, err := server.New(conf, dexSrv, logger.L)
|
|
||||||
if err != nil {
|
|
||||||
logger.L.Fatalf("Failed to initialize server: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
go s.Run(mainCtx)
|
|
||||||
|
|
||||||
c := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(c, os.Interrupt)
|
|
||||||
|
|
||||||
logger.L.Info("Application successfully started")
|
|
||||||
|
|
||||||
logger.L.Debug("Waiting for stop signal")
|
|
||||||
select {
|
|
||||||
case <-s.Done():
|
|
||||||
logger.L.Fatal("Unexpected exit from server")
|
|
||||||
case <-c:
|
|
||||||
logger.L.Info("Stopping main application")
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.L.Debugf("Waiting %v for all daemons to stop", stopTimeout)
|
|
||||||
select {
|
|
||||||
case <-time.After(stopTimeout):
|
|
||||||
logger.L.Fatalf("Failed to stop all daemons in the expected time")
|
|
||||||
case <-s.Done():
|
|
||||||
logger.L.Info("web server successfully stopped")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.L.Info("Application successfully stopped")
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
package middlewares
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func WithLogger(handler http.Handler, l *logrus.Logger) http.Handler {
|
|
||||||
return &LoggerMiddleware{
|
|
||||||
l: l,
|
|
||||||
h: handler,
|
|
||||||
}
|
|
||||||
}
|
|
6
polyculeconnect/migrations/0_initial_schema.down.sql
Normal file
6
polyculeconnect/migrations/0_initial_schema.down.sql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
DROP TABLE "auth_code";
|
||||||
|
DROP TABLE "auth_request";
|
||||||
|
DROP TABLE "user";
|
||||||
|
DROP TABLE "backend";
|
||||||
|
DROP TABLE "client";
|
||||||
|
DROP TABLE "refresh_token";
|
68
polyculeconnect/migrations/0_initial_schema.up.sql
Normal file
68
polyculeconnect/migrations/0_initial_schema.up.sql
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
CREATE TABLE "backend" (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
oidc_issuer TEXT NOT NULL,
|
||||||
|
oidc_client_id TEXT NOT NULL,
|
||||||
|
oidc_client_secret TEXT NOT NULL,
|
||||||
|
oidc_redirect_uri TEXT NOT NULL,
|
||||||
|
oidc_scopes blob NOT NULL DEFAULT '[]' -- list of strings, json-encoded,
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "client" (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
secret TEXT NOT NULL,
|
||||||
|
redirect_uris blob NOT NULL,
|
||||||
|
trusted_peers blob NOT NULL,
|
||||||
|
public integer NOT NULL DEFAULT 0,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "user" (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL DEFAULT '',
|
||||||
|
family_name TEXT NOT NULL DEFAULT '',
|
||||||
|
given_name TEXT NOT NULL DEFAULT '',
|
||||||
|
nickname TEXT NOT NULL DEFAULT '',
|
||||||
|
picture TEXT NOT NULL DEFAULT '',
|
||||||
|
updated_at timestamp,
|
||||||
|
email TEXT NOT NULL DEFAULT '',
|
||||||
|
email_verified INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "auth_request" (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
client_id TEXT NOT NULL,
|
||||||
|
backend_id TEXT NOT NULL,
|
||||||
|
scopes blob NOT NULL, -- list of strings, json-encoded
|
||||||
|
redirect_uri TEXT NOT NULL,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
nonce TEXT NOT NULL,
|
||||||
|
response_type TEXT NOT NULL,
|
||||||
|
creation_time timestamp NOT NULL,
|
||||||
|
done INTEGER NOT NULL DEFAULT 0,
|
||||||
|
code_challenge STRING NOT NULL DEFAULT '',
|
||||||
|
code_challenge_method STRING NOT NULL DEFAULT '',
|
||||||
|
auth_time timestamp,
|
||||||
|
user_id TEXT NOT NULL DEFAULT '',
|
||||||
|
consent INTEGER NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY(backend_id) REFERENCES backend(id),
|
||||||
|
FOREIGN KEY(client_id) REFERENCES client(id),
|
||||||
|
FOREIGN KEY(user_id) REFERENCES user(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "auth_code" (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
code TEXT NOT NULL,
|
||||||
|
auth_request_id TEXT NOT NULL,
|
||||||
|
FOREIGN KEY(auth_request_id) REFERENCES auth_request(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE refresh_token (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
client_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
scopes blob NOT NULL, -- list of strings, json-encoded
|
||||||
|
auth_time timestamp NOT NULL,
|
||||||
|
FOREIGN KEY(client_id) REFERENCES client(id),
|
||||||
|
FOREIGN KEY(user_id) REFERENCES user(id)
|
||||||
|
);
|
|
@ -9,10 +9,14 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/config"
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/config"
|
||||||
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/controller/auth"
|
||||||
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/controller/ui"
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/controller/ui"
|
||||||
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/middlewares"
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/middlewares"
|
||||||
dex_server "github.com/dexidp/dex/server"
|
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/storage"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/google/uuid"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||||
|
"github.com/zitadel/oidc/v3/pkg/op"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
|
@ -24,7 +28,7 @@ type Server struct {
|
||||||
address string
|
address string
|
||||||
handler *http.ServeMux
|
handler *http.ServeMux
|
||||||
controllers map[string]http.Handler
|
controllers map[string]http.Handler
|
||||||
l *logrus.Logger
|
l *zap.SugaredLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUnixListener(sockPath string) (net.Listener, error) {
|
func newUnixListener(sockPath string) (net.Listener, error) {
|
||||||
|
@ -42,7 +46,7 @@ func newUnixListener(sockPath string) (net.Listener, error) {
|
||||||
return sock, nil
|
return sock, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(appConf *config.AppConfig, dexSrv *dex_server.Server, logger *logrus.Logger) (*Server, error) {
|
func New(appConf *config.AppConfig, oidcHandler *op.Provider, st *storage.Storage, logger *zap.SugaredLogger) (*Server, error) {
|
||||||
var listener net.Listener
|
var listener net.Listener
|
||||||
var addr string
|
var addr string
|
||||||
var err error
|
var err error
|
||||||
|
@ -64,10 +68,32 @@ func New(appConf *config.AppConfig, dexSrv *dex_server.Server, logger *logrus.Lo
|
||||||
}
|
}
|
||||||
|
|
||||||
controllers := map[string]http.Handler{
|
controllers := map[string]http.Handler{
|
||||||
ui.StaticRoute: middlewares.WithLogger(&ui.StaticController{}, logger),
|
ui.StaticRoute: middlewares.WithLogger(ui.NewStaticController(appConf.StaticDir), logger),
|
||||||
"/": middlewares.WithLogger(ui.NewIndexController(logger, dexSrv), logger),
|
auth.ApprovalRoute: middlewares.WithLogger(auth.NewApprovalController(logger, st.LocalStorage, appConf.StaticDir), logger),
|
||||||
|
"/": middlewares.WithLogger(ui.NewIndexController(logger, oidcHandler, appConf.StaticDir), logger),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userInfoHandler := auth.NewAuthCallbackController(logger, st)
|
||||||
|
loginHandlers := map[uuid.UUID]http.Handler{}
|
||||||
|
callbackHandlers := map[uuid.UUID]http.Handler{}
|
||||||
|
|
||||||
|
backends, err := st.LocalStorage.BackendStorage().GetAllBackends(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get list of backends from storage: %w", err)
|
||||||
|
}
|
||||||
|
for _, b := range backends {
|
||||||
|
provider, err := rp.NewRelyingPartyOIDC(context.Background(), b.Config.Issuer, b.Config.ClientID, b.Config.ClientSecret, b.Config.RedirectURI, b.Config.Scopes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create connector for backend %s: %w", b.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loginHandlers[b.ID] = middlewares.WithLogger(auth.NewAuthRedirectController(logger, provider, st), logger)
|
||||||
|
callbackHandlers[b.ID] = middlewares.WithLogger(rp.CodeExchangeHandler(rp.UserinfoCallback(userInfoHandler.HandleUserInfoCallback), provider), logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
controllers[auth.AuthRedirectRoute] = middlewares.WithLogger(auth.NewAuthDispatchController(logger, st, loginHandlers), logger)
|
||||||
|
controllers[auth.AuthCallbackRoute] = middlewares.WithLogger(auth.NewCallbackDispatchController(logger, st, callbackHandlers), logger)
|
||||||
|
|
||||||
m := http.NewServeMux()
|
m := http.NewServeMux()
|
||||||
|
|
||||||
return &Server{
|
return &Server{
|
||||||
|
|
34
polyculeconnect/services/idsecret.go
Normal file
34
polyculeconnect/services/idsecret.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// size in bytes of the client ids and secrets
|
||||||
|
const IDSecretSize = 32
|
||||||
|
|
||||||
|
func GenerateRandomHex(size int) (string, error) {
|
||||||
|
raw := make([]byte, size)
|
||||||
|
n, err := rand.Read(raw)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read from random generator: %w", err)
|
||||||
|
}
|
||||||
|
if n != size {
|
||||||
|
return "", fmt.Errorf("failed to read from random generator (%d/%d)", n, size)
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(raw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateClientIDSecret() (string, string, error) {
|
||||||
|
clientID, err := GenerateRandomHex(IDSecretSize)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to generate client id: %w", err)
|
||||||
|
}
|
||||||
|
clientSecret, err := GenerateRandomHex(IDSecretSize)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to generate client secret: %w", err)
|
||||||
|
}
|
||||||
|
return clientID, clientSecret, nil
|
||||||
|
}
|
5
polyculeconnect/static/scripts/approval.js
Normal file
5
polyculeconnect/static/scripts/approval.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
let approvalForm = document.getElementById("approvalform");
|
||||||
|
|
||||||
|
approvalForm.addEventListener("submit", (e) => {
|
||||||
|
handleSuccess();
|
||||||
|
});
|
17
polyculeconnect/static/scripts/appstate.js
Normal file
17
polyculeconnect/static/scripts/appstate.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
const STATE_SUCCESS = "SUCCESS";
|
||||||
|
const STATE_IN_PROGRESS = "IN PROGRESS"
|
||||||
|
const STATE_EMPTY = "NO STATE"
|
||||||
|
|
||||||
|
const stateKey = "appState"
|
||||||
|
|
||||||
|
function getState() {
|
||||||
|
state = localStorage.getItem(stateKey);
|
||||||
|
if (state === null) {
|
||||||
|
return STATE_EMPTY;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setState(value) {
|
||||||
|
localStorage.setItem(stateKey, value);
|
||||||
|
}
|
6
polyculeconnect/static/scripts/error.js
Normal file
6
polyculeconnect/static/scripts/error.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
let backButton = document.getElementById("error-back");
|
||||||
|
|
||||||
|
backButton.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
goBackToLogin();
|
||||||
|
});
|
11
polyculeconnect/static/scripts/form.js
Normal file
11
polyculeconnect/static/scripts/form.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
let connectorForm = document.getElementById("connectorform");
|
||||||
|
|
||||||
|
connectorForm.addEventListener("submit", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
let connectorName = document.getElementById("cname").value;
|
||||||
|
let rememberMe = document.getElementById("remember-me").checked;
|
||||||
|
if (rememberMe === true) {
|
||||||
|
localStorage.setItem(connectorNameKey, connectorName);
|
||||||
|
}
|
||||||
|
chooseConnector(connectorName);
|
||||||
|
});
|
|
@ -1,11 +1,47 @@
|
||||||
let connectorForm = document.getElementById("connectorform");
|
const connectorNameKey = "connectorName";
|
||||||
|
const connectorIDParam = "connector_id";
|
||||||
|
|
||||||
connectorForm.addEventListener("submit", (e) => {
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
|
function chooseConnector(connectorName) {
|
||||||
let nextURL = new URL(window.location.href);
|
let nextURL = new URL(window.location.href);
|
||||||
let connectorName = document.getElementById("cname").value;
|
nextURL.searchParams.append(connectorIDParam, connectorName);
|
||||||
nextURL.searchParams.append("connector_id", connectorName)
|
setState(STATE_IN_PROGRESS);
|
||||||
|
|
||||||
window.location.href = nextURL;
|
window.location.href = nextURL;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// Clean the cache in case previous authentication didn't succeed
|
||||||
|
// in order not to get stuck in a login loop
|
||||||
|
function handleFailedState() {
|
||||||
|
if (getState() !== STATE_SUCCESS) {
|
||||||
|
localStorage.removeItem(connectorNameKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the connector name to local storage in case the auth succeeded
|
||||||
|
// and the remember-me box was checked
|
||||||
|
function handleSuccess(connectorName) {
|
||||||
|
setState(STATE_SUCCESS);
|
||||||
|
if (localStorage.getItem(rememberMeKey)) {
|
||||||
|
localStorage.removeItem(rememberMeKey);
|
||||||
|
localStorage.setItem(connectorNameKey, connectorName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLoginPage() {
|
||||||
|
handleFailedState();
|
||||||
|
let connectorName = localStorage.getItem(connectorNameKey);
|
||||||
|
if (getState() === STATE_SUCCESS && connectorName != null) {
|
||||||
|
chooseConnector(connectorName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBackToLogin() {
|
||||||
|
let nextURL = new URL(window.location.href);
|
||||||
|
nextURL.searchParams.delete(connectorIDParam);
|
||||||
|
window.location.href = nextURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.location.pathname === "/authorize" && !urlParams.has(connectorIDParam)) {
|
||||||
|
handleLoginPage();
|
||||||
|
}
|
||||||
|
|
107
polyculeconnect/static/style/index.css
Normal file
107
polyculeconnect/static/style/index.css
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
:root {
|
||||||
|
--crust: #dce0e8;
|
||||||
|
--mantle: #e6e9ef;
|
||||||
|
--base: #eff1f5;
|
||||||
|
--surface-0: #ccd0da;
|
||||||
|
--surface-1: #bcc0cc;
|
||||||
|
--surface-2: #acb0be;
|
||||||
|
--overlay-0: #9ca0b0;
|
||||||
|
--overlay-1: #8c8fa1;
|
||||||
|
--overlay-2: #7c7f93;
|
||||||
|
--subtext-0: #6c6f85;
|
||||||
|
--subtext-1: #5c5f77;
|
||||||
|
--text: #4c4f69;
|
||||||
|
|
||||||
|
--logo-purple: #340c46;
|
||||||
|
--logo-yellow: #fcbf00;
|
||||||
|
--logo-pink: #e50051;
|
||||||
|
--logo-blue: #009fe3;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--base);
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
|
||||||
|
.site-logo img {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background-color: var(--mantle);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1;
|
||||||
|
row-gap: 20px;
|
||||||
|
padding: 15px 50px;
|
||||||
|
max-width: 50%;
|
||||||
|
width: fit-content;
|
||||||
|
margin: auto;
|
||||||
|
border: 1px solid var(--surface-0);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-content {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-elements {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1;
|
||||||
|
row-gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input::placeholder {
|
||||||
|
color: var(--subtext-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox-label {
|
||||||
|
color: var(--subtext-0);
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
border: none;
|
||||||
|
color: var(--mantle);
|
||||||
|
padding: 5px 20px;
|
||||||
|
border-radius: 3px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: medium;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-accept {
|
||||||
|
background-color: var(--logo-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-cancel {
|
||||||
|
background-color: var(--logo-pink);
|
||||||
|
}
|
|
@ -1,42 +1,36 @@
|
||||||
{{ template "header.html" . }}
|
{{ template "header.html" . }}
|
||||||
|
|
||||||
<div class="theme-panel">
|
<script src="/static/scripts/approval.js" defer></script>
|
||||||
<h2 class="theme-heading">Grant Access</h2>
|
|
||||||
|
|
||||||
<hr class="dex-separator">
|
<div class="container">
|
||||||
<div>
|
<div class="container-content">
|
||||||
{{ if .Scopes }}
|
{{ if .Scopes }}
|
||||||
<div class="dex-subtle-text">{{ .Client }} would like to:</div>
|
<div>{{ .Client }} would like to:</div>
|
||||||
<ul class="dex-list">
|
<ul>
|
||||||
{{ range $scope := .Scopes }}
|
{{ range $scope := .Scopes }}
|
||||||
<li>{{ $scope }}</li>
|
<li>{{ $scope }}</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<div class="dex-subtle-text">{{ .Client }} has not requested any personal information</div>
|
<div>{{ .Client }} has not requested any personal information</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<hr class="dex-separator">
|
|
||||||
|
|
||||||
<div>
|
<div class="form-buttons" id="approvalform">
|
||||||
<div class="theme-form-row">
|
<form method="post" class="container-form">
|
||||||
<form method="post">
|
<input type="hidden" name="req" value="{{ .AuthReqID }}" />
|
||||||
<input type="hidden" name="req" value="{{ .AuthReqID }}" />
|
<input type="hidden" name="approval" value="approve">
|
||||||
<input type="hidden" name="approval" value="approve">
|
<button type="submit" class="button button-accept">
|
||||||
<button type="submit" class="dex-btn theme-btn--success">
|
<span>Grant Access</span>
|
||||||
<span class="dex-btn-text">Grant Access</span>
|
</button>
|
||||||
</button>
|
</form>
|
||||||
</form>
|
<form method="post" class="container-form">
|
||||||
</div>
|
<input type="hidden" name="req" value="{{ .AuthReqID }}" />
|
||||||
<div class="theme-form-row">
|
<input type="hidden" name="approval" value="rejected">
|
||||||
<form method="post">
|
<button type="submit" class="button button-cancel">
|
||||||
<input type="hidden" name="req" value="{{ .AuthReqID }}" />
|
<span>Cancel</span>
|
||||||
<input type="hidden" name="approval" value="rejected">
|
</button>
|
||||||
<button type="submit" class="dex-btn theme-btn-provider">
|
</form>
|
||||||
<span class="dex-btn-text">Cancel</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
{{ template "header.html" . }}
|
{{ template "header.html" . }}
|
||||||
|
|
||||||
<div class="theme-panel">
|
<script src="/static/scripts/error.js" defer></script>
|
||||||
<h2 class="theme-heading">{{ .ErrType }}</h2>
|
|
||||||
<p>{{ .ErrMsg }}</p>
|
<div class="container">
|
||||||
|
<div class="container-content">
|
||||||
|
<button id="error-back" class="button button-cancel">Back</button>
|
||||||
|
<h2>{{ .ErrType }}</h2>
|
||||||
|
<p>{{ .ErrMsg }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ template "footer.html" . }}
|
{{ template "footer.html" . }}
|
|
@ -7,6 +7,8 @@
|
||||||
<title>PolyculeConnect</title>
|
<title>PolyculeConnect</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/style/index.css">
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/icons/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/static/icons/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/icons/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/static/icons/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/icons/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/static/icons/favicon-16x16.png">
|
||||||
|
@ -14,6 +16,16 @@
|
||||||
<link rel="mask-icon" href="/static/icons/safari-pinned-tab.svg" color="#5bbad5">
|
<link rel="mask-icon" href="/static/icons/safari-pinned-tab.svg" color="#5bbad5">
|
||||||
<meta name="msapplication-TileColor" content="#9f00a7">
|
<meta name="msapplication-TileColor" content="#9f00a7">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<script src="/static/scripts/appstate.js"></script>
|
||||||
|
<script src="/static/scripts/index.js"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
<div class="site-header">
|
||||||
|
<div class="site-logo">
|
||||||
|
<img src="/static/img/logo-text.png" alt="PolyculeConnect website logo">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
|
@ -1,13 +1,27 @@
|
||||||
{{ template "header.html" . }}
|
{{ template "header.html" . }}
|
||||||
|
|
||||||
<script src="/static/scripts/index.js" defer></script>
|
<script src="/static/scripts/form.js" defer></script>
|
||||||
|
|
||||||
<div>
|
<div class="container">
|
||||||
<form action="" id="connectorform">
|
<div class="container-content">
|
||||||
<label for="cname">Connector name</label>
|
Enter the service to use for login.
|
||||||
<input type="text" id="cname" name="connector_id">
|
</div>
|
||||||
<input type="submit">
|
|
||||||
</form>
|
<div class="container-content">
|
||||||
|
<form action="" id="connectorform" class="container-form">
|
||||||
|
<div class="form-elements">
|
||||||
|
<input type="text" id="cname" name="connector_id" placeholder="Service name" required
|
||||||
|
class="form-input">
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="remember-me" name="remember_me" class="form-checkbox">
|
||||||
|
<label for="remember-me" class="form-checkbox-label">Remember my choice</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-buttons">
|
||||||
|
<input type="submit" class="button button-accept" value="Continue">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ template "footer.html" . }}
|
{{ template "footer.html" . }}
|
Loading…
Reference in a new issue