Compare commits
15 Commits
clients/go
...
v1.10.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 44a1b9ad3a | |||
| df7773229c | |||
| 4430ce38a4 | |||
| 4ed2cecec5 | |||
| 9385c3846d | |||
| e450ade988 | |||
| 5b5e1a7ed6 | |||
| e4220b840e | |||
| cff7276293 | |||
| be3bc807b7 | |||
| ead32f72f8 | |||
| d7d80c0f25 | |||
| 41d01edfb4 | |||
| 9b521f3d99 | |||
| 115f23a3ea |
@@ -1377,7 +1377,7 @@ Error types exposed by every library:
|
|||||||
|
|
||||||
#### Go (`clients/go/`)
|
#### Go (`clients/go/`)
|
||||||
|
|
||||||
- Module: `git.wntrmute.dev/kyle/mcias/clients/go`
|
- Module: `git.wntrmute.dev/mc/mcias/clients/go`
|
||||||
- Package: `mciasgoclient`
|
- Package: `mciasgoclient`
|
||||||
- HTTP: `net/http` with custom `*tls.Config` for CA cert
|
- HTTP: `net/http` with custom `*tls.Config` for CA cert
|
||||||
- Token state: guarded by `sync.RWMutex`
|
- Token state: guarded by `sync.RWMutex`
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ expose the same API surface:
|
|||||||
|
|
||||||
| Language | Location | Install |
|
| Language | Location | Install |
|
||||||
|----------|----------|---------|
|
|----------|----------|---------|
|
||||||
| Go | `clients/go/` | `go get git.wntrmute.dev/kyle/mcias/clients/go` |
|
| Go | `clients/go/` | `go get git.wntrmute.dev/mc/mcias/clients/go` |
|
||||||
| Python | `clients/python/` | `pip install ./clients/python` |
|
| Python | `clients/python/` | `pip install ./clients/python` |
|
||||||
| Rust | `clients/rust/` | `cargo add mcias-client` |
|
| Rust | `clients/rust/` | `cargo add mcias-client` |
|
||||||
| Common Lisp | `clients/lisp/` | ASDF `mcias-client` |
|
| Common Lisp | `clients/lisp/` | ASDF `mcias-client` |
|
||||||
@@ -389,7 +389,7 @@ expose the same API surface:
|
|||||||
### Go
|
### Go
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import mcias "git.wntrmute.dev/kyle/mcias/clients/go"
|
import mcias "git.wntrmute.dev/mc/mcias/clients/go"
|
||||||
|
|
||||||
c, err := mcias.New("https://auth.example.com:8443", "/etc/mcias/server.crt", "")
|
c, err := mcias.New("https://auth.example.com:8443", "/etc/mcias/server.crt", "")
|
||||||
if err != nil { ... }
|
if err != nil { ... }
|
||||||
|
|||||||
10
Makefile
10
Makefile
@@ -19,7 +19,8 @@
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Variables
|
# Variables
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
MODULE := git.wntrmute.dev/kyle/mcias
|
MODULE := git.wntrmute.dev/mc/mcias
|
||||||
|
MCR := mcr.svc.mcp.metacircular.net:8443
|
||||||
BINARIES := mciassrv mciasctl mciasdb mciasgrpcctl
|
BINARIES := mciassrv mciasctl mciasdb mciasgrpcctl
|
||||||
BIN_DIR := bin
|
BIN_DIR := bin
|
||||||
MAN_DIR := man/man1
|
MAN_DIR := man/man1
|
||||||
@@ -163,9 +164,12 @@ dist: man
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# docker — build the Docker image
|
# docker — build the Docker image
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
.PHONY: docker
|
.PHONY: docker push
|
||||||
docker:
|
docker:
|
||||||
docker build --force-rm -t mcias:$(VERSION) -t mcias:latest .
|
docker build --force-rm -t $(MCR)/mcias:$(VERSION) .
|
||||||
|
|
||||||
|
push: docker
|
||||||
|
docker push $(MCR)/mcias:$(VERSION)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# docker-clean — remove local mcias Docker images
|
# docker-clean — remove local mcias Docker images
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ features implemented beyond the original plan scope.
|
|||||||
|
|
||||||
### Step 0.1: Go module and dependency setup
|
### Step 0.1: Go module and dependency setup
|
||||||
**Acceptance criteria:**
|
**Acceptance criteria:**
|
||||||
- `go.mod` exists with module path `git.wntrmute.dev/kyle/mcias`
|
- `go.mod` exists with module path `git.wntrmute.dev/mc/mcias`
|
||||||
- Required dependencies declared: `modernc.org/sqlite` (CGo-free SQLite),
|
- Required dependencies declared: `modernc.org/sqlite` (CGo-free SQLite),
|
||||||
`golang.org/x/crypto` (Argon2, Ed25519 helpers), `github.com/golang-jwt/jwt/v5`,
|
`golang.org/x/crypto` (Argon2, Ed25519 helpers), `github.com/golang-jwt/jwt/v5`,
|
||||||
`github.com/pelletier/go-toml/v2`, `github.com/google/uuid`,
|
`github.com/pelletier/go-toml/v2`, `github.com/google/uuid`,
|
||||||
@@ -543,7 +543,7 @@ implementation notes.
|
|||||||
|
|
||||||
### Step 9.2: Go client library
|
### Step 9.2: Go client library
|
||||||
**Acceptance criteria:**
|
**Acceptance criteria:**
|
||||||
- `clients/go/` — Go module `git.wntrmute.dev/kyle/mcias/clients/go`
|
- `clients/go/` — Go module `git.wntrmute.dev/mc/mcias/clients/go`
|
||||||
- Package `mciasgoclient` exposes the canonical API surface from Step 9.1
|
- Package `mciasgoclient` exposes the canonical API surface from Step 9.1
|
||||||
- Uses `net/http` with `crypto/tls`; custom CA cert supported via `x509.CertPool`
|
- Uses `net/http` with `crypto/tls`; custom CA cert supported via `x509.CertPool`
|
||||||
- Token stored in-memory; `Client.Token()` accessor returns current token
|
- Token stored in-memory; `Client.Token()` accessor returns current token
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for the technical design and
|
|||||||
**Prerequisites:** Go 1.26+, a C compiler (required by modernc.org/sqlite).
|
**Prerequisites:** Go 1.26+, a C compiler (required by modernc.org/sqlite).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://git.wntrmute.dev/kyle/mcias
|
git clone https://git.wntrmute.dev/mc/mcias
|
||||||
cd mcias
|
cd mcias
|
||||||
make build # produces bin/mciassrv, other binaries
|
make build # produces bin/mciassrv, other binaries
|
||||||
sudo make install
|
sudo make install
|
||||||
|
|||||||
13
RUNBOOK.md
13
RUNBOOK.md
@@ -461,6 +461,19 @@ See `dist/mcias.conf.docker.example` for the full annotated Docker config.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## MCP Deployment
|
||||||
|
|
||||||
|
MCIAS is **not** managed by MCP and does not run on rift. Because MCIAS is the
|
||||||
|
authentication root for the entire platform — including MCP itself — running it
|
||||||
|
under MCP would create a circular dependency. Instead, MCIAS runs as a systemd
|
||||||
|
service on a separate VPS (`svc.metacircular.net`).
|
||||||
|
|
||||||
|
All deployment, upgrades, and operational tasks use systemd directly on the VPS.
|
||||||
|
See the [Installation](#installation), [Routine Operations](#routine-operations),
|
||||||
|
and [Upgrading](#upgrading) sections above for the relevant procedures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Server fails to start: "open database"
|
### Server fails to start: "open database"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ set_pg_creds(account_id, host, port, database, username, password) → void
|
|||||||
| `MciasConflictError` | 409 | Conflict (e.g. duplicate username) |
|
| `MciasConflictError` | 409 | Conflict (e.g. duplicate username) |
|
||||||
| `MciasServerError` | 5xx | Unexpected server error |
|
| `MciasServerError` | 5xx | Unexpected server error |
|
||||||
`testdata/` contains canonical JSON response fixtures shared across language tests.
|
`testdata/` contains canonical JSON response fixtures shared across language tests.
|
||||||
- `go/` — Go module `git.wntrmute.dev/kyle/mcias/clients/go`
|
- `go/` — Go module `git.wntrmute.dev/mc/mcias/clients/go`
|
||||||
- `rust/` — Rust crate `mcias-client`
|
- `rust/` — Rust crate `mcias-client`
|
||||||
- `lisp/` — ASDF system `mcias-client`
|
- `lisp/` — ASDF system `mcias-client`
|
||||||
- `python/` — Python package `mcias_client`
|
- `python/` — Python package `mcias_client`
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ Go client library for the [MCIAS](../../README.md) identity and access managemen
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go get git.wntrmute.dev/kyle/mcias/clients/go
|
go get git.wntrmute.dev/mc/mcias/clients/go
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "git.wntrmute.dev/kyle/mcias/clients/go/mcias"
|
import "git.wntrmute.dev/mc/mcias/clients/go/mcias"
|
||||||
|
|
||||||
// Connect to the MCIAS server.
|
// Connect to the MCIAS server.
|
||||||
client, err := mcias.New("https://auth.example.com", mcias.Options{})
|
client, err := mcias.New("https://auth.example.com", mcias.Options{})
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
mcias "git.wntrmute.dev/kyle/mcias/clients/go"
|
mcias "git.wntrmute.dev/mc/mcias/clients/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module git.wntrmute.dev/kyle/mcias/clients/go
|
module git.wntrmute.dev/mc/mcias/clients/go
|
||||||
|
|
||||||
go 1.21
|
go 1.21
|
||||||
|
|||||||
1225
cmd/mciasctl/main.go
1225
cmd/mciasctl/main.go
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"golang.org/x/term"
|
|
||||||
|
"git.wntrmute.dev/mc/mcdsl/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t *tool) runAccount(args []string) {
|
func (t *tool) runAccount(args []string) {
|
||||||
@@ -233,20 +234,14 @@ func (t *tool) accountResetTOTP(args []string) {
|
|||||||
// readPassword reads a password from the terminal without echo.
|
// readPassword reads a password from the terminal without echo.
|
||||||
// Falls back to a regular line read if stdin is not a terminal (e.g. in tests).
|
// Falls back to a regular line read if stdin is not a terminal (e.g. in tests).
|
||||||
func readPassword(prompt string) (string, error) {
|
func readPassword(prompt string) (string, error) {
|
||||||
|
pw, err := terminal.ReadPassword(prompt)
|
||||||
|
if err == nil {
|
||||||
|
return pw, nil
|
||||||
|
}
|
||||||
|
// Fallback for piped input (e.g. tests).
|
||||||
fmt.Fprint(os.Stderr, prompt)
|
fmt.Fprint(os.Stderr, prompt)
|
||||||
fd := int(os.Stdin.Fd()) //nolint:gosec // G115: file descriptors are non-negative and fit in int on all supported platforms
|
|
||||||
if term.IsTerminal(fd) {
|
|
||||||
pw, err := term.ReadPassword(fd)
|
|
||||||
fmt.Fprintln(os.Stderr) // newline after hidden input
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("read password from terminal: %w", err)
|
|
||||||
}
|
|
||||||
return string(pw), nil
|
|
||||||
}
|
|
||||||
// Not a terminal: read a plain line (for piped input in tests).
|
|
||||||
var line string
|
var line string
|
||||||
_, err := fmt.Fscanln(os.Stdin, &line)
|
if _, err := fmt.Fscanln(os.Stdin, &line); err != nil {
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("read password: %w", err)
|
return "", fmt.Errorf("read password: %w", err)
|
||||||
}
|
}
|
||||||
return line, nil
|
return line, nil
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t *tool) runAudit(args []string) {
|
func (t *tool) runAudit(args []string) {
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newTestTool creates a tool backed by an in-memory SQLite database with a
|
// newTestTool creates a tool backed by an in-memory SQLite database with a
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t *tool) runPGCreds(args []string) {
|
func (t *tool) runPGCreds(args []string) {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// runRekey re-encrypts all secrets under a new passphrase-derived master key.
|
// runRekey re-encrypts all secrets under a new passphrase-derived master key.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t *tool) runSchema(args []string) {
|
func (t *tool) runSchema(args []string) {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// runSnapshot handles the "snapshot" command.
|
// runSnapshot handles the "snapshot" command.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -31,12 +31,12 @@ import (
|
|||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/grpcserver"
|
"git.wntrmute.dev/mc/mcias/internal/grpcserver"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/server"
|
"git.wntrmute.dev/mc/mcias/internal/server"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1774273680,
|
||||||
|
"narHash": "sha256-a++tZ1RQsDb1I0NHrFwdGuRlR5TORvCEUksM459wKUA=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "fdc7b8f7b30fdbedec91b71ed82f36e1637483ed",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
45
flake.nix
Normal file
45
flake.nix
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
description = "mcias - Metacircular Identity and Access Service";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{ self, nixpkgs }:
|
||||||
|
let
|
||||||
|
system = "x86_64-linux";
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
version = "1.8.0";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages.${system} = {
|
||||||
|
default = pkgs.buildGoModule {
|
||||||
|
pname = "mciasctl";
|
||||||
|
inherit version;
|
||||||
|
src = ./.;
|
||||||
|
vendorHash = null;
|
||||||
|
subPackages = [
|
||||||
|
"cmd/mciasctl"
|
||||||
|
"cmd/mciasgrpcctl"
|
||||||
|
];
|
||||||
|
ldflags = [
|
||||||
|
"-s"
|
||||||
|
"-w"
|
||||||
|
"-X main.version=${version}"
|
||||||
|
];
|
||||||
|
postInstall = ''
|
||||||
|
mkdir -p $out/share/zsh/site-functions
|
||||||
|
mkdir -p $out/share/bash-completion/completions
|
||||||
|
mkdir -p $out/share/fish/vendor_completions.d
|
||||||
|
$out/bin/mciasctl completion zsh > $out/share/zsh/site-functions/_mciasctl
|
||||||
|
$out/bin/mciasctl completion bash > $out/share/bash-completion/completions/mciasctl
|
||||||
|
$out/bin/mciasctl completion fish > $out/share/fish/vendor_completions.d/mciasctl.fish
|
||||||
|
$out/bin/mciasgrpcctl completion zsh > $out/share/zsh/site-functions/_mciasgrpcctl
|
||||||
|
$out/bin/mciasgrpcctl completion bash > $out/share/bash-completion/completions/mciasgrpcctl
|
||||||
|
$out/bin/mciasgrpcctl completion fish > $out/share/fish/vendor_completions.d/mciasgrpcctl.fish
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.11
|
// protoc-gen-go v1.36.11
|
||||||
// protoc v3.20.3
|
// protoc v6.32.1
|
||||||
// source: mcias/v1/account.proto
|
// source: mcias/v1/account.proto
|
||||||
|
|
||||||
package mciasv1
|
package mciasv1
|
||||||
@@ -1080,7 +1080,7 @@ const file_mcias_v1_account_proto_rawDesc = "" +
|
|||||||
"\n" +
|
"\n" +
|
||||||
"GetPGCreds\x12\x1b.mcias.v1.GetPGCredsRequest\x1a\x1c.mcias.v1.GetPGCredsResponse\x12G\n" +
|
"GetPGCreds\x12\x1b.mcias.v1.GetPGCredsRequest\x1a\x1c.mcias.v1.GetPGCredsResponse\x12G\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"SetPGCreds\x12\x1b.mcias.v1.SetPGCredsRequest\x1a\x1c.mcias.v1.SetPGCredsResponseB2Z0git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
"SetPGCreds\x12\x1b.mcias.v1.SetPGCredsRequest\x1a\x1c.mcias.v1.SetPGCredsResponseB0Z.git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_mcias_v1_account_proto_rawDescOnce sync.Once
|
file_mcias_v1_account_proto_rawDescOnce sync.Once
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.6.1
|
// - protoc-gen-go-grpc v1.6.1
|
||||||
// - protoc v3.20.3
|
// - protoc v6.32.1
|
||||||
// source: mcias/v1/account.proto
|
// source: mcias/v1/account.proto
|
||||||
|
|
||||||
package mciasv1
|
package mciasv1
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.11
|
// protoc-gen-go v1.36.11
|
||||||
// protoc v3.20.3
|
// protoc v6.32.1
|
||||||
// source: mcias/v1/admin.proto
|
// source: mcias/v1/admin.proto
|
||||||
|
|
||||||
package mciasv1
|
package mciasv1
|
||||||
@@ -238,7 +238,7 @@ const file_mcias_v1_admin_proto_rawDesc = "" +
|
|||||||
"\x01x\x18\x05 \x01(\tR\x01x2\x9a\x01\n" +
|
"\x01x\x18\x05 \x01(\tR\x01x2\x9a\x01\n" +
|
||||||
"\fAdminService\x12;\n" +
|
"\fAdminService\x12;\n" +
|
||||||
"\x06Health\x12\x17.mcias.v1.HealthRequest\x1a\x18.mcias.v1.HealthResponse\x12M\n" +
|
"\x06Health\x12\x17.mcias.v1.HealthRequest\x1a\x18.mcias.v1.HealthResponse\x12M\n" +
|
||||||
"\fGetPublicKey\x12\x1d.mcias.v1.GetPublicKeyRequest\x1a\x1e.mcias.v1.GetPublicKeyResponseB2Z0git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
"\fGetPublicKey\x12\x1d.mcias.v1.GetPublicKeyRequest\x1a\x1e.mcias.v1.GetPublicKeyResponseB0Z.git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_mcias_v1_admin_proto_rawDescOnce sync.Once
|
file_mcias_v1_admin_proto_rawDescOnce sync.Once
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.6.1
|
// - protoc-gen-go-grpc v1.6.1
|
||||||
// - protoc v3.20.3
|
// - protoc v6.32.1
|
||||||
// source: mcias/v1/admin.proto
|
// source: mcias/v1/admin.proto
|
||||||
|
|
||||||
package mciasv1
|
package mciasv1
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.11
|
// protoc-gen-go v1.36.11
|
||||||
// protoc v3.20.3
|
// protoc v6.32.1
|
||||||
// source: mcias/v1/auth.proto
|
// source: mcias/v1/auth.proto
|
||||||
|
|
||||||
package mciasv1
|
package mciasv1
|
||||||
@@ -919,7 +919,7 @@ const file_mcias_v1_auth_proto_rawDesc = "" +
|
|||||||
"\n" +
|
"\n" +
|
||||||
"RemoveTOTP\x12\x1b.mcias.v1.RemoveTOTPRequest\x1a\x1c.mcias.v1.RemoveTOTPResponse\x12n\n" +
|
"RemoveTOTP\x12\x1b.mcias.v1.RemoveTOTPRequest\x1a\x1c.mcias.v1.RemoveTOTPResponse\x12n\n" +
|
||||||
"\x17ListWebAuthnCredentials\x12(.mcias.v1.ListWebAuthnCredentialsRequest\x1a).mcias.v1.ListWebAuthnCredentialsResponse\x12q\n" +
|
"\x17ListWebAuthnCredentials\x12(.mcias.v1.ListWebAuthnCredentialsRequest\x1a).mcias.v1.ListWebAuthnCredentialsResponse\x12q\n" +
|
||||||
"\x18RemoveWebAuthnCredential\x12).mcias.v1.RemoveWebAuthnCredentialRequest\x1a*.mcias.v1.RemoveWebAuthnCredentialResponseB2Z0git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
"\x18RemoveWebAuthnCredential\x12).mcias.v1.RemoveWebAuthnCredentialRequest\x1a*.mcias.v1.RemoveWebAuthnCredentialResponseB0Z.git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_mcias_v1_auth_proto_rawDescOnce sync.Once
|
file_mcias_v1_auth_proto_rawDescOnce sync.Once
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.6.1
|
// - protoc-gen-go-grpc v1.6.1
|
||||||
// - protoc v3.20.3
|
// - protoc v6.32.1
|
||||||
// source: mcias/v1/auth.proto
|
// source: mcias/v1/auth.proto
|
||||||
|
|
||||||
package mciasv1
|
package mciasv1
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.11
|
// protoc-gen-go v1.36.11
|
||||||
// protoc v3.20.3
|
// protoc v6.32.1
|
||||||
// source: mcias/v1/common.proto
|
// source: mcias/v1/common.proto
|
||||||
|
|
||||||
package mciasv1
|
package mciasv1
|
||||||
@@ -349,7 +349,7 @@ const file_mcias_v1_common_proto_rawDesc = "" +
|
|||||||
"\x04port\x18\x05 \x01(\x05R\x04port\"5\n" +
|
"\x04port\x18\x05 \x01(\x05R\x04port\"5\n" +
|
||||||
"\x05Error\x12\x18\n" +
|
"\x05Error\x12\x18\n" +
|
||||||
"\amessage\x18\x01 \x01(\tR\amessage\x12\x12\n" +
|
"\amessage\x18\x01 \x01(\tR\amessage\x12\x12\n" +
|
||||||
"\x04code\x18\x02 \x01(\tR\x04codeB2Z0git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
"\x04code\x18\x02 \x01(\tR\x04codeB0Z.git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_mcias_v1_common_proto_rawDescOnce sync.Once
|
file_mcias_v1_common_proto_rawDescOnce sync.Once
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.11
|
// protoc-gen-go v1.36.11
|
||||||
// protoc v3.20.3
|
// protoc v6.32.1
|
||||||
// source: mcias/v1/policy.proto
|
// source: mcias/v1/policy.proto
|
||||||
|
|
||||||
package mciasv1
|
package mciasv1
|
||||||
@@ -703,7 +703,7 @@ const file_mcias_v1_policy_proto_rawDesc = "" +
|
|||||||
"\x10CreatePolicyRule\x12!.mcias.v1.CreatePolicyRuleRequest\x1a\".mcias.v1.CreatePolicyRuleResponse\x12P\n" +
|
"\x10CreatePolicyRule\x12!.mcias.v1.CreatePolicyRuleRequest\x1a\".mcias.v1.CreatePolicyRuleResponse\x12P\n" +
|
||||||
"\rGetPolicyRule\x12\x1e.mcias.v1.GetPolicyRuleRequest\x1a\x1f.mcias.v1.GetPolicyRuleResponse\x12Y\n" +
|
"\rGetPolicyRule\x12\x1e.mcias.v1.GetPolicyRuleRequest\x1a\x1f.mcias.v1.GetPolicyRuleResponse\x12Y\n" +
|
||||||
"\x10UpdatePolicyRule\x12!.mcias.v1.UpdatePolicyRuleRequest\x1a\".mcias.v1.UpdatePolicyRuleResponse\x12Y\n" +
|
"\x10UpdatePolicyRule\x12!.mcias.v1.UpdatePolicyRuleRequest\x1a\".mcias.v1.UpdatePolicyRuleResponse\x12Y\n" +
|
||||||
"\x10DeletePolicyRule\x12!.mcias.v1.DeletePolicyRuleRequest\x1a\".mcias.v1.DeletePolicyRuleResponseB2Z0git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
"\x10DeletePolicyRule\x12!.mcias.v1.DeletePolicyRuleRequest\x1a\".mcias.v1.DeletePolicyRuleResponseB0Z.git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_mcias_v1_policy_proto_rawDescOnce sync.Once
|
file_mcias_v1_policy_proto_rawDescOnce sync.Once
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.6.1
|
// - protoc-gen-go-grpc v1.6.1
|
||||||
// - protoc v3.20.3
|
// - protoc v6.32.1
|
||||||
// source: mcias/v1/policy.proto
|
// source: mcias/v1/policy.proto
|
||||||
|
|
||||||
package mciasv1
|
package mciasv1
|
||||||
|
|||||||
703
gen/mcias/v1/sso_client.pb.go
Normal file
703
gen/mcias/v1/sso_client.pb.go
Normal file
@@ -0,0 +1,703 @@
|
|||||||
|
// SSOClientService: CRUD management of SSO client registrations.
|
||||||
|
|
||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.36.11
|
||||||
|
// protoc v6.32.1
|
||||||
|
// source: mcias/v1/sso_client.proto
|
||||||
|
|
||||||
|
package mciasv1
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
unsafe "unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
// SSOClient is the wire representation of an SSO client registration.
|
||||||
|
type SSOClient struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
|
||||||
|
RedirectUri string `protobuf:"bytes,2,opt,name=redirect_uri,json=redirectUri,proto3" json:"redirect_uri,omitempty"`
|
||||||
|
Tags []string `protobuf:"bytes,3,rep,name=tags,proto3" json:"tags,omitempty"`
|
||||||
|
Enabled bool `protobuf:"varint,4,opt,name=enabled,proto3" json:"enabled,omitempty"`
|
||||||
|
CreatedAt string `protobuf:"bytes,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // RFC3339
|
||||||
|
UpdatedAt string `protobuf:"bytes,6,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` // RFC3339
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SSOClient) Reset() {
|
||||||
|
*x = SSOClient{}
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SSOClient) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SSOClient) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *SSOClient) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[0]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use SSOClient.ProtoReflect.Descriptor instead.
|
||||||
|
func (*SSOClient) Descriptor() ([]byte, []int) {
|
||||||
|
return file_mcias_v1_sso_client_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SSOClient) GetClientId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ClientId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SSOClient) GetRedirectUri() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.RedirectUri
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SSOClient) GetTags() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Tags
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SSOClient) GetEnabled() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Enabled
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SSOClient) GetCreatedAt() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.CreatedAt
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SSOClient) GetUpdatedAt() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.UpdatedAt
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListSSOClientsRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListSSOClientsRequest) Reset() {
|
||||||
|
*x = ListSSOClientsRequest{}
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListSSOClientsRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ListSSOClientsRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ListSSOClientsRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[1]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ListSSOClientsRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ListSSOClientsRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_mcias_v1_sso_client_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListSSOClientsResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Clients []*SSOClient `protobuf:"bytes,1,rep,name=clients,proto3" json:"clients,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListSSOClientsResponse) Reset() {
|
||||||
|
*x = ListSSOClientsResponse{}
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListSSOClientsResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ListSSOClientsResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ListSSOClientsResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[2]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ListSSOClientsResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ListSSOClientsResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_mcias_v1_sso_client_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ListSSOClientsResponse) GetClients() []*SSOClient {
|
||||||
|
if x != nil {
|
||||||
|
return x.Clients
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateSSOClientRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
|
||||||
|
RedirectUri string `protobuf:"bytes,2,opt,name=redirect_uri,json=redirectUri,proto3" json:"redirect_uri,omitempty"`
|
||||||
|
Tags []string `protobuf:"bytes,3,rep,name=tags,proto3" json:"tags,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateSSOClientRequest) Reset() {
|
||||||
|
*x = CreateSSOClientRequest{}
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[3]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateSSOClientRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CreateSSOClientRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *CreateSSOClientRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[3]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use CreateSSOClientRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*CreateSSOClientRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_mcias_v1_sso_client_proto_rawDescGZIP(), []int{3}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateSSOClientRequest) GetClientId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ClientId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateSSOClientRequest) GetRedirectUri() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.RedirectUri
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateSSOClientRequest) GetTags() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Tags
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateSSOClientResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Client *SSOClient `protobuf:"bytes,1,opt,name=client,proto3" json:"client,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateSSOClientResponse) Reset() {
|
||||||
|
*x = CreateSSOClientResponse{}
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[4]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateSSOClientResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CreateSSOClientResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *CreateSSOClientResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[4]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use CreateSSOClientResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*CreateSSOClientResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_mcias_v1_sso_client_proto_rawDescGZIP(), []int{4}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CreateSSOClientResponse) GetClient() *SSOClient {
|
||||||
|
if x != nil {
|
||||||
|
return x.Client
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetSSOClientRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetSSOClientRequest) Reset() {
|
||||||
|
*x = GetSSOClientRequest{}
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[5]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetSSOClientRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*GetSSOClientRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *GetSSOClientRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[5]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use GetSSOClientRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*GetSSOClientRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_mcias_v1_sso_client_proto_rawDescGZIP(), []int{5}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetSSOClientRequest) GetClientId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ClientId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetSSOClientResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Client *SSOClient `protobuf:"bytes,1,opt,name=client,proto3" json:"client,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetSSOClientResponse) Reset() {
|
||||||
|
*x = GetSSOClientResponse{}
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[6]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetSSOClientResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*GetSSOClientResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *GetSSOClientResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[6]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use GetSSOClientResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*GetSSOClientResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_mcias_v1_sso_client_proto_rawDescGZIP(), []int{6}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetSSOClientResponse) GetClient() *SSOClient {
|
||||||
|
if x != nil {
|
||||||
|
return x.Client
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateSSOClientRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
|
||||||
|
RedirectUri *string `protobuf:"bytes,2,opt,name=redirect_uri,json=redirectUri,proto3,oneof" json:"redirect_uri,omitempty"`
|
||||||
|
Tags []string `protobuf:"bytes,3,rep,name=tags,proto3" json:"tags,omitempty"`
|
||||||
|
Enabled *bool `protobuf:"varint,4,opt,name=enabled,proto3,oneof" json:"enabled,omitempty"`
|
||||||
|
UpdateTags bool `protobuf:"varint,5,opt,name=update_tags,json=updateTags,proto3" json:"update_tags,omitempty"` // when true, tags field is applied (allows clearing)
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateSSOClientRequest) Reset() {
|
||||||
|
*x = UpdateSSOClientRequest{}
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[7]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateSSOClientRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*UpdateSSOClientRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *UpdateSSOClientRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[7]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use UpdateSSOClientRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*UpdateSSOClientRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_mcias_v1_sso_client_proto_rawDescGZIP(), []int{7}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateSSOClientRequest) GetClientId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ClientId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateSSOClientRequest) GetRedirectUri() string {
|
||||||
|
if x != nil && x.RedirectUri != nil {
|
||||||
|
return *x.RedirectUri
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateSSOClientRequest) GetTags() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Tags
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateSSOClientRequest) GetEnabled() bool {
|
||||||
|
if x != nil && x.Enabled != nil {
|
||||||
|
return *x.Enabled
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateSSOClientRequest) GetUpdateTags() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.UpdateTags
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateSSOClientResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Client *SSOClient `protobuf:"bytes,1,opt,name=client,proto3" json:"client,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateSSOClientResponse) Reset() {
|
||||||
|
*x = UpdateSSOClientResponse{}
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[8]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateSSOClientResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*UpdateSSOClientResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *UpdateSSOClientResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[8]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use UpdateSSOClientResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*UpdateSSOClientResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_mcias_v1_sso_client_proto_rawDescGZIP(), []int{8}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UpdateSSOClientResponse) GetClient() *SSOClient {
|
||||||
|
if x != nil {
|
||||||
|
return x.Client
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteSSOClientRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DeleteSSOClientRequest) Reset() {
|
||||||
|
*x = DeleteSSOClientRequest{}
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[9]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DeleteSSOClientRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*DeleteSSOClientRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *DeleteSSOClientRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[9]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use DeleteSSOClientRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*DeleteSSOClientRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_mcias_v1_sso_client_proto_rawDescGZIP(), []int{9}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DeleteSSOClientRequest) GetClientId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ClientId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteSSOClientResponse struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DeleteSSOClientResponse) Reset() {
|
||||||
|
*x = DeleteSSOClientResponse{}
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[10]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *DeleteSSOClientResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*DeleteSSOClientResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *DeleteSSOClientResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_mcias_v1_sso_client_proto_msgTypes[10]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use DeleteSSOClientResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*DeleteSSOClientResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_mcias_v1_sso_client_proto_rawDescGZIP(), []int{10}
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_mcias_v1_sso_client_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
const file_mcias_v1_sso_client_proto_rawDesc = "" +
|
||||||
|
"\n" +
|
||||||
|
"\x19mcias/v1/sso_client.proto\x12\bmcias.v1\"\xb7\x01\n" +
|
||||||
|
"\tSSOClient\x12\x1b\n" +
|
||||||
|
"\tclient_id\x18\x01 \x01(\tR\bclientId\x12!\n" +
|
||||||
|
"\fredirect_uri\x18\x02 \x01(\tR\vredirectUri\x12\x12\n" +
|
||||||
|
"\x04tags\x18\x03 \x03(\tR\x04tags\x12\x18\n" +
|
||||||
|
"\aenabled\x18\x04 \x01(\bR\aenabled\x12\x1d\n" +
|
||||||
|
"\n" +
|
||||||
|
"created_at\x18\x05 \x01(\tR\tcreatedAt\x12\x1d\n" +
|
||||||
|
"\n" +
|
||||||
|
"updated_at\x18\x06 \x01(\tR\tupdatedAt\"\x17\n" +
|
||||||
|
"\x15ListSSOClientsRequest\"G\n" +
|
||||||
|
"\x16ListSSOClientsResponse\x12-\n" +
|
||||||
|
"\aclients\x18\x01 \x03(\v2\x13.mcias.v1.SSOClientR\aclients\"l\n" +
|
||||||
|
"\x16CreateSSOClientRequest\x12\x1b\n" +
|
||||||
|
"\tclient_id\x18\x01 \x01(\tR\bclientId\x12!\n" +
|
||||||
|
"\fredirect_uri\x18\x02 \x01(\tR\vredirectUri\x12\x12\n" +
|
||||||
|
"\x04tags\x18\x03 \x03(\tR\x04tags\"F\n" +
|
||||||
|
"\x17CreateSSOClientResponse\x12+\n" +
|
||||||
|
"\x06client\x18\x01 \x01(\v2\x13.mcias.v1.SSOClientR\x06client\"2\n" +
|
||||||
|
"\x13GetSSOClientRequest\x12\x1b\n" +
|
||||||
|
"\tclient_id\x18\x01 \x01(\tR\bclientId\"C\n" +
|
||||||
|
"\x14GetSSOClientResponse\x12+\n" +
|
||||||
|
"\x06client\x18\x01 \x01(\v2\x13.mcias.v1.SSOClientR\x06client\"\xce\x01\n" +
|
||||||
|
"\x16UpdateSSOClientRequest\x12\x1b\n" +
|
||||||
|
"\tclient_id\x18\x01 \x01(\tR\bclientId\x12&\n" +
|
||||||
|
"\fredirect_uri\x18\x02 \x01(\tH\x00R\vredirectUri\x88\x01\x01\x12\x12\n" +
|
||||||
|
"\x04tags\x18\x03 \x03(\tR\x04tags\x12\x1d\n" +
|
||||||
|
"\aenabled\x18\x04 \x01(\bH\x01R\aenabled\x88\x01\x01\x12\x1f\n" +
|
||||||
|
"\vupdate_tags\x18\x05 \x01(\bR\n" +
|
||||||
|
"updateTagsB\x0f\n" +
|
||||||
|
"\r_redirect_uriB\n" +
|
||||||
|
"\n" +
|
||||||
|
"\b_enabled\"F\n" +
|
||||||
|
"\x17UpdateSSOClientResponse\x12+\n" +
|
||||||
|
"\x06client\x18\x01 \x01(\v2\x13.mcias.v1.SSOClientR\x06client\"5\n" +
|
||||||
|
"\x16DeleteSSOClientRequest\x12\x1b\n" +
|
||||||
|
"\tclient_id\x18\x01 \x01(\tR\bclientId\"\x19\n" +
|
||||||
|
"\x17DeleteSSOClientResponse2\xbe\x03\n" +
|
||||||
|
"\x10SSOClientService\x12S\n" +
|
||||||
|
"\x0eListSSOClients\x12\x1f.mcias.v1.ListSSOClientsRequest\x1a .mcias.v1.ListSSOClientsResponse\x12V\n" +
|
||||||
|
"\x0fCreateSSOClient\x12 .mcias.v1.CreateSSOClientRequest\x1a!.mcias.v1.CreateSSOClientResponse\x12M\n" +
|
||||||
|
"\fGetSSOClient\x12\x1d.mcias.v1.GetSSOClientRequest\x1a\x1e.mcias.v1.GetSSOClientResponse\x12V\n" +
|
||||||
|
"\x0fUpdateSSOClient\x12 .mcias.v1.UpdateSSOClientRequest\x1a!.mcias.v1.UpdateSSOClientResponse\x12V\n" +
|
||||||
|
"\x0fDeleteSSOClient\x12 .mcias.v1.DeleteSSOClientRequest\x1a!.mcias.v1.DeleteSSOClientResponseB0Z.git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_mcias_v1_sso_client_proto_rawDescOnce sync.Once
|
||||||
|
file_mcias_v1_sso_client_proto_rawDescData []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_mcias_v1_sso_client_proto_rawDescGZIP() []byte {
|
||||||
|
file_mcias_v1_sso_client_proto_rawDescOnce.Do(func() {
|
||||||
|
file_mcias_v1_sso_client_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_mcias_v1_sso_client_proto_rawDesc), len(file_mcias_v1_sso_client_proto_rawDesc)))
|
||||||
|
})
|
||||||
|
return file_mcias_v1_sso_client_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_mcias_v1_sso_client_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
|
||||||
|
var file_mcias_v1_sso_client_proto_goTypes = []any{
|
||||||
|
(*SSOClient)(nil), // 0: mcias.v1.SSOClient
|
||||||
|
(*ListSSOClientsRequest)(nil), // 1: mcias.v1.ListSSOClientsRequest
|
||||||
|
(*ListSSOClientsResponse)(nil), // 2: mcias.v1.ListSSOClientsResponse
|
||||||
|
(*CreateSSOClientRequest)(nil), // 3: mcias.v1.CreateSSOClientRequest
|
||||||
|
(*CreateSSOClientResponse)(nil), // 4: mcias.v1.CreateSSOClientResponse
|
||||||
|
(*GetSSOClientRequest)(nil), // 5: mcias.v1.GetSSOClientRequest
|
||||||
|
(*GetSSOClientResponse)(nil), // 6: mcias.v1.GetSSOClientResponse
|
||||||
|
(*UpdateSSOClientRequest)(nil), // 7: mcias.v1.UpdateSSOClientRequest
|
||||||
|
(*UpdateSSOClientResponse)(nil), // 8: mcias.v1.UpdateSSOClientResponse
|
||||||
|
(*DeleteSSOClientRequest)(nil), // 9: mcias.v1.DeleteSSOClientRequest
|
||||||
|
(*DeleteSSOClientResponse)(nil), // 10: mcias.v1.DeleteSSOClientResponse
|
||||||
|
}
|
||||||
|
var file_mcias_v1_sso_client_proto_depIdxs = []int32{
|
||||||
|
0, // 0: mcias.v1.ListSSOClientsResponse.clients:type_name -> mcias.v1.SSOClient
|
||||||
|
0, // 1: mcias.v1.CreateSSOClientResponse.client:type_name -> mcias.v1.SSOClient
|
||||||
|
0, // 2: mcias.v1.GetSSOClientResponse.client:type_name -> mcias.v1.SSOClient
|
||||||
|
0, // 3: mcias.v1.UpdateSSOClientResponse.client:type_name -> mcias.v1.SSOClient
|
||||||
|
1, // 4: mcias.v1.SSOClientService.ListSSOClients:input_type -> mcias.v1.ListSSOClientsRequest
|
||||||
|
3, // 5: mcias.v1.SSOClientService.CreateSSOClient:input_type -> mcias.v1.CreateSSOClientRequest
|
||||||
|
5, // 6: mcias.v1.SSOClientService.GetSSOClient:input_type -> mcias.v1.GetSSOClientRequest
|
||||||
|
7, // 7: mcias.v1.SSOClientService.UpdateSSOClient:input_type -> mcias.v1.UpdateSSOClientRequest
|
||||||
|
9, // 8: mcias.v1.SSOClientService.DeleteSSOClient:input_type -> mcias.v1.DeleteSSOClientRequest
|
||||||
|
2, // 9: mcias.v1.SSOClientService.ListSSOClients:output_type -> mcias.v1.ListSSOClientsResponse
|
||||||
|
4, // 10: mcias.v1.SSOClientService.CreateSSOClient:output_type -> mcias.v1.CreateSSOClientResponse
|
||||||
|
6, // 11: mcias.v1.SSOClientService.GetSSOClient:output_type -> mcias.v1.GetSSOClientResponse
|
||||||
|
8, // 12: mcias.v1.SSOClientService.UpdateSSOClient:output_type -> mcias.v1.UpdateSSOClientResponse
|
||||||
|
10, // 13: mcias.v1.SSOClientService.DeleteSSOClient:output_type -> mcias.v1.DeleteSSOClientResponse
|
||||||
|
9, // [9:14] is the sub-list for method output_type
|
||||||
|
4, // [4:9] is the sub-list for method input_type
|
||||||
|
4, // [4:4] is the sub-list for extension type_name
|
||||||
|
4, // [4:4] is the sub-list for extension extendee
|
||||||
|
0, // [0:4] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_mcias_v1_sso_client_proto_init() }
|
||||||
|
func file_mcias_v1_sso_client_proto_init() {
|
||||||
|
if File_mcias_v1_sso_client_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file_mcias_v1_sso_client_proto_msgTypes[7].OneofWrappers = []any{}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_mcias_v1_sso_client_proto_rawDesc), len(file_mcias_v1_sso_client_proto_rawDesc)),
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 11,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 1,
|
||||||
|
},
|
||||||
|
GoTypes: file_mcias_v1_sso_client_proto_goTypes,
|
||||||
|
DependencyIndexes: file_mcias_v1_sso_client_proto_depIdxs,
|
||||||
|
MessageInfos: file_mcias_v1_sso_client_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_mcias_v1_sso_client_proto = out.File
|
||||||
|
file_mcias_v1_sso_client_proto_goTypes = nil
|
||||||
|
file_mcias_v1_sso_client_proto_depIdxs = nil
|
||||||
|
}
|
||||||
289
gen/mcias/v1/sso_client_grpc.pb.go
Normal file
289
gen/mcias/v1/sso_client_grpc.pb.go
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
// SSOClientService: CRUD management of SSO client registrations.
|
||||||
|
|
||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.6.1
|
||||||
|
// - protoc v6.32.1
|
||||||
|
// source: mcias/v1/sso_client.proto
|
||||||
|
|
||||||
|
package mciasv1
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.64.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
|
const (
|
||||||
|
SSOClientService_ListSSOClients_FullMethodName = "/mcias.v1.SSOClientService/ListSSOClients"
|
||||||
|
SSOClientService_CreateSSOClient_FullMethodName = "/mcias.v1.SSOClientService/CreateSSOClient"
|
||||||
|
SSOClientService_GetSSOClient_FullMethodName = "/mcias.v1.SSOClientService/GetSSOClient"
|
||||||
|
SSOClientService_UpdateSSOClient_FullMethodName = "/mcias.v1.SSOClientService/UpdateSSOClient"
|
||||||
|
SSOClientService_DeleteSSOClient_FullMethodName = "/mcias.v1.SSOClientService/DeleteSSOClient"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SSOClientServiceClient is the client API for SSOClientService service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
//
|
||||||
|
// SSOClientService manages SSO client registrations (admin only).
|
||||||
|
type SSOClientServiceClient interface {
|
||||||
|
// ListSSOClients returns all registered SSO clients.
|
||||||
|
ListSSOClients(ctx context.Context, in *ListSSOClientsRequest, opts ...grpc.CallOption) (*ListSSOClientsResponse, error)
|
||||||
|
// CreateSSOClient registers a new SSO client.
|
||||||
|
CreateSSOClient(ctx context.Context, in *CreateSSOClientRequest, opts ...grpc.CallOption) (*CreateSSOClientResponse, error)
|
||||||
|
// GetSSOClient returns a single SSO client by client_id.
|
||||||
|
GetSSOClient(ctx context.Context, in *GetSSOClientRequest, opts ...grpc.CallOption) (*GetSSOClientResponse, error)
|
||||||
|
// UpdateSSOClient applies a partial update to an SSO client.
|
||||||
|
UpdateSSOClient(ctx context.Context, in *UpdateSSOClientRequest, opts ...grpc.CallOption) (*UpdateSSOClientResponse, error)
|
||||||
|
// DeleteSSOClient removes an SSO client registration.
|
||||||
|
DeleteSSOClient(ctx context.Context, in *DeleteSSOClientRequest, opts ...grpc.CallOption) (*DeleteSSOClientResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type sSOClientServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSSOClientServiceClient(cc grpc.ClientConnInterface) SSOClientServiceClient {
|
||||||
|
return &sSOClientServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *sSOClientServiceClient) ListSSOClients(ctx context.Context, in *ListSSOClientsRequest, opts ...grpc.CallOption) (*ListSSOClientsResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListSSOClientsResponse)
|
||||||
|
err := c.cc.Invoke(ctx, SSOClientService_ListSSOClients_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *sSOClientServiceClient) CreateSSOClient(ctx context.Context, in *CreateSSOClientRequest, opts ...grpc.CallOption) (*CreateSSOClientResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CreateSSOClientResponse)
|
||||||
|
err := c.cc.Invoke(ctx, SSOClientService_CreateSSOClient_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *sSOClientServiceClient) GetSSOClient(ctx context.Context, in *GetSSOClientRequest, opts ...grpc.CallOption) (*GetSSOClientResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetSSOClientResponse)
|
||||||
|
err := c.cc.Invoke(ctx, SSOClientService_GetSSOClient_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *sSOClientServiceClient) UpdateSSOClient(ctx context.Context, in *UpdateSSOClientRequest, opts ...grpc.CallOption) (*UpdateSSOClientResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UpdateSSOClientResponse)
|
||||||
|
err := c.cc.Invoke(ctx, SSOClientService_UpdateSSOClient_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *sSOClientServiceClient) DeleteSSOClient(ctx context.Context, in *DeleteSSOClientRequest, opts ...grpc.CallOption) (*DeleteSSOClientResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(DeleteSSOClientResponse)
|
||||||
|
err := c.cc.Invoke(ctx, SSOClientService_DeleteSSOClient_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSOClientServiceServer is the server API for SSOClientService service.
|
||||||
|
// All implementations must embed UnimplementedSSOClientServiceServer
|
||||||
|
// for forward compatibility.
|
||||||
|
//
|
||||||
|
// SSOClientService manages SSO client registrations (admin only).
|
||||||
|
type SSOClientServiceServer interface {
|
||||||
|
// ListSSOClients returns all registered SSO clients.
|
||||||
|
ListSSOClients(context.Context, *ListSSOClientsRequest) (*ListSSOClientsResponse, error)
|
||||||
|
// CreateSSOClient registers a new SSO client.
|
||||||
|
CreateSSOClient(context.Context, *CreateSSOClientRequest) (*CreateSSOClientResponse, error)
|
||||||
|
// GetSSOClient returns a single SSO client by client_id.
|
||||||
|
GetSSOClient(context.Context, *GetSSOClientRequest) (*GetSSOClientResponse, error)
|
||||||
|
// UpdateSSOClient applies a partial update to an SSO client.
|
||||||
|
UpdateSSOClient(context.Context, *UpdateSSOClientRequest) (*UpdateSSOClientResponse, error)
|
||||||
|
// DeleteSSOClient removes an SSO client registration.
|
||||||
|
DeleteSSOClient(context.Context, *DeleteSSOClientRequest) (*DeleteSSOClientResponse, error)
|
||||||
|
mustEmbedUnimplementedSSOClientServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedSSOClientServiceServer must be embedded to have
|
||||||
|
// forward compatible implementations.
|
||||||
|
//
|
||||||
|
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||||
|
// pointer dereference when methods are called.
|
||||||
|
type UnimplementedSSOClientServiceServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedSSOClientServiceServer) ListSSOClients(context.Context, *ListSSOClientsRequest) (*ListSSOClientsResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method ListSSOClients not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedSSOClientServiceServer) CreateSSOClient(context.Context, *CreateSSOClientRequest) (*CreateSSOClientResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method CreateSSOClient not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedSSOClientServiceServer) GetSSOClient(context.Context, *GetSSOClientRequest) (*GetSSOClientResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method GetSSOClient not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedSSOClientServiceServer) UpdateSSOClient(context.Context, *UpdateSSOClientRequest) (*UpdateSSOClientResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method UpdateSSOClient not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedSSOClientServiceServer) DeleteSSOClient(context.Context, *DeleteSSOClientRequest) (*DeleteSSOClientResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method DeleteSSOClient not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedSSOClientServiceServer) mustEmbedUnimplementedSSOClientServiceServer() {}
|
||||||
|
func (UnimplementedSSOClientServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeSSOClientServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to SSOClientServiceServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeSSOClientServiceServer interface {
|
||||||
|
mustEmbedUnimplementedSSOClientServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterSSOClientServiceServer(s grpc.ServiceRegistrar, srv SSOClientServiceServer) {
|
||||||
|
// If the following call panics, it indicates UnimplementedSSOClientServiceServer was
|
||||||
|
// embedded by pointer and is nil. This will cause panics if an
|
||||||
|
// unimplemented method is ever invoked, so we test this at initialization
|
||||||
|
// time to prevent it from happening at runtime later due to I/O.
|
||||||
|
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||||
|
t.testEmbeddedByValue()
|
||||||
|
}
|
||||||
|
s.RegisterService(&SSOClientService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _SSOClientService_ListSSOClients_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListSSOClientsRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(SSOClientServiceServer).ListSSOClients(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: SSOClientService_ListSSOClients_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(SSOClientServiceServer).ListSSOClients(ctx, req.(*ListSSOClientsRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _SSOClientService_CreateSSOClient_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CreateSSOClientRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(SSOClientServiceServer).CreateSSOClient(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: SSOClientService_CreateSSOClient_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(SSOClientServiceServer).CreateSSOClient(ctx, req.(*CreateSSOClientRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _SSOClientService_GetSSOClient_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetSSOClientRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(SSOClientServiceServer).GetSSOClient(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: SSOClientService_GetSSOClient_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(SSOClientServiceServer).GetSSOClient(ctx, req.(*GetSSOClientRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _SSOClientService_UpdateSSOClient_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UpdateSSOClientRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(SSOClientServiceServer).UpdateSSOClient(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: SSOClientService_UpdateSSOClient_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(SSOClientServiceServer).UpdateSSOClient(ctx, req.(*UpdateSSOClientRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _SSOClientService_DeleteSSOClient_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(DeleteSSOClientRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(SSOClientServiceServer).DeleteSSOClient(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: SSOClientService_DeleteSSOClient_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(SSOClientServiceServer).DeleteSSOClient(ctx, req.(*DeleteSSOClientRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSOClientService_ServiceDesc is the grpc.ServiceDesc for SSOClientService service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var SSOClientService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "mcias.v1.SSOClientService",
|
||||||
|
HandlerType: (*SSOClientServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "ListSSOClients",
|
||||||
|
Handler: _SSOClientService_ListSSOClients_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "CreateSSOClient",
|
||||||
|
Handler: _SSOClientService_CreateSSOClient_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetSSOClient",
|
||||||
|
Handler: _SSOClientService_GetSSOClient_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "UpdateSSOClient",
|
||||||
|
Handler: _SSOClientService_UpdateSSOClient_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "DeleteSSOClient",
|
||||||
|
Handler: _SSOClientService_DeleteSSOClient_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "mcias/v1/sso_client.proto",
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.11
|
// protoc-gen-go v1.36.11
|
||||||
// protoc v3.20.3
|
// protoc v6.32.1
|
||||||
// source: mcias/v1/token.proto
|
// source: mcias/v1/token.proto
|
||||||
|
|
||||||
package mciasv1
|
package mciasv1
|
||||||
@@ -346,7 +346,7 @@ const file_mcias_v1_token_proto_rawDesc = "" +
|
|||||||
"\fTokenService\x12P\n" +
|
"\fTokenService\x12P\n" +
|
||||||
"\rValidateToken\x12\x1e.mcias.v1.ValidateTokenRequest\x1a\x1f.mcias.v1.ValidateTokenResponse\x12\\\n" +
|
"\rValidateToken\x12\x1e.mcias.v1.ValidateTokenRequest\x1a\x1f.mcias.v1.ValidateTokenResponse\x12\\\n" +
|
||||||
"\x11IssueServiceToken\x12\".mcias.v1.IssueServiceTokenRequest\x1a#.mcias.v1.IssueServiceTokenResponse\x12J\n" +
|
"\x11IssueServiceToken\x12\".mcias.v1.IssueServiceTokenRequest\x1a#.mcias.v1.IssueServiceTokenResponse\x12J\n" +
|
||||||
"\vRevokeToken\x12\x1c.mcias.v1.RevokeTokenRequest\x1a\x1d.mcias.v1.RevokeTokenResponseB2Z0git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
"\vRevokeToken\x12\x1c.mcias.v1.RevokeTokenRequest\x1a\x1d.mcias.v1.RevokeTokenResponseB0Z.git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1b\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_mcias_v1_token_proto_rawDescOnce sync.Once
|
file_mcias_v1_token_proto_rawDescOnce sync.Once
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.6.1
|
// - protoc-gen-go-grpc v1.6.1
|
||||||
// - protoc v3.20.3
|
// - protoc v6.32.1
|
||||||
// source: mcias/v1/token.proto
|
// source: mcias/v1/token.proto
|
||||||
|
|
||||||
package mciasv1
|
package mciasv1
|
||||||
|
|||||||
26
go.mod
26
go.mod
@@ -1,38 +1,40 @@
|
|||||||
module git.wntrmute.dev/kyle/mcias
|
module git.wntrmute.dev/mc/mcias
|
||||||
|
|
||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
git.wntrmute.dev/mc/mcdsl v1.4.0
|
||||||
|
github.com/go-webauthn/webauthn v0.16.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4
|
github.com/pelletier/go-toml/v2 v2.3.0
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
|
github.com/spf13/cobra v1.10.2
|
||||||
golang.org/x/crypto v0.49.0
|
golang.org/x/crypto v0.49.0
|
||||||
golang.org/x/term v0.41.0
|
google.golang.org/grpc v1.79.3
|
||||||
google.golang.org/grpc v1.74.2
|
google.golang.org/protobuf v1.36.11
|
||||||
google.golang.org/protobuf v1.36.7
|
modernc.org/sqlite v1.47.0
|
||||||
modernc.org/sqlite v1.46.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/go-webauthn/webauthn v0.16.1 // indirect
|
|
||||||
github.com/go-webauthn/x v0.2.2 // indirect
|
github.com/go-webauthn/x v0.2.2 // indirect
|
||||||
github.com/google/go-tpm v0.9.8 // indirect
|
github.com/google/go-tpm v0.9.8 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
|
||||||
golang.org/x/net v0.51.0 // indirect
|
golang.org/x/net v0.51.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
|
golang.org/x/term v0.41.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
modernc.org/libc v1.67.6 // indirect
|
modernc.org/libc v1.70.0 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
82
go.sum
82
go.sum
@@ -1,3 +1,8 @@
|
|||||||
|
git.wntrmute.dev/mc/mcdsl v1.4.0 h1:PsEIyskcjBduwHSRwNB/U/uSeU/cv3C8MVr0SRjBRLg=
|
||||||
|
git.wntrmute.dev/mc/mcdsl v1.4.0/go.mod h1:MhYahIu7Sg53lE2zpQ20nlrsoNRjQzOJBAlCmom2wJc=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
@@ -24,46 +29,56 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||||
|
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
|
||||||
|
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
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/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
@@ -79,28 +94,31 @@ golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
|||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
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=
|
||||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
|
||||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
|
||||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
||||||
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
@@ -109,8 +127,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=
|
||||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import (
|
|||||||
|
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrInvalidCredentials is returned for any authentication failure.
|
// ErrInvalidCredentials is returned for any authentication failure.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// openTestDB opens an in-memory SQLite database for testing.
|
// openTestDB opens an in-memory SQLite database for testing.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// openTestDB is defined in db_test.go in this package; reused here.
|
// openTestDB is defined in db_test.go in this package; reused here.
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ var migrationsFS embed.FS
|
|||||||
// LatestSchemaVersion is the highest migration version defined in the
|
// LatestSchemaVersion is the highest migration version defined in the
|
||||||
// migrations/ directory. Update this constant whenever a new migration file
|
// migrations/ directory. Update this constant whenever a new migration file
|
||||||
// is added.
|
// is added.
|
||||||
const LatestSchemaVersion = 9
|
const LatestSchemaVersion = 10
|
||||||
|
|
||||||
// newMigrate constructs a migrate.Migrate instance backed by the embedded SQL
|
// newMigrate constructs a migrate.Migrate instance backed by the embedded SQL
|
||||||
// files. It opens a dedicated *sql.DB using the same DSN as the main
|
// files. It opens a dedicated *sql.DB using the same DSN as the main
|
||||||
|
|||||||
10
internal/db/migrations/000010_sso_clients.up.sql
Normal file
10
internal/db/migrations/000010_sso_clients.up.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE sso_clients (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
client_id TEXT NOT NULL UNIQUE,
|
||||||
|
redirect_uri TEXT NOT NULL,
|
||||||
|
tags_json TEXT NOT NULL DEFAULT '[]',
|
||||||
|
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0,1)),
|
||||||
|
created_by INTEGER REFERENCES accounts(id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
||||||
|
);
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListCredentialedAccountIDs returns the set of account IDs that already have
|
// ListCredentialedAccountIDs returns the set of account IDs that already have
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// policyRuleCols is the column list for all policy rule SELECT queries.
|
// policyRuleCols is the column list for all policy rule SELECT queries.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCreateAndGetPolicyRule(t *testing.T) {
|
func TestCreateAndGetPolicyRule(t *testing.T) {
|
||||||
|
|||||||
206
internal/db/sso_clients.go
Normal file
206
internal/db/sso_clients.go
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ssoClientCols = `id, client_id, redirect_uri, tags_json, enabled, created_by, created_at, updated_at`
|
||||||
|
|
||||||
|
// CreateSSOClient inserts a new SSO client. The client_id must be unique
|
||||||
|
// and the redirect_uri must start with "https://".
|
||||||
|
func (db *DB) CreateSSOClient(clientID, redirectURI string, tags []string, createdBy *int64) (*model.SSOClient, error) {
|
||||||
|
if clientID == "" {
|
||||||
|
return nil, fmt.Errorf("db: client_id is required")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(redirectURI, "https://") {
|
||||||
|
return nil, fmt.Errorf("db: redirect_uri must start with https://")
|
||||||
|
}
|
||||||
|
if tags == nil {
|
||||||
|
tags = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
tagsJSON, err := json.Marshal(tags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("db: marshal tags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n := now()
|
||||||
|
result, err := db.sql.Exec(`
|
||||||
|
INSERT INTO sso_clients (client_id, redirect_uri, tags_json, enabled, created_by, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, 1, ?, ?, ?)
|
||||||
|
`, clientID, redirectURI, string(tagsJSON), createdBy, n, n)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("db: create SSO client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := result.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("db: create SSO client last insert id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
createdAt, err := parseTime(n)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.SSOClient{
|
||||||
|
ID: id,
|
||||||
|
ClientID: clientID,
|
||||||
|
RedirectURI: redirectURI,
|
||||||
|
Tags: tags,
|
||||||
|
Enabled: true,
|
||||||
|
CreatedBy: createdBy,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
UpdatedAt: createdAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSSOClient retrieves an SSO client by client_id.
|
||||||
|
// Returns ErrNotFound if no such client exists.
|
||||||
|
func (db *DB) GetSSOClient(clientID string) (*model.SSOClient, error) {
|
||||||
|
return scanSSOClient(db.sql.QueryRow(`
|
||||||
|
SELECT `+ssoClientCols+`
|
||||||
|
FROM sso_clients WHERE client_id = ?
|
||||||
|
`, clientID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSSOClients returns all SSO clients ordered by client_id.
|
||||||
|
func (db *DB) ListSSOClients() ([]*model.SSOClient, error) {
|
||||||
|
rows, err := db.sql.Query(`
|
||||||
|
SELECT ` + ssoClientCols + `
|
||||||
|
FROM sso_clients ORDER BY client_id ASC
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("db: list SSO clients: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
var clients []*model.SSOClient
|
||||||
|
for rows.Next() {
|
||||||
|
c, err := scanSSOClientRow(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
clients = append(clients, c)
|
||||||
|
}
|
||||||
|
return clients, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSSOClient updates the mutable fields of an SSO client.
|
||||||
|
// Only non-nil fields are changed.
|
||||||
|
func (db *DB) UpdateSSOClient(clientID string, redirectURI *string, tags *[]string, enabled *bool) error {
|
||||||
|
n := now()
|
||||||
|
setClauses := "updated_at = ?"
|
||||||
|
args := []interface{}{n}
|
||||||
|
|
||||||
|
if redirectURI != nil {
|
||||||
|
if !strings.HasPrefix(*redirectURI, "https://") {
|
||||||
|
return fmt.Errorf("db: redirect_uri must start with https://")
|
||||||
|
}
|
||||||
|
setClauses += ", redirect_uri = ?"
|
||||||
|
args = append(args, *redirectURI)
|
||||||
|
}
|
||||||
|
if tags != nil {
|
||||||
|
tagsJSON, err := json.Marshal(*tags)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("db: marshal tags: %w", err)
|
||||||
|
}
|
||||||
|
setClauses += ", tags_json = ?"
|
||||||
|
args = append(args, string(tagsJSON))
|
||||||
|
}
|
||||||
|
if enabled != nil {
|
||||||
|
enabledInt := 0
|
||||||
|
if *enabled {
|
||||||
|
enabledInt = 1
|
||||||
|
}
|
||||||
|
setClauses += ", enabled = ?"
|
||||||
|
args = append(args, enabledInt)
|
||||||
|
}
|
||||||
|
args = append(args, clientID)
|
||||||
|
|
||||||
|
res, err := db.sql.Exec(`UPDATE sso_clients SET `+setClauses+` WHERE client_id = ?`, args...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("db: update SSO client %s: %w", clientID, err)
|
||||||
|
}
|
||||||
|
n2, _ := res.RowsAffected()
|
||||||
|
if n2 == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSSOClient removes an SSO client by client_id.
|
||||||
|
func (db *DB) DeleteSSOClient(clientID string) error {
|
||||||
|
res, err := db.sql.Exec(`DELETE FROM sso_clients WHERE client_id = ?`, clientID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("db: delete SSO client %s: %w", clientID, err)
|
||||||
|
}
|
||||||
|
n, _ := res.RowsAffected()
|
||||||
|
if n == 0 {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanSSOClient scans a single SSO client from a *sql.Row.
|
||||||
|
func scanSSOClient(row *sql.Row) (*model.SSOClient, error) {
|
||||||
|
var c model.SSOClient
|
||||||
|
var enabledInt int
|
||||||
|
var tagsJSON, createdAtStr, updatedAtStr string
|
||||||
|
var createdBy *int64
|
||||||
|
|
||||||
|
err := row.Scan(&c.ID, &c.ClientID, &c.RedirectURI, &tagsJSON,
|
||||||
|
&enabledInt, &createdBy, &createdAtStr, &updatedAtStr)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("db: scan SSO client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return finishSSOClientScan(&c, enabledInt, createdBy, tagsJSON, createdAtStr, updatedAtStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanSSOClientRow scans a single SSO client from *sql.Rows.
|
||||||
|
func scanSSOClientRow(rows *sql.Rows) (*model.SSOClient, error) {
|
||||||
|
var c model.SSOClient
|
||||||
|
var enabledInt int
|
||||||
|
var tagsJSON, createdAtStr, updatedAtStr string
|
||||||
|
var createdBy *int64
|
||||||
|
|
||||||
|
err := rows.Scan(&c.ID, &c.ClientID, &c.RedirectURI, &tagsJSON,
|
||||||
|
&enabledInt, &createdBy, &createdAtStr, &updatedAtStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("db: scan SSO client row: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return finishSSOClientScan(&c, enabledInt, createdBy, tagsJSON, createdAtStr, updatedAtStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func finishSSOClientScan(c *model.SSOClient, enabledInt int, createdBy *int64, tagsJSON, createdAtStr, updatedAtStr string) (*model.SSOClient, error) {
|
||||||
|
c.Enabled = enabledInt == 1
|
||||||
|
c.CreatedBy = createdBy
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if c.CreatedAt, err = parseTime(createdAtStr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if c.UpdatedAt, err = parseTime(updatedAtStr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(tagsJSON), &c.Tags); err != nil {
|
||||||
|
return nil, fmt.Errorf("db: unmarshal SSO client tags: %w", err)
|
||||||
|
}
|
||||||
|
if c.Tags == nil {
|
||||||
|
c.Tags = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
192
internal/db/sso_clients_test.go
Normal file
192
internal/db/sso_clients_test.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateAndGetSSOClient(t *testing.T) {
|
||||||
|
db := openTestDB(t)
|
||||||
|
|
||||||
|
c, err := db.CreateSSOClient("mcr", "https://mcr.example.com/sso/callback", []string{"env:prod"}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateSSOClient: %v", err)
|
||||||
|
}
|
||||||
|
if c.ID == 0 {
|
||||||
|
t.Error("expected non-zero ID")
|
||||||
|
}
|
||||||
|
if c.ClientID != "mcr" {
|
||||||
|
t.Errorf("client_id = %q, want %q", c.ClientID, "mcr")
|
||||||
|
}
|
||||||
|
if !c.Enabled {
|
||||||
|
t.Error("new client should be enabled by default")
|
||||||
|
}
|
||||||
|
if len(c.Tags) != 1 || c.Tags[0] != "env:prod" {
|
||||||
|
t.Errorf("tags = %v, want [env:prod]", c.Tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := db.GetSSOClient("mcr")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetSSOClient: %v", err)
|
||||||
|
}
|
||||||
|
if got.RedirectURI != "https://mcr.example.com/sso/callback" {
|
||||||
|
t.Errorf("redirect_uri = %q", got.RedirectURI)
|
||||||
|
}
|
||||||
|
if len(got.Tags) != 1 || got.Tags[0] != "env:prod" {
|
||||||
|
t.Errorf("tags = %v after round-trip", got.Tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSSOClient_DuplicateClientID(t *testing.T) {
|
||||||
|
db := openTestDB(t)
|
||||||
|
|
||||||
|
_, err := db.CreateSSOClient("mcr", "https://mcr.example.com/cb", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("first create: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.CreateSSOClient("mcr", "https://other.example.com/cb", nil, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for duplicate client_id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSSOClient_Validation(t *testing.T) {
|
||||||
|
db := openTestDB(t)
|
||||||
|
|
||||||
|
_, err := db.CreateSSOClient("", "https://example.com/cb", nil, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for empty client_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.CreateSSOClient("mcr", "http://example.com/cb", nil, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for non-https redirect_uri")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSSOClient_NotFound(t *testing.T) {
|
||||||
|
db := openTestDB(t)
|
||||||
|
|
||||||
|
_, err := db.GetSSOClient("nonexistent")
|
||||||
|
if !errors.Is(err, ErrNotFound) {
|
||||||
|
t.Errorf("expected ErrNotFound, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListSSOClients(t *testing.T) {
|
||||||
|
db := openTestDB(t)
|
||||||
|
|
||||||
|
clients, err := db.ListSSOClients()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListSSOClients (empty): %v", err)
|
||||||
|
}
|
||||||
|
if len(clients) != 0 {
|
||||||
|
t.Errorf("expected 0 clients, got %d", len(clients))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = db.CreateSSOClient("mcat", "https://mcat.example.com/cb", nil, nil)
|
||||||
|
_, _ = db.CreateSSOClient("mcr", "https://mcr.example.com/cb", nil, nil)
|
||||||
|
|
||||||
|
clients, err = db.ListSSOClients()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListSSOClients: %v", err)
|
||||||
|
}
|
||||||
|
if len(clients) != 2 {
|
||||||
|
t.Fatalf("expected 2 clients, got %d", len(clients))
|
||||||
|
}
|
||||||
|
// Ordered by client_id ASC.
|
||||||
|
if clients[0].ClientID != "mcat" {
|
||||||
|
t.Errorf("first client = %q, want %q", clients[0].ClientID, "mcat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSSOClient(t *testing.T) {
|
||||||
|
db := openTestDB(t)
|
||||||
|
|
||||||
|
_, err := db.CreateSSOClient("mcr", "https://mcr.example.com/cb", []string{"a"}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newURI := "https://mcr.example.com/sso/callback"
|
||||||
|
newTags := []string{"b", "c"}
|
||||||
|
disabled := false
|
||||||
|
if err := db.UpdateSSOClient("mcr", &newURI, &newTags, &disabled); err != nil {
|
||||||
|
t.Fatalf("UpdateSSOClient: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := db.GetSSOClient("mcr")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get after update: %v", err)
|
||||||
|
}
|
||||||
|
if got.RedirectURI != newURI {
|
||||||
|
t.Errorf("redirect_uri = %q, want %q", got.RedirectURI, newURI)
|
||||||
|
}
|
||||||
|
if len(got.Tags) != 2 || got.Tags[0] != "b" {
|
||||||
|
t.Errorf("tags = %v, want [b c]", got.Tags)
|
||||||
|
}
|
||||||
|
if got.Enabled {
|
||||||
|
t.Error("expected enabled=false after update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSSOClient_NotFound(t *testing.T) {
|
||||||
|
db := openTestDB(t)
|
||||||
|
|
||||||
|
uri := "https://x.example.com/cb"
|
||||||
|
err := db.UpdateSSOClient("nonexistent", &uri, nil, nil)
|
||||||
|
if !errors.Is(err, ErrNotFound) {
|
||||||
|
t.Errorf("expected ErrNotFound, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteSSOClient(t *testing.T) {
|
||||||
|
db := openTestDB(t)
|
||||||
|
|
||||||
|
_, err := db.CreateSSOClient("mcr", "https://mcr.example.com/cb", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.DeleteSSOClient("mcr"); err != nil {
|
||||||
|
t.Fatalf("DeleteSSOClient: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.GetSSOClient("mcr")
|
||||||
|
if !errors.Is(err, ErrNotFound) {
|
||||||
|
t.Errorf("expected ErrNotFound after delete, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteSSOClient_NotFound(t *testing.T) {
|
||||||
|
db := openTestDB(t)
|
||||||
|
|
||||||
|
err := db.DeleteSSOClient("nonexistent")
|
||||||
|
if !errors.Is(err, ErrNotFound) {
|
||||||
|
t.Errorf("expected ErrNotFound, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSSOClient_NilTags(t *testing.T) {
|
||||||
|
db := openTestDB(t)
|
||||||
|
|
||||||
|
c, err := db.CreateSSOClient("mcr", "https://mcr.example.com/cb", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create: %v", err)
|
||||||
|
}
|
||||||
|
if c.Tags == nil {
|
||||||
|
t.Error("Tags should be empty slice, not nil")
|
||||||
|
}
|
||||||
|
if len(c.Tags) != 0 {
|
||||||
|
t.Errorf("expected 0 tags, got %d", len(c.Tags))
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := db.GetSSOClient("mcr")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get: %v", err)
|
||||||
|
}
|
||||||
|
if got.Tags == nil || len(got.Tags) != 0 {
|
||||||
|
t.Errorf("Tags round-trip: got %v", got.Tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package db
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetAccountTags_Empty(t *testing.T) {
|
func TestGetAccountTags_Empty(t *testing.T) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateWebAuthnCredential inserts a new WebAuthn credential record.
|
// CreateWebAuthnCredential inserts a new WebAuthn credential record.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWebAuthnCRUD(t *testing.T) {
|
func TestWebAuthnCRUD(t *testing.T) {
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/validate"
|
"git.wntrmute.dev/mc/mcias/internal/validate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type accountServiceServer struct {
|
type accountServiceServer struct {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
type adminServiceServer struct {
|
type adminServiceServer struct {
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
type authServiceServer struct {
|
type authServiceServer struct {
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type credentialServiceServer struct {
|
type credentialServiceServer struct {
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ import (
|
|||||||
"google.golang.org/grpc/peer"
|
"google.golang.org/grpc/peer"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
// contextKey is the unexported context key type for this package.
|
// contextKey is the unexported context key type for this package.
|
||||||
@@ -118,6 +118,7 @@ func (s *Server) buildServer(extra ...grpc.ServerOption) *grpc.Server {
|
|||||||
mciasv1.RegisterAccountServiceServer(srv, &accountServiceServer{s: s})
|
mciasv1.RegisterAccountServiceServer(srv, &accountServiceServer{s: s})
|
||||||
mciasv1.RegisterCredentialServiceServer(srv, &credentialServiceServer{s: s})
|
mciasv1.RegisterCredentialServiceServer(srv, &credentialServiceServer{s: s})
|
||||||
mciasv1.RegisterPolicyServiceServer(srv, &policyServiceServer{s: s})
|
mciasv1.RegisterPolicyServiceServer(srv, &policyServiceServer{s: s})
|
||||||
|
mciasv1.RegisterSSOClientServiceServer(srv, &ssoClientServiceServer{s: s})
|
||||||
|
|
||||||
return srv
|
return srv
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/grpc/test/bufconn"
|
"google.golang.org/grpc/test/bufconn"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import (
|
|||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/policy"
|
"git.wntrmute.dev/mc/mcias/internal/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
type policyServiceServer struct {
|
type policyServiceServer struct {
|
||||||
|
|||||||
187
internal/grpcserver/ssoclientservice.go
Normal file
187
internal/grpcserver/ssoclientservice.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
// ssoclientservice implements mciasv1.SSOClientServiceServer.
|
||||||
|
// All handlers are admin-only and delegate to the same db package used by
|
||||||
|
// the REST SSO client handlers in internal/server/handlers_sso_clients.go.
|
||||||
|
package grpcserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ssoClientServiceServer struct {
|
||||||
|
mciasv1.UnimplementedSSOClientServiceServer
|
||||||
|
s *Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func ssoClientToProto(c *model.SSOClient) *mciasv1.SSOClient {
|
||||||
|
return &mciasv1.SSOClient{
|
||||||
|
ClientId: c.ClientID,
|
||||||
|
RedirectUri: c.RedirectURI,
|
||||||
|
Tags: c.Tags,
|
||||||
|
Enabled: c.Enabled,
|
||||||
|
CreatedAt: c.CreatedAt.UTC().Format(time.RFC3339),
|
||||||
|
UpdatedAt: c.UpdatedAt.UTC().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *ssoClientServiceServer) ListSSOClients(ctx context.Context, _ *mciasv1.ListSSOClientsRequest) (*mciasv1.ListSSOClientsResponse, error) {
|
||||||
|
if err := ss.s.requireAdmin(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
clients, err := ss.s.db.ListSSOClients()
|
||||||
|
if err != nil {
|
||||||
|
ss.s.logger.Error("list SSO clients", "error", err)
|
||||||
|
return nil, status.Error(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &mciasv1.ListSSOClientsResponse{
|
||||||
|
Clients: make([]*mciasv1.SSOClient, 0, len(clients)),
|
||||||
|
}
|
||||||
|
for _, c := range clients {
|
||||||
|
resp.Clients = append(resp.Clients, ssoClientToProto(c))
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *ssoClientServiceServer) CreateSSOClient(ctx context.Context, req *mciasv1.CreateSSOClientRequest) (*mciasv1.CreateSSOClientResponse, error) {
|
||||||
|
if err := ss.s.requireAdmin(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ClientId == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "client_id is required")
|
||||||
|
}
|
||||||
|
if req.RedirectUri == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "redirect_uri is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := claimsFromContext(ctx)
|
||||||
|
var createdBy *int64
|
||||||
|
if claims != nil {
|
||||||
|
if actor, err := ss.s.db.GetAccountByUUID(claims.Subject); err == nil {
|
||||||
|
createdBy = &actor.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := ss.s.db.CreateSSOClient(req.ClientId, req.RedirectUri, req.Tags, createdBy)
|
||||||
|
if err != nil {
|
||||||
|
ss.s.logger.Error("create SSO client", "error", err)
|
||||||
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ss.s.db.WriteAuditEvent(model.EventSSOClientCreated, createdBy, nil, peerIP(ctx), //nolint:errcheck
|
||||||
|
fmt.Sprintf(`{"client_id":%q}`, c.ClientID))
|
||||||
|
|
||||||
|
return &mciasv1.CreateSSOClientResponse{Client: ssoClientToProto(c)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *ssoClientServiceServer) GetSSOClient(ctx context.Context, req *mciasv1.GetSSOClientRequest) (*mciasv1.GetSSOClientResponse, error) {
|
||||||
|
if err := ss.s.requireAdmin(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req.ClientId == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "client_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := ss.s.db.GetSSOClient(req.ClientId)
|
||||||
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
|
return nil, status.Error(codes.NotFound, "SSO client not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
ss.s.logger.Error("get SSO client", "error", err)
|
||||||
|
return nil, status.Error(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &mciasv1.GetSSOClientResponse{Client: ssoClientToProto(c)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *ssoClientServiceServer) UpdateSSOClient(ctx context.Context, req *mciasv1.UpdateSSOClientRequest) (*mciasv1.UpdateSSOClientResponse, error) {
|
||||||
|
if err := ss.s.requireAdmin(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req.ClientId == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "client_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var redirectURI *string
|
||||||
|
if req.RedirectUri != nil {
|
||||||
|
v := req.GetRedirectUri()
|
||||||
|
redirectURI = &v
|
||||||
|
}
|
||||||
|
var tags *[]string
|
||||||
|
if req.UpdateTags {
|
||||||
|
t := req.Tags
|
||||||
|
tags = &t
|
||||||
|
}
|
||||||
|
var enabled *bool
|
||||||
|
if req.Enabled != nil {
|
||||||
|
v := req.GetEnabled()
|
||||||
|
enabled = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ss.s.db.UpdateSSOClient(req.ClientId, redirectURI, tags, enabled)
|
||||||
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
|
return nil, status.Error(codes.NotFound, "SSO client not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
ss.s.logger.Error("update SSO client", "error", err)
|
||||||
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := claimsFromContext(ctx)
|
||||||
|
var actorID *int64
|
||||||
|
if claims != nil {
|
||||||
|
if actor, err := ss.s.db.GetAccountByUUID(claims.Subject); err == nil {
|
||||||
|
actorID = &actor.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ss.s.db.WriteAuditEvent(model.EventSSOClientUpdated, actorID, nil, peerIP(ctx), //nolint:errcheck
|
||||||
|
fmt.Sprintf(`{"client_id":%q}`, req.ClientId))
|
||||||
|
|
||||||
|
updated, err := ss.s.db.GetSSOClient(req.ClientId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Error(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &mciasv1.UpdateSSOClientResponse{Client: ssoClientToProto(updated)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *ssoClientServiceServer) DeleteSSOClient(ctx context.Context, req *mciasv1.DeleteSSOClientRequest) (*mciasv1.DeleteSSOClientResponse, error) {
|
||||||
|
if err := ss.s.requireAdmin(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req.ClientId == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "client_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ss.s.db.DeleteSSOClient(req.ClientId)
|
||||||
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
|
return nil, status.Error(codes.NotFound, "SSO client not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
ss.s.logger.Error("delete SSO client", "error", err)
|
||||||
|
return nil, status.Error(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := claimsFromContext(ctx)
|
||||||
|
var actorID *int64
|
||||||
|
if claims != nil {
|
||||||
|
if actor, err := ss.s.db.GetAccountByUUID(claims.Subject); err == nil {
|
||||||
|
actorID = &actor.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ss.s.db.WriteAuditEvent(model.EventSSOClientDeleted, actorID, nil, peerIP(ctx), //nolint:errcheck
|
||||||
|
fmt.Sprintf(`{"client_id":%q}`, req.ClientId))
|
||||||
|
|
||||||
|
return &mciasv1.DeleteSSOClientResponse{}, nil
|
||||||
|
}
|
||||||
@@ -10,10 +10,10 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
type tokenServiceServer struct {
|
type tokenServiceServer struct {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1"
|
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListWebAuthnCredentials returns metadata for an account's WebAuthn credentials.
|
// ListWebAuthnCredentials returns metadata for an account's WebAuthn credentials.
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/policy"
|
"git.wntrmute.dev/mc/mcias/internal/policy"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
// contextKey is the unexported type for context keys in this package, preventing
|
// contextKey is the unexported type for context keys in this package, preventing
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
func generateTestKey(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) {
|
func generateTestKey(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) {
|
||||||
|
|||||||
@@ -218,8 +218,29 @@ const (
|
|||||||
EventWebAuthnRemoved = "webauthn_removed"
|
EventWebAuthnRemoved = "webauthn_removed"
|
||||||
EventWebAuthnLoginOK = "webauthn_login_ok"
|
EventWebAuthnLoginOK = "webauthn_login_ok"
|
||||||
EventWebAuthnLoginFail = "webauthn_login_fail"
|
EventWebAuthnLoginFail = "webauthn_login_fail"
|
||||||
|
|
||||||
|
EventSSOAuthorize = "sso_authorize"
|
||||||
|
EventSSOLoginOK = "sso_login_ok"
|
||||||
|
|
||||||
|
EventSSOClientCreated = "sso_client_created"
|
||||||
|
EventSSOClientUpdated = "sso_client_updated"
|
||||||
|
EventSSOClientDeleted = "sso_client_deleted"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SSOClient represents a registered relying-party application that may use
|
||||||
|
// the MCIAS SSO authorization code flow. The ClientID serves as both the
|
||||||
|
// unique identifier and the service_name for policy evaluation.
|
||||||
|
type SSOClient struct {
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
CreatedBy *int64 `json:"-"`
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
RedirectURI string `json:"redirect_uri"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
ID int64 `json:"-"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
// ServiceAccountDelegate records that a specific account has been granted
|
// ServiceAccountDelegate records that a specific account has been granted
|
||||||
// permission to issue tokens for a given system account. Only admins can
|
// permission to issue tokens for a given system account. Only admins can
|
||||||
// add or remove delegates; delegates can issue/rotate tokens for that specific
|
// add or remove delegates; delegates can issue/rotate tokens for that specific
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ const (
|
|||||||
|
|
||||||
ActionEnrollWebAuthn Action = "webauthn:enroll" // self-service
|
ActionEnrollWebAuthn Action = "webauthn:enroll" // self-service
|
||||||
ActionRemoveWebAuthn Action = "webauthn:remove" // admin
|
ActionRemoveWebAuthn Action = "webauthn:remove" // admin
|
||||||
|
|
||||||
|
ActionManageSSOClients Action = "sso_clients:manage" // admin
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResourceType identifies what kind of object a request targets.
|
// ResourceType identifies what kind of object a request targets.
|
||||||
@@ -64,6 +66,7 @@ const (
|
|||||||
ResourceTOTP ResourceType = "totp"
|
ResourceTOTP ResourceType = "totp"
|
||||||
ResourcePolicy ResourceType = "policy"
|
ResourcePolicy ResourceType = "policy"
|
||||||
ResourceWebAuthn ResourceType = "webauthn"
|
ResourceWebAuthn ResourceType = "webauthn"
|
||||||
|
ResourceSSOClient ResourceType = "sso_client"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Effect is the outcome of policy evaluation.
|
// Effect is the outcome of policy evaluation.
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/middleware"
|
"git.wntrmute.dev/mc/mcias/internal/middleware"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/policy"
|
"git.wntrmute.dev/mc/mcias/internal/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---- Tag endpoints ----
|
// ---- Tag endpoints ----
|
||||||
|
|||||||
145
internal/server/handlers_sso.go
Normal file
145
internal/server/handlers_sso.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/middleware"
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/policy"
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/sso"
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ssoTokenRequest is the request body for POST /v1/sso/token.
|
||||||
|
type ssoTokenRequest struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
RedirectURI string `json:"redirect_uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSSOTokenExchange exchanges an SSO authorization code for a JWT token.
|
||||||
|
//
|
||||||
|
// Security design:
|
||||||
|
// - The authorization code is single-use (consumed via LoadAndDelete).
|
||||||
|
// - The client_id and redirect_uri must match the values stored when the code
|
||||||
|
// was issued, preventing a stolen code from being exchanged by a different
|
||||||
|
// service.
|
||||||
|
// - Policy evaluation uses the service_name and tags from the registered SSO
|
||||||
|
// client config (not from the request), preventing identity spoofing.
|
||||||
|
// - The code expires after 60 seconds to limit the interception window.
|
||||||
|
func (s *Server) handleSSOTokenExchange(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req ssoTokenRequest
|
||||||
|
if !decodeJSON(w, r, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Code == "" || req.ClientID == "" || req.RedirectURI == "" {
|
||||||
|
middleware.WriteError(w, http.StatusBadRequest, "code, client_id, and redirect_uri are required", "bad_request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume the authorization code (single-use).
|
||||||
|
ac, ok := sso.Consume(req.Code)
|
||||||
|
if !ok {
|
||||||
|
middleware.WriteError(w, http.StatusUnauthorized, "invalid or expired authorization code", "invalid_code")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security: verify client_id and redirect_uri match the stored values.
|
||||||
|
if ac.ClientID != req.ClientID || ac.RedirectURI != req.RedirectURI {
|
||||||
|
s.logger.Warn("sso: token exchange parameter mismatch",
|
||||||
|
"expected_client", ac.ClientID, "got_client", req.ClientID)
|
||||||
|
middleware.WriteError(w, http.StatusUnauthorized, "invalid or expired authorization code", "invalid_code")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the registered SSO client from the database for policy context.
|
||||||
|
client, clientErr := s.db.GetSSOClient(req.ClientID)
|
||||||
|
if clientErr != nil {
|
||||||
|
// Should not happen if the authorize endpoint validated, but defend in depth.
|
||||||
|
middleware.WriteError(w, http.StatusUnauthorized, "unknown client", "invalid_code")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !client.Enabled {
|
||||||
|
middleware.WriteError(w, http.StatusForbidden, "SSO client is disabled", "client_disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load account.
|
||||||
|
acct, err := s.db.GetAccountByID(ac.AccountID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("sso: load account for token exchange", "error", err, "account_id", ac.AccountID)
|
||||||
|
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if acct.Status != model.AccountStatusActive {
|
||||||
|
middleware.WriteError(w, http.StatusForbidden, "account is not active", "account_inactive")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load roles for policy evaluation and expiry decision.
|
||||||
|
roles, err := s.db.GetRoles(acct.ID)
|
||||||
|
if err != nil {
|
||||||
|
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Policy evaluation: client_id serves as both identifier and service_name.
|
||||||
|
{
|
||||||
|
input := policy.PolicyInput{
|
||||||
|
Subject: acct.UUID,
|
||||||
|
AccountType: string(acct.AccountType),
|
||||||
|
Roles: roles,
|
||||||
|
Action: policy.ActionLogin,
|
||||||
|
Resource: policy.Resource{
|
||||||
|
ServiceName: client.ClientID,
|
||||||
|
Tags: client.Tags,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if effect, _ := s.polEng.Evaluate(input); effect == policy.Deny {
|
||||||
|
s.writeAudit(r, model.EventLoginFail, &acct.ID, nil,
|
||||||
|
audit.JSON("reason", "policy_deny", "service_name", client.ClientID, "via", "sso"))
|
||||||
|
middleware.WriteError(w, http.StatusForbidden, "access denied by policy", "policy_denied")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine expiry.
|
||||||
|
expiry := s.cfg.DefaultExpiry()
|
||||||
|
for _, rol := range roles {
|
||||||
|
if rol == "admin" {
|
||||||
|
expiry = s.cfg.AdminExpiry()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
privKey, err := s.vault.PrivKey()
|
||||||
|
if err != nil {
|
||||||
|
middleware.WriteError(w, http.StatusServiceUnavailable, "vault sealed", "vault_sealed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tokenStr, claims, err := token.IssueToken(privKey, s.cfg.Tokens.Issuer, acct.UUID, roles, expiry)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("sso: issue token", "error", err)
|
||||||
|
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.TrackToken(claims.JTI, acct.ID, claims.IssuedAt, claims.ExpiresAt); err != nil {
|
||||||
|
s.logger.Error("sso: track token", "error", err)
|
||||||
|
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.writeAudit(r, model.EventSSOLoginOK, &acct.ID, nil,
|
||||||
|
audit.JSON("jti", claims.JTI, "client_id", client.ClientID))
|
||||||
|
s.writeAudit(r, model.EventTokenIssued, &acct.ID, nil,
|
||||||
|
audit.JSON("jti", claims.JTI, "via", "sso"))
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, loginResponse{
|
||||||
|
Token: tokenStr,
|
||||||
|
ExpiresAt: claims.ExpiresAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
})
|
||||||
|
}
|
||||||
175
internal/server/handlers_sso_clients.go
Normal file
175
internal/server/handlers_sso_clients.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/middleware"
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ssoClientResponse struct {
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
RedirectURI string `json:"redirect_uri"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ssoClientToResponse(c *model.SSOClient) ssoClientResponse {
|
||||||
|
return ssoClientResponse{
|
||||||
|
ClientID: c.ClientID,
|
||||||
|
RedirectURI: c.RedirectURI,
|
||||||
|
Tags: c.Tags,
|
||||||
|
Enabled: c.Enabled,
|
||||||
|
CreatedAt: c.CreatedAt.Format(time.RFC3339),
|
||||||
|
UpdatedAt: c.UpdatedAt.Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleListSSOClients(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clients, err := s.db.ListSSOClients()
|
||||||
|
if err != nil {
|
||||||
|
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := make([]ssoClientResponse, 0, len(clients))
|
||||||
|
for _, c := range clients {
|
||||||
|
resp = append(resp, ssoClientToResponse(c))
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
type createSSOClientRequest struct {
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
RedirectURI string `json:"redirect_uri"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleCreateSSOClient(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req createSSOClientRequest
|
||||||
|
if !decodeJSON(w, r, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ClientID == "" {
|
||||||
|
middleware.WriteError(w, http.StatusBadRequest, "client_id is required", "bad_request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.RedirectURI == "" {
|
||||||
|
middleware.WriteError(w, http.StatusBadRequest, "redirect_uri is required", "bad_request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := middleware.ClaimsFromContext(r.Context())
|
||||||
|
var createdBy *int64
|
||||||
|
if claims != nil {
|
||||||
|
if actor, err := s.db.GetAccountByUUID(claims.Subject); err == nil {
|
||||||
|
createdBy = &actor.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := s.db.CreateSSOClient(req.ClientID, req.RedirectURI, req.Tags, createdBy)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("create SSO client", "error", err)
|
||||||
|
middleware.WriteError(w, http.StatusBadRequest, err.Error(), "bad_request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.writeAudit(r, model.EventSSOClientCreated, createdBy, nil,
|
||||||
|
fmt.Sprintf(`{"client_id":%q}`, c.ClientID))
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusCreated, ssoClientToResponse(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleGetSSOClient(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clientID := r.PathValue("clientId")
|
||||||
|
|
||||||
|
c, err := s.db.GetSSOClient(clientID)
|
||||||
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
|
middleware.WriteError(w, http.StatusNotFound, "SSO client not found", "not_found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, ssoClientToResponse(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateSSOClientRequest struct {
|
||||||
|
RedirectURI *string `json:"redirect_uri,omitempty"`
|
||||||
|
Tags *[]string `json:"tags,omitempty"`
|
||||||
|
Enabled *bool `json:"enabled,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUpdateSSOClient(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clientID := r.PathValue("clientId")
|
||||||
|
|
||||||
|
var req updateSSOClientRequest
|
||||||
|
if !decodeJSON(w, r, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.db.UpdateSSOClient(clientID, req.RedirectURI, req.Tags, req.Enabled)
|
||||||
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
|
middleware.WriteError(w, http.StatusNotFound, "SSO client not found", "not_found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("update SSO client", "error", err)
|
||||||
|
middleware.WriteError(w, http.StatusBadRequest, err.Error(), "bad_request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := middleware.ClaimsFromContext(r.Context())
|
||||||
|
var actorID *int64
|
||||||
|
if claims != nil {
|
||||||
|
if actor, err := s.db.GetAccountByUUID(claims.Subject); err == nil {
|
||||||
|
actorID = &actor.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.writeAudit(r, model.EventSSOClientUpdated, actorID, nil,
|
||||||
|
fmt.Sprintf(`{"client_id":%q}`, clientID))
|
||||||
|
|
||||||
|
c, err := s.db.GetSSOClient(clientID)
|
||||||
|
if err != nil {
|
||||||
|
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, ssoClientToResponse(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleDeleteSSOClient(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clientID := r.PathValue("clientId")
|
||||||
|
|
||||||
|
err := s.db.DeleteSSOClient(clientID)
|
||||||
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
|
middleware.WriteError(w, http.StatusNotFound, "SSO client not found", "not_found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := middleware.ClaimsFromContext(r.Context())
|
||||||
|
var actorID *int64
|
||||||
|
if claims != nil {
|
||||||
|
if actor, err := s.db.GetAccountByUUID(claims.Subject); err == nil {
|
||||||
|
actorID = &actor.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.writeAudit(r, model.EventSSOClientDeleted, actorID, nil,
|
||||||
|
fmt.Sprintf(`{"client_id":%q}`, clientID))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
@@ -23,14 +23,14 @@ import (
|
|||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
libwebauthn "github.com/go-webauthn/webauthn/webauthn"
|
libwebauthn "github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/middleware"
|
"git.wntrmute.dev/mc/mcias/internal/middleware"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/policy"
|
"git.wntrmute.dev/mc/mcias/internal/policy"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
mciaswebauthn "git.wntrmute.dev/kyle/mcias/internal/webauthn"
|
mciaswebauthn "git.wntrmute.dev/mc/mcias/internal/webauthn"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -21,19 +21,19 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/middleware"
|
"git.wntrmute.dev/mc/mcias/internal/middleware"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/policy"
|
"git.wntrmute.dev/mc/mcias/internal/policy"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/ui"
|
"git.wntrmute.dev/mc/mcias/internal/ui"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/validate"
|
"git.wntrmute.dev/mc/mcias/internal/validate"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
"git.wntrmute.dev/kyle/mcias/web"
|
"git.wntrmute.dev/mc/mcias/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server holds the dependencies injected into all handlers.
|
// Server holds the dependencies injected into all handlers.
|
||||||
@@ -215,6 +215,7 @@ func (s *Server) Handler() http.Handler {
|
|||||||
mux.HandleFunc("GET /v1/health", s.handleHealth)
|
mux.HandleFunc("GET /v1/health", s.handleHealth)
|
||||||
mux.HandleFunc("GET /v1/keys/public", s.handlePublicKey)
|
mux.HandleFunc("GET /v1/keys/public", s.handlePublicKey)
|
||||||
mux.Handle("POST /v1/auth/login", loginRateLimit(http.HandlerFunc(s.handleLogin)))
|
mux.Handle("POST /v1/auth/login", loginRateLimit(http.HandlerFunc(s.handleLogin)))
|
||||||
|
mux.Handle("POST /v1/sso/token", loginRateLimit(http.HandlerFunc(s.handleSSOTokenExchange)))
|
||||||
mux.Handle("POST /v1/token/validate", loginRateLimit(http.HandlerFunc(s.handleTokenValidate)))
|
mux.Handle("POST /v1/token/validate", loginRateLimit(http.HandlerFunc(s.handleTokenValidate)))
|
||||||
|
|
||||||
// API documentation: Swagger UI at /docs and raw spec at /docs/openapi.yaml.
|
// API documentation: Swagger UI at /docs and raw spec at /docs/openapi.yaml.
|
||||||
@@ -372,6 +373,18 @@ func (s *Server) Handler() http.Handler {
|
|||||||
mux.Handle("DELETE /v1/policy/rules/{id}",
|
mux.Handle("DELETE /v1/policy/rules/{id}",
|
||||||
requirePolicy(policy.ActionManageRules, policy.ResourcePolicy, nil)(http.HandlerFunc(s.handleDeletePolicyRule)))
|
requirePolicy(policy.ActionManageRules, policy.ResourcePolicy, nil)(http.HandlerFunc(s.handleDeletePolicyRule)))
|
||||||
|
|
||||||
|
// SSO client management (admin-only).
|
||||||
|
mux.Handle("GET /v1/sso/clients",
|
||||||
|
requirePolicy(policy.ActionManageSSOClients, policy.ResourceSSOClient, nil)(http.HandlerFunc(s.handleListSSOClients)))
|
||||||
|
mux.Handle("POST /v1/sso/clients",
|
||||||
|
requirePolicy(policy.ActionManageSSOClients, policy.ResourceSSOClient, nil)(http.HandlerFunc(s.handleCreateSSOClient)))
|
||||||
|
mux.Handle("GET /v1/sso/clients/{clientId}",
|
||||||
|
requirePolicy(policy.ActionManageSSOClients, policy.ResourceSSOClient, nil)(http.HandlerFunc(s.handleGetSSOClient)))
|
||||||
|
mux.Handle("PATCH /v1/sso/clients/{clientId}",
|
||||||
|
requirePolicy(policy.ActionManageSSOClients, policy.ResourceSSOClient, nil)(http.HandlerFunc(s.handleUpdateSSOClient)))
|
||||||
|
mux.Handle("DELETE /v1/sso/clients/{clientId}",
|
||||||
|
requirePolicy(policy.ActionManageSSOClients, policy.ResourceSSOClient, nil)(http.HandlerFunc(s.handleDeleteSSOClient)))
|
||||||
|
|
||||||
// UI routes (HTMX-based management frontend).
|
// UI routes (HTMX-based management frontend).
|
||||||
uiSrv, err := ui.New(s.db, s.cfg, s.vault, s.logger)
|
uiSrv, err := ui.New(s.db, s.cfg, s.vault, s.logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/policy"
|
"git.wntrmute.dev/mc/mcias/internal/policy"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
// generateTOTPCode computes a valid RFC 6238 TOTP code for the current time
|
// generateTOTPCode computes a valid RFC 6238 TOTP code for the current time
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ package server
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/middleware"
|
"git.wntrmute.dev/mc/mcias/internal/middleware"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
// unsealRequest is the request body for POST /v1/vault/unseal.
|
// unsealRequest is the request body for POST /v1/vault/unseal.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHandleHealthSealed(t *testing.T) {
|
func TestHandleHealthSealed(t *testing.T) {
|
||||||
|
|||||||
91
internal/sso/session.go
Normal file
91
internal/sso/session.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package sso
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sessionTTL = 5 * time.Minute
|
||||||
|
sessionBytes = 16 // 128 bits of entropy for the nonce
|
||||||
|
)
|
||||||
|
|
||||||
|
// Session holds the SSO parameters between /sso/authorize and login completion.
|
||||||
|
// The nonce is embedded as a hidden form field in the login page and carried
|
||||||
|
// through the multi-step login flow (password → TOTP, or WebAuthn).
|
||||||
|
type Session struct { //nolint:govet // fieldalignment: field order matches logical grouping
|
||||||
|
ClientID string
|
||||||
|
RedirectURI string
|
||||||
|
State string
|
||||||
|
ExpiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// pendingSessions stores SSO sessions created at /sso/authorize.
|
||||||
|
var pendingSessions sync.Map //nolint:gochecknoglobals
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
go cleanupSessions()
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupSessions() {
|
||||||
|
ticker := time.NewTicker(cleanupPeriod)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
now := time.Now()
|
||||||
|
pendingSessions.Range(func(key, value any) bool {
|
||||||
|
s, ok := value.(*Session)
|
||||||
|
if !ok || now.After(s.ExpiresAt) {
|
||||||
|
pendingSessions.Delete(key)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreSession creates and stores a new SSO session, returning the hex-encoded
|
||||||
|
// nonce that should be embedded in the login form.
|
||||||
|
func StoreSession(clientID, redirectURI, state string) (string, error) {
|
||||||
|
raw, err := crypto.RandomBytes(sessionBytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("sso: generate session nonce: %w", err)
|
||||||
|
}
|
||||||
|
nonce := fmt.Sprintf("%x", raw)
|
||||||
|
pendingSessions.Store(nonce, &Session{
|
||||||
|
ClientID: clientID,
|
||||||
|
RedirectURI: redirectURI,
|
||||||
|
State: state,
|
||||||
|
ExpiresAt: time.Now().Add(sessionTTL),
|
||||||
|
})
|
||||||
|
return nonce, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConsumeSession retrieves and deletes an SSO session by nonce.
|
||||||
|
// Returns the Session and true if valid, or (nil, false) if unknown or expired.
|
||||||
|
func ConsumeSession(nonce string) (*Session, bool) {
|
||||||
|
v, ok := pendingSessions.LoadAndDelete(nonce)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
s, ok2 := v.(*Session)
|
||||||
|
if !ok2 || time.Now().After(s.ExpiresAt) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return s, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSession retrieves an SSO session without consuming it (for read-only checks
|
||||||
|
// during multi-step login). Returns nil if unknown or expired.
|
||||||
|
func GetSession(nonce string) *Session {
|
||||||
|
v, ok := pendingSessions.Load(nonce)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s, ok2 := v.(*Session)
|
||||||
|
if !ok2 || time.Now().After(s.ExpiresAt) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
93
internal/sso/store.go
Normal file
93
internal/sso/store.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// Package sso implements the authorization code store for the SSO redirect flow.
|
||||||
|
//
|
||||||
|
// MCIAS acts as the SSO provider: downstream services (MCR, MCAT, Metacrypt)
|
||||||
|
// redirect users to MCIAS for login, and MCIAS issues a short-lived, single-use
|
||||||
|
// authorization code that the service exchanges for a JWT token.
|
||||||
|
//
|
||||||
|
// Security design:
|
||||||
|
// - Authorization codes are 32 random bytes (256 bits), hex-encoded.
|
||||||
|
// - Codes are single-use: consumed via sync.Map LoadAndDelete on first exchange.
|
||||||
|
// - Codes expire after 60 seconds to limit the window for interception.
|
||||||
|
// - A background goroutine evicts expired codes every 5 minutes.
|
||||||
|
// - The code is bound to the client_id and redirect_uri presented at authorize
|
||||||
|
// time; the token exchange endpoint must verify both match.
|
||||||
|
package sso
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
codeTTL = 60 * time.Second
|
||||||
|
codeBytes = 32 // 256 bits of entropy
|
||||||
|
cleanupPeriod = 5 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthCode is a pending authorization code awaiting exchange for a JWT.
|
||||||
|
type AuthCode struct { //nolint:govet // fieldalignment: field order matches logical grouping
|
||||||
|
ClientID string
|
||||||
|
RedirectURI string
|
||||||
|
State string
|
||||||
|
AccountID int64
|
||||||
|
ExpiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// pendingCodes stores issued authorization codes awaiting exchange.
|
||||||
|
var pendingCodes sync.Map //nolint:gochecknoglobals
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
go cleanupCodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupCodes() {
|
||||||
|
ticker := time.NewTicker(cleanupPeriod)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
now := time.Now()
|
||||||
|
pendingCodes.Range(func(key, value any) bool {
|
||||||
|
ac, ok := value.(*AuthCode)
|
||||||
|
if !ok || now.After(ac.ExpiresAt) {
|
||||||
|
pendingCodes.Delete(key)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store creates and stores a new authorization code bound to the given
|
||||||
|
// client_id, redirect_uri, state, and account. Returns the hex-encoded code.
|
||||||
|
func Store(clientID, redirectURI, state string, accountID int64) (string, error) {
|
||||||
|
raw, err := crypto.RandomBytes(codeBytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("sso: generate authorization code: %w", err)
|
||||||
|
}
|
||||||
|
code := fmt.Sprintf("%x", raw)
|
||||||
|
pendingCodes.Store(code, &AuthCode{
|
||||||
|
ClientID: clientID,
|
||||||
|
RedirectURI: redirectURI,
|
||||||
|
State: state,
|
||||||
|
AccountID: accountID,
|
||||||
|
ExpiresAt: time.Now().Add(codeTTL),
|
||||||
|
})
|
||||||
|
return code, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume retrieves and deletes an authorization code. Returns the AuthCode
|
||||||
|
// and true if the code was valid and not expired, or (nil, false) otherwise.
|
||||||
|
//
|
||||||
|
// Security: LoadAndDelete ensures single-use; the code cannot be replayed.
|
||||||
|
func Consume(code string) (*AuthCode, bool) {
|
||||||
|
v, ok := pendingCodes.LoadAndDelete(code)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
ac, ok2 := v.(*AuthCode)
|
||||||
|
if !ok2 || time.Now().After(ac.ExpiresAt) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return ac, true
|
||||||
|
}
|
||||||
132
internal/sso/store_test.go
Normal file
132
internal/sso/store_test.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package sso
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStoreAndConsume(t *testing.T) {
|
||||||
|
code, err := Store("mcr", "https://mcr.example.com/cb", "state123", 42)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Store: %v", err)
|
||||||
|
}
|
||||||
|
if code == "" {
|
||||||
|
t.Fatal("Store returned empty code")
|
||||||
|
}
|
||||||
|
|
||||||
|
ac, ok := Consume(code)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Consume returned false for valid code")
|
||||||
|
}
|
||||||
|
if ac.ClientID != "mcr" {
|
||||||
|
t.Errorf("ClientID = %q, want %q", ac.ClientID, "mcr")
|
||||||
|
}
|
||||||
|
if ac.RedirectURI != "https://mcr.example.com/cb" {
|
||||||
|
t.Errorf("RedirectURI = %q", ac.RedirectURI)
|
||||||
|
}
|
||||||
|
if ac.State != "state123" {
|
||||||
|
t.Errorf("State = %q", ac.State)
|
||||||
|
}
|
||||||
|
if ac.AccountID != 42 {
|
||||||
|
t.Errorf("AccountID = %d, want 42", ac.AccountID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsumeSingleUse(t *testing.T) {
|
||||||
|
code, err := Store("mcr", "https://mcr.example.com/cb", "s", 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := Consume(code); !ok {
|
||||||
|
t.Fatal("first Consume should succeed")
|
||||||
|
}
|
||||||
|
if _, ok := Consume(code); ok {
|
||||||
|
t.Error("second Consume should fail (single-use)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsumeUnknownCode(t *testing.T) {
|
||||||
|
if _, ok := Consume("nonexistent"); ok {
|
||||||
|
t.Error("Consume should fail for unknown code")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsumeExpiredCode(t *testing.T) {
|
||||||
|
code, err := Store("mcr", "https://mcr.example.com/cb", "s", 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually expire the code.
|
||||||
|
v, loaded := pendingCodes.Load(code)
|
||||||
|
if !loaded {
|
||||||
|
t.Fatal("code not found in pendingCodes")
|
||||||
|
}
|
||||||
|
ac, ok := v.(*AuthCode)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("unexpected type in pendingCodes")
|
||||||
|
}
|
||||||
|
ac.ExpiresAt = time.Now().Add(-1 * time.Second)
|
||||||
|
|
||||||
|
if _, ok := Consume(code); ok {
|
||||||
|
t.Error("Consume should fail for expired code")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreSessionAndConsume(t *testing.T) {
|
||||||
|
nonce, err := StoreSession("mcr", "https://mcr.example.com/cb", "state456")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("StoreSession: %v", err)
|
||||||
|
}
|
||||||
|
if nonce == "" {
|
||||||
|
t.Fatal("StoreSession returned empty nonce")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSession should return it without consuming.
|
||||||
|
s := GetSession(nonce)
|
||||||
|
if s == nil {
|
||||||
|
t.Fatal("GetSession returned nil")
|
||||||
|
}
|
||||||
|
if s.ClientID != "mcr" {
|
||||||
|
t.Errorf("ClientID = %q", s.ClientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still available after GetSession.
|
||||||
|
s2, ok := ConsumeSession(nonce)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("ConsumeSession returned false")
|
||||||
|
}
|
||||||
|
if s2.State != "state456" {
|
||||||
|
t.Errorf("State = %q", s2.State)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consumed — should be gone.
|
||||||
|
if _, ok := ConsumeSession(nonce); ok {
|
||||||
|
t.Error("second ConsumeSession should fail")
|
||||||
|
}
|
||||||
|
if GetSession(nonce) != nil {
|
||||||
|
t.Error("GetSession should return nil after consume")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsumeSessionExpired(t *testing.T) {
|
||||||
|
nonce, err := StoreSession("mcr", "https://mcr.example.com/cb", "s")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("StoreSession: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
v, loaded := pendingSessions.Load(nonce)
|
||||||
|
if !loaded {
|
||||||
|
t.Fatal("session not found in pendingSessions")
|
||||||
|
}
|
||||||
|
sess, ok := v.(*Session)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("unexpected type in pendingSessions")
|
||||||
|
}
|
||||||
|
sess.ExpiresAt = time.Now().Add(-1 * time.Second)
|
||||||
|
|
||||||
|
if _, ok := ConsumeSession(nonce); ok {
|
||||||
|
t.Error("ConsumeSession should fail for expired session")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package ui
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
// uiContextKey is the unexported type for UI context values, preventing
|
// uiContextKey is the unexported type for UI context values, preventing
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CSRFManager implements HMAC-signed Double-Submit Cookie CSRF protection.
|
// CSRFManager implements HMAC-signed Double-Submit Cookie CSRF protection.
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/validate"
|
"git.wntrmute.dev/mc/mcias/internal/validate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// knownRoles lists the built-in roles shown as checkboxes in the roles editor.
|
// knownRoles lists the built-in roles shown as checkboxes in the roles editor.
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
const auditPageSize = 50
|
const auditPageSize = 50
|
||||||
|
|||||||
@@ -3,18 +3,19 @@ package ui
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/validate"
|
"git.wntrmute.dev/mc/mcias/internal/validate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// handleLoginPage renders the login form.
|
// handleLoginPage renders the login form.
|
||||||
func (u *UIServer) handleLoginPage(w http.ResponseWriter, r *http.Request) {
|
func (u *UIServer) handleLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||||
u.render(w, "login", LoginData{
|
u.render(w, "login", LoginData{
|
||||||
WebAuthnEnabled: u.cfg.WebAuthnEnabled(),
|
WebAuthnEnabled: u.cfg.WebAuthnEnabled(),
|
||||||
|
SSONonce: r.URL.Query().Get("sso"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +98,8 @@ func (u *UIServer) handleLoginPost(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ssoNonce := r.FormValue("sso_nonce")
|
||||||
|
|
||||||
// TOTP required: issue a server-side nonce and show the TOTP step form.
|
// TOTP required: issue a server-side nonce and show the TOTP step form.
|
||||||
// Security: the nonce replaces the password hidden field (F-02). The password
|
// Security: the nonce replaces the password hidden field (F-02). The password
|
||||||
// is not stored anywhere after this point; only the account ID is retained.
|
// is not stored anywhere after this point; only the account ID is retained.
|
||||||
@@ -110,11 +113,12 @@ func (u *UIServer) handleLoginPost(w http.ResponseWriter, r *http.Request) {
|
|||||||
u.render(w, "totp_step", LoginData{
|
u.render(w, "totp_step", LoginData{
|
||||||
Username: username,
|
Username: username,
|
||||||
Nonce: nonce,
|
Nonce: nonce,
|
||||||
|
SSONonce: ssoNonce,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
u.finishLogin(w, r, acct)
|
u.finishLogin(w, r, acct, ssoNonce)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleTOTPStep handles the second POST when totp_step=1 is set.
|
// handleTOTPStep handles the second POST when totp_step=1 is set.
|
||||||
@@ -129,6 +133,7 @@ func (u *UIServer) handleTOTPStep(w http.ResponseWriter, r *http.Request) {
|
|||||||
username := r.FormValue("username") //nolint:gosec // body already limited by caller
|
username := r.FormValue("username") //nolint:gosec // body already limited by caller
|
||||||
nonce := r.FormValue("totp_nonce") //nolint:gosec // body already limited by caller
|
nonce := r.FormValue("totp_nonce") //nolint:gosec // body already limited by caller
|
||||||
totpCode := r.FormValue("totp_code") //nolint:gosec // body already limited by caller
|
totpCode := r.FormValue("totp_code") //nolint:gosec // body already limited by caller
|
||||||
|
ssoNonce := r.FormValue("sso_nonce") //nolint:gosec // body already limited by caller
|
||||||
|
|
||||||
// Security: consume the nonce (single-use); reject if unknown or expired.
|
// Security: consume the nonce (single-use); reject if unknown or expired.
|
||||||
accountID, ok := u.consumeTOTPNonce(nonce)
|
accountID, ok := u.consumeTOTPNonce(nonce)
|
||||||
@@ -172,6 +177,7 @@ func (u *UIServer) handleTOTPStep(w http.ResponseWriter, r *http.Request) {
|
|||||||
Error: "invalid TOTP code",
|
Error: "invalid TOTP code",
|
||||||
Username: username,
|
Username: username,
|
||||||
Nonce: newNonce,
|
Nonce: newNonce,
|
||||||
|
SSONonce: ssoNonce,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -189,15 +195,36 @@ func (u *UIServer) handleTOTPStep(w http.ResponseWriter, r *http.Request) {
|
|||||||
Error: "invalid TOTP code",
|
Error: "invalid TOTP code",
|
||||||
Username: username,
|
Username: username,
|
||||||
Nonce: newNonce,
|
Nonce: newNonce,
|
||||||
|
SSONonce: ssoNonce,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
u.finishLogin(w, r, acct)
|
u.finishLogin(w, r, acct, ssoNonce)
|
||||||
}
|
}
|
||||||
|
|
||||||
// finishLogin issues a JWT, sets the session cookie, and redirects to dashboard.
|
// finishLogin issues a JWT, sets the session cookie, and redirects to dashboard.
|
||||||
func (u *UIServer) finishLogin(w http.ResponseWriter, r *http.Request, acct *model.Account) {
|
// When ssoNonce is non-empty, the login is part of an SSO redirect flow: instead
|
||||||
|
// of setting a session cookie, an authorization code is issued and the user is
|
||||||
|
// redirected back to the service's callback URL.
|
||||||
|
func (u *UIServer) finishLogin(w http.ResponseWriter, r *http.Request, acct *model.Account, ssoNonce string) {
|
||||||
|
// SSO redirect flow: issue authorization code and redirect to service.
|
||||||
|
if ssoNonce != "" {
|
||||||
|
if callbackURL, ok := u.buildSSOCallback(r, ssoNonce, acct.ID); ok {
|
||||||
|
// Security: htmx follows 302 redirects via fetch, which fails
|
||||||
|
// cross-origin (no CORS on the service callback). Use HX-Redirect
|
||||||
|
// so htmx performs a full page navigation instead.
|
||||||
|
if isHTMX(r) {
|
||||||
|
w.Header().Set("HX-Redirect", callbackURL)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, callbackURL, http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// SSO session expired/consumed — fall through to normal login.
|
||||||
|
}
|
||||||
|
|
||||||
// Determine token expiry based on admin role.
|
// Determine token expiry based on admin role.
|
||||||
expiry := u.cfg.DefaultExpiry()
|
expiry := u.cfg.DefaultExpiry()
|
||||||
roles, err := u.db.GetRoles(acct.ID)
|
roles, err := u.db.GetRoles(acct.ID)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package ui
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// handleDashboard renders the main dashboard page. Admin users see account
|
// handleDashboard renders the main dashboard page. Admin users see account
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/policy"
|
"git.wntrmute.dev/mc/mcias/internal/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---- Policies page ----
|
// ---- Policies page ----
|
||||||
|
|||||||
90
internal/ui/handlers_sso.go
Normal file
90
internal/ui/handlers_sso.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/sso"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleSSOAuthorize validates the SSO request parameters against registered
|
||||||
|
// clients, creates an SSO session, and redirects to /login with the SSO nonce.
|
||||||
|
//
|
||||||
|
// Security: the client_id and redirect_uri are validated against the MCIAS
|
||||||
|
// config (exact match). The state parameter is opaque and carried through
|
||||||
|
// unchanged. An SSO session is created server-side so the nonce is the only
|
||||||
|
// value embedded in the login form.
|
||||||
|
func (u *UIServer) handleSSOAuthorize(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clientID := r.URL.Query().Get("client_id")
|
||||||
|
redirectURI := r.URL.Query().Get("redirect_uri")
|
||||||
|
state := r.URL.Query().Get("state")
|
||||||
|
|
||||||
|
if clientID == "" || redirectURI == "" || state == "" {
|
||||||
|
http.Error(w, "missing required parameters: client_id, redirect_uri, state", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security: validate client_id against registered SSO clients in the database.
|
||||||
|
client, err := u.db.GetSSOClient(clientID)
|
||||||
|
if err != nil {
|
||||||
|
u.logger.Warn("sso: unknown client_id", "client_id", clientID)
|
||||||
|
http.Error(w, "unknown client_id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !client.Enabled {
|
||||||
|
u.logger.Warn("sso: disabled client", "client_id", clientID)
|
||||||
|
http.Error(w, "SSO client is disabled", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security: redirect_uri must exactly match the registered URI to prevent
|
||||||
|
// open-redirect attacks.
|
||||||
|
if redirectURI != client.RedirectURI {
|
||||||
|
u.logger.Warn("sso: redirect_uri mismatch",
|
||||||
|
"client_id", clientID,
|
||||||
|
"expected", client.RedirectURI,
|
||||||
|
"got", redirectURI)
|
||||||
|
http.Error(w, "redirect_uri does not match registered URI", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, err := sso.StoreSession(clientID, redirectURI, state)
|
||||||
|
if err != nil {
|
||||||
|
u.logger.Error("sso: store session", "error", err)
|
||||||
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u.writeAudit(r, model.EventSSOAuthorize, nil, nil,
|
||||||
|
audit.JSON("client_id", clientID))
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/login?sso="+url.QueryEscape(nonce), http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSSOCallback consumes the SSO session, generates an authorization code,
|
||||||
|
// and returns the callback URL with code and state parameters. Returns ("", false)
|
||||||
|
// if the SSO session is expired or already consumed.
|
||||||
|
//
|
||||||
|
// Security: the SSO session is consumed (single-use) and the authorization code
|
||||||
|
// is stored server-side for exchange via POST /v1/sso/token. The state parameter
|
||||||
|
// is carried through unchanged for the service to validate.
|
||||||
|
func (u *UIServer) buildSSOCallback(r *http.Request, ssoNonce string, accountID int64) (string, bool) {
|
||||||
|
sess, ok := sso.ConsumeSession(ssoNonce)
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
code, err := sso.Store(sess.ClientID, sess.RedirectURI, sess.State, accountID)
|
||||||
|
if err != nil {
|
||||||
|
u.logger.Error("sso: store auth code", "error", err)
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
u.writeAudit(r, model.EventSSOLoginOK, &accountID, nil,
|
||||||
|
audit.JSON("client_id", sess.ClientID))
|
||||||
|
|
||||||
|
return sess.RedirectURI + "?code=" + url.QueryEscape(code) + "&state=" + url.QueryEscape(sess.State), true
|
||||||
|
}
|
||||||
129
internal/ui/handlers_sso_clients.go
Normal file
129
internal/ui/handlers_sso_clients.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (u *UIServer) handleSSOClientsPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
csrfToken, err := u.setCSRFCookies(w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clients, err := u.db.ListSSOClients()
|
||||||
|
if err != nil {
|
||||||
|
u.renderError(w, r, http.StatusInternalServerError, "failed to load SSO clients")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u.render(w, "sso_clients", SSOClientsData{
|
||||||
|
PageData: PageData{CSRFToken: csrfToken, ActorName: u.actorName(r), IsAdmin: isAdmin(r)},
|
||||||
|
Clients: clients,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UIServer) handleCreateSSOClientUI(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes)
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
u.renderError(w, r, http.StatusBadRequest, "invalid form submission")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientID := strings.TrimSpace(r.FormValue("client_id"))
|
||||||
|
redirectURI := strings.TrimSpace(r.FormValue("redirect_uri"))
|
||||||
|
tagsStr := strings.TrimSpace(r.FormValue("tags"))
|
||||||
|
|
||||||
|
if clientID == "" || redirectURI == "" {
|
||||||
|
u.renderError(w, r, http.StatusBadRequest, "client_id and redirect_uri are required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var tags []string
|
||||||
|
if tagsStr != "" {
|
||||||
|
for _, t := range strings.Split(tagsStr, ",") {
|
||||||
|
t = strings.TrimSpace(t)
|
||||||
|
if t != "" {
|
||||||
|
tags = append(tags, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := claimsFromContext(r.Context())
|
||||||
|
var actorID *int64
|
||||||
|
if claims != nil {
|
||||||
|
if acct, err := u.db.GetAccountByUUID(claims.Subject); err == nil {
|
||||||
|
actorID = &acct.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := u.db.CreateSSOClient(clientID, redirectURI, tags, actorID)
|
||||||
|
if err != nil {
|
||||||
|
u.renderError(w, r, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u.writeAudit(r, model.EventSSOClientCreated, actorID, nil,
|
||||||
|
audit.JSON("client_id", c.ClientID))
|
||||||
|
|
||||||
|
u.render(w, "sso_client_row", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UIServer) handleToggleSSOClient(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clientID := r.PathValue("clientId")
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes)
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
u.renderError(w, r, http.StatusBadRequest, "invalid form")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled := r.FormValue("enabled") == "true"
|
||||||
|
if err := u.db.UpdateSSOClient(clientID, nil, nil, &enabled); err != nil {
|
||||||
|
u.renderError(w, r, http.StatusInternalServerError, "failed to update SSO client")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := claimsFromContext(r.Context())
|
||||||
|
var actorID *int64
|
||||||
|
if claims != nil {
|
||||||
|
if acct, err := u.db.GetAccountByUUID(claims.Subject); err == nil {
|
||||||
|
actorID = &acct.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u.writeAudit(r, model.EventSSOClientUpdated, actorID, nil,
|
||||||
|
fmt.Sprintf(`{"client_id":%q,"enabled":%v}`, clientID, enabled))
|
||||||
|
|
||||||
|
c, err := u.db.GetSSOClient(clientID)
|
||||||
|
if err != nil {
|
||||||
|
u.renderError(w, r, http.StatusInternalServerError, "failed to reload SSO client")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.render(w, "sso_client_row", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UIServer) handleDeleteSSOClientUI(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clientID := r.PathValue("clientId")
|
||||||
|
|
||||||
|
if err := u.db.DeleteSSOClient(clientID); err != nil {
|
||||||
|
u.renderError(w, r, http.StatusInternalServerError, "failed to delete SSO client")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := claimsFromContext(r.Context())
|
||||||
|
var actorID *int64
|
||||||
|
if claims != nil {
|
||||||
|
if acct, err := u.db.GetAccountByUUID(claims.Subject); err == nil {
|
||||||
|
actorID = &acct.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u.writeAudit(r, model.EventSSOClientDeleted, actorID, nil,
|
||||||
|
audit.JSON("client_id", clientID))
|
||||||
|
|
||||||
|
// Return empty response so HTMX removes the row.
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
|
|
||||||
qrcode "github.com/skip2/go-qrcode"
|
qrcode "github.com/skip2/go-qrcode"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// handleTOTPEnrollStart processes the password re-auth step and generates
|
// handleTOTPEnrollStart processes the password re-auth step and generates
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ package ui
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/middleware"
|
"git.wntrmute.dev/mc/mcias/internal/middleware"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UnsealData is the view model for the unseal page.
|
// UnsealData is the view model for the unseal page.
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import (
|
|||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
libwebauthn "github.com/go-webauthn/webauthn/webauthn"
|
libwebauthn "github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
"git.wntrmute.dev/mc/mcias/internal/audit"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
mciaswebauthn "git.wntrmute.dev/kyle/mcias/internal/webauthn"
|
mciaswebauthn "git.wntrmute.dev/mc/mcias/internal/webauthn"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -27,10 +27,11 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// webauthnCeremony holds a pending WebAuthn ceremony.
|
// webauthnCeremony holds a pending WebAuthn ceremony.
|
||||||
type webauthnCeremony struct {
|
type webauthnCeremony struct { //nolint:govet // fieldalignment: field order matches logical grouping
|
||||||
expiresAt time.Time
|
expiresAt time.Time
|
||||||
session *libwebauthn.SessionData
|
session *libwebauthn.SessionData
|
||||||
accountID int64
|
accountID int64
|
||||||
|
ssoNonce string // non-empty when login is part of an SSO redirect flow
|
||||||
}
|
}
|
||||||
|
|
||||||
// pendingWebAuthnCeremonies stores in-flight WebAuthn ceremonies for the UI.
|
// pendingWebAuthnCeremonies stores in-flight WebAuthn ceremonies for the UI.
|
||||||
@@ -55,7 +56,7 @@ func cleanupUIWebAuthnCeremonies() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func storeUICeremony(session *libwebauthn.SessionData, accountID int64) (string, error) {
|
func storeUICeremony(session *libwebauthn.SessionData, accountID int64, ssoNonce string) (string, error) {
|
||||||
raw, err := crypto.RandomBytes(webauthnNonceBytes)
|
raw, err := crypto.RandomBytes(webauthnNonceBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("webauthn: generate ceremony nonce: %w", err)
|
return "", fmt.Errorf("webauthn: generate ceremony nonce: %w", err)
|
||||||
@@ -64,6 +65,7 @@ func storeUICeremony(session *libwebauthn.SessionData, accountID int64) (string,
|
|||||||
pendingUIWebAuthnCeremonies.Store(nonce, &webauthnCeremony{
|
pendingUIWebAuthnCeremonies.Store(nonce, &webauthnCeremony{
|
||||||
session: session,
|
session: session,
|
||||||
accountID: accountID,
|
accountID: accountID,
|
||||||
|
ssoNonce: ssoNonce,
|
||||||
expiresAt: time.Now().Add(webauthnCeremonyTTL),
|
expiresAt: time.Now().Add(webauthnCeremonyTTL),
|
||||||
})
|
})
|
||||||
return nonce, nil
|
return nonce, nil
|
||||||
@@ -170,7 +172,7 @@ func (u *UIServer) handleWebAuthnBegin(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nonce, err := storeUICeremony(session, acct.ID)
|
nonce, err := storeUICeremony(session, acct.ID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSONError(w, http.StatusInternalServerError, "internal error")
|
writeJSONError(w, http.StatusInternalServerError, "internal error")
|
||||||
return
|
return
|
||||||
@@ -352,6 +354,7 @@ func (u *UIServer) handleWebAuthnLoginBegin(w http.ResponseWriter, r *http.Reque
|
|||||||
r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes)
|
r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes)
|
||||||
var req struct {
|
var req struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
|
SSONonce string `json:"sso_nonce"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
writeJSONError(w, http.StatusBadRequest, "invalid JSON")
|
writeJSONError(w, http.StatusBadRequest, "invalid JSON")
|
||||||
@@ -413,7 +416,7 @@ func (u *UIServer) handleWebAuthnLoginBegin(w http.ResponseWriter, r *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nonce, err := storeUICeremony(session, accountID)
|
nonce, err := storeUICeremony(session, accountID, req.SSONonce)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSONError(w, http.StatusInternalServerError, "internal error")
|
writeJSONError(w, http.StatusInternalServerError, "internal error")
|
||||||
return
|
return
|
||||||
@@ -582,6 +585,17 @@ func (u *UIServer) handleWebAuthnLoginFinish(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
_ = u.db.ClearLoginFailures(acct.ID)
|
_ = u.db.ClearLoginFailures(acct.ID)
|
||||||
|
|
||||||
|
// SSO redirect flow: issue authorization code and return redirect URL as JSON.
|
||||||
|
if ceremony.ssoNonce != "" {
|
||||||
|
if callbackURL, ok := u.buildSSOCallback(r, ceremony.ssoNonce, acct.ID); ok {
|
||||||
|
u.writeAudit(r, model.EventWebAuthnLoginOK, &acct.ID, nil, "")
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]string{"redirect": callbackURL})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// SSO session expired — fall through to normal login.
|
||||||
|
}
|
||||||
|
|
||||||
// Issue JWT and set session cookie.
|
// Issue JWT and set session cookie.
|
||||||
expiry := u.cfg.DefaultExpiry()
|
expiry := u.cfg.DefaultExpiry()
|
||||||
roles, err := u.db.GetRoles(acct.ID)
|
roles, err := u.db.GetRoles(acct.ID)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/token"
|
"git.wntrmute.dev/mc/mcias/internal/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
// validateSessionToken wraps token.ValidateToken for use by UI session middleware.
|
// validateSessionToken wraps token.ValidateToken for use by UI session middleware.
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/middleware"
|
"git.wntrmute.dev/mc/mcias/internal/middleware"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
"git.wntrmute.dev/kyle/mcias/web"
|
"git.wntrmute.dev/mc/mcias/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -267,6 +267,7 @@ func New(database *db.DB, cfg *config.Config, v *vault.Vault, logger *slog.Logge
|
|||||||
"templates/fragments/webauthn_enroll.html",
|
"templates/fragments/webauthn_enroll.html",
|
||||||
"templates/fragments/totp_section.html",
|
"templates/fragments/totp_section.html",
|
||||||
"templates/fragments/totp_enroll_qr.html",
|
"templates/fragments/totp_enroll_qr.html",
|
||||||
|
"templates/fragments/sso_client_row.html",
|
||||||
}
|
}
|
||||||
base, err := template.New("").Funcs(funcMap).ParseFS(web.TemplateFS, sharedFiles...)
|
base, err := template.New("").Funcs(funcMap).ParseFS(web.TemplateFS, sharedFiles...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -287,6 +288,7 @@ func New(database *db.DB, cfg *config.Config, v *vault.Vault, logger *slog.Logge
|
|||||||
"profile": "templates/profile.html",
|
"profile": "templates/profile.html",
|
||||||
"unseal": "templates/unseal.html",
|
"unseal": "templates/unseal.html",
|
||||||
"service_accounts": "templates/service_accounts.html",
|
"service_accounts": "templates/service_accounts.html",
|
||||||
|
"sso_clients": "templates/sso_clients.html",
|
||||||
}
|
}
|
||||||
tmpls := make(map[string]*template.Template, len(pageFiles))
|
tmpls := make(map[string]*template.Template, len(pageFiles))
|
||||||
for name, file := range pageFiles {
|
for name, file := range pageFiles {
|
||||||
@@ -445,6 +447,9 @@ func (u *UIServer) Register(mux *http.ServeMux) {
|
|||||||
uiMux.HandleFunc("GET /unseal", u.handleUnsealPage)
|
uiMux.HandleFunc("GET /unseal", u.handleUnsealPage)
|
||||||
uiMux.Handle("POST /unseal", unsealRateLimit(http.HandlerFunc(u.handleUnsealPost)))
|
uiMux.Handle("POST /unseal", unsealRateLimit(http.HandlerFunc(u.handleUnsealPost)))
|
||||||
|
|
||||||
|
// SSO authorize route (no session required, rate-limited).
|
||||||
|
uiMux.Handle("GET /sso/authorize", loginRateLimit(http.HandlerFunc(u.handleSSOAuthorize)))
|
||||||
|
|
||||||
// Auth routes (no session required).
|
// Auth routes (no session required).
|
||||||
uiMux.HandleFunc("GET /login", u.handleLoginPage)
|
uiMux.HandleFunc("GET /login", u.handleLoginPage)
|
||||||
uiMux.Handle("POST /login", loginRateLimit(http.HandlerFunc(u.handleLoginPost)))
|
uiMux.Handle("POST /login", loginRateLimit(http.HandlerFunc(u.handleLoginPost)))
|
||||||
@@ -498,6 +503,10 @@ func (u *UIServer) Register(mux *http.ServeMux) {
|
|||||||
uiMux.Handle("DELETE /policies/{id}", admin(u.handleDeletePolicyRule))
|
uiMux.Handle("DELETE /policies/{id}", admin(u.handleDeletePolicyRule))
|
||||||
uiMux.Handle("PUT /accounts/{id}/tags", admin(u.handleSetAccountTags))
|
uiMux.Handle("PUT /accounts/{id}/tags", admin(u.handleSetAccountTags))
|
||||||
uiMux.Handle("PUT /accounts/{id}/password", admin(u.handleAdminResetPassword))
|
uiMux.Handle("PUT /accounts/{id}/password", admin(u.handleAdminResetPassword))
|
||||||
|
uiMux.Handle("GET /sso-clients", adminGet(u.handleSSOClientsPage))
|
||||||
|
uiMux.Handle("POST /sso-clients", admin(u.handleCreateSSOClientUI))
|
||||||
|
uiMux.Handle("PATCH /sso-clients/{clientId}/toggle", admin(u.handleToggleSSOClient))
|
||||||
|
uiMux.Handle("DELETE /sso-clients/{clientId}", admin(u.handleDeleteSSOClientUI))
|
||||||
|
|
||||||
// Service accounts page — accessible to any authenticated user; shows only
|
// Service accounts page — accessible to any authenticated user; shows only
|
||||||
// the service accounts for which the current user is a token-issue delegate.
|
// the service accounts for which the current user is a token-issue delegate.
|
||||||
@@ -746,8 +755,11 @@ func noDirListing(next http.Handler) http.Handler {
|
|||||||
func securityHeaders(next http.Handler) http.Handler {
|
func securityHeaders(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
h := w.Header()
|
h := w.Header()
|
||||||
|
// Security: 'unsafe-hashes' with the htmx swap indicator style hash
|
||||||
|
// allows htmx to apply its settling/swapping CSS transitions without
|
||||||
|
// opening the door to arbitrary inline styles.
|
||||||
h.Set("Content-Security-Policy",
|
h.Set("Content-Security-Policy",
|
||||||
"default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'")
|
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-hashes' 'sha256-bsV5JivYxvGywDAZ22EZJKBFip65Ng9xoJVLbBg7bdo='; img-src 'self' data:; font-src 'self'")
|
||||||
h.Set("X-Content-Type-Options", "nosniff")
|
h.Set("X-Content-Type-Options", "nosniff")
|
||||||
h.Set("X-Frame-Options", "DENY")
|
h.Set("X-Frame-Options", "DENY")
|
||||||
h.Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
|
h.Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
|
||||||
@@ -810,6 +822,7 @@ type PageData struct {
|
|||||||
type LoginData struct {
|
type LoginData struct {
|
||||||
Error string
|
Error string
|
||||||
Username string // pre-filled on TOTP step
|
Username string // pre-filled on TOTP step
|
||||||
|
SSONonce string // SSO session nonce (hidden field for SSO redirect flow)
|
||||||
// Security (F-02): Password is no longer carried in the HTML form. Instead
|
// Security (F-02): Password is no longer carried in the HTML form. Instead
|
||||||
// a short-lived server-side nonce is issued after successful password
|
// a short-lived server-side nonce is issued after successful password
|
||||||
// verification, and only the nonce is embedded in the TOTP step form.
|
// verification, and only the nonce is embedded in the TOTP step form.
|
||||||
@@ -916,6 +929,12 @@ type PolicyRuleView struct {
|
|||||||
IsPending bool // true if not_before is in the future
|
IsPending bool // true if not_before is in the future
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSOClientsData is the view model for the SSO clients admin page.
|
||||||
|
type SSOClientsData struct {
|
||||||
|
PageData
|
||||||
|
Clients []*model.SSOClient
|
||||||
|
}
|
||||||
|
|
||||||
// PoliciesData is the view model for the policies list page.
|
// PoliciesData is the view model for the policies list page.
|
||||||
type PoliciesData struct {
|
type PoliciesData struct {
|
||||||
PageData
|
PageData
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/vault"
|
"git.wntrmute.dev/mc/mcias/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testIssuer = "https://auth.example.com"
|
const testIssuer = "https://auth.example.com"
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/db"
|
"git.wntrmute.dev/mc/mcias/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DeriveFromPassphrase derives the master encryption key from a passphrase
|
// DeriveFromPassphrase derives the master encryption key from a passphrase
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewWebAuthn creates a configured go-webauthn instance from MCIAS config.
|
// NewWebAuthn creates a configured go-webauthn instance from MCIAS config.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
libwebauthn "github.com/go-webauthn/webauthn/webauthn"
|
libwebauthn "github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/config"
|
"git.wntrmute.dev/mc/mcias/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewWebAuthn(t *testing.T) {
|
func TestNewWebAuthn(t *testing.T) {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DecryptCredential decrypts a stored WebAuthn credential's ID and public key
|
// DecryptCredential decrypts a stored WebAuthn credential's ID and public key
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"github.com/go-webauthn/webauthn/protocol"
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
libwebauthn "github.com/go-webauthn/webauthn/webauthn"
|
libwebauthn "github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
"git.wntrmute.dev/mc/mcias/internal/crypto"
|
||||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testMasterKey(t *testing.T) []byte {
|
func testMasterKey(t *testing.T) []byte {
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
//
|
//
|
||||||
// Prerequisites: protoc, protoc-gen-go, protoc-gen-go-grpc must be in PATH.
|
// Prerequisites: protoc, protoc-gen-go, protoc-gen-go-grpc must be in PATH.
|
||||||
//
|
//
|
||||||
//go:generate protoc --proto_path=../proto --go_out=../gen --go_opt=paths=source_relative --go-grpc_out=../gen --go-grpc_opt=paths=source_relative mcias/v1/common.proto mcias/v1/admin.proto mcias/v1/auth.proto mcias/v1/token.proto mcias/v1/account.proto mcias/v1/policy.proto
|
//go:generate protoc --proto_path=../proto --go_out=../gen --go_opt=paths=source_relative --go-grpc_out=../gen --go-grpc_opt=paths=source_relative mcias/v1/common.proto mcias/v1/admin.proto mcias/v1/auth.proto mcias/v1/token.proto mcias/v1/account.proto mcias/v1/policy.proto mcias/v1/sso_client.proto
|
||||||
package proto
|
package proto
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ syntax = "proto3";
|
|||||||
|
|
||||||
package mcias.v1;
|
package mcias.v1;
|
||||||
|
|
||||||
option go_package = "git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1";
|
option go_package = "git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1";
|
||||||
|
|
||||||
import "mcias/v1/common.proto";
|
import "mcias/v1/common.proto";
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user