diff --git a/.env.example b/.env.example index e230aefc..2129a746 100644 --- a/.env.example +++ b/.env.example @@ -21,3 +21,24 @@ SLACK_EVENTS_CHANNEL= # Homer Dashboard Integration (optional) # URL of your Homer dashboard to import links from HOMER_URL= + +# Authentication (see docs/AUTHENTICATION.md) +# Comma separated permissions granted to anonymous callers. +# Leave the line commented out to keep the transitional default: every +# permission except access:manage. Setting it wins even when the value is +# empty, and an empty value means no anonymous access at all, which locks the +# UI out until the login screen ships. Uncomment it to pick a narrower set. +# AUTH_ANONYMOUS_PERMISSIONS=event:read,catalog:read,lock:read,links:read +# Password of the initial "admin" account, used only when the user collection is empty. +# Unset: a random password is generated and printed once in the server logs. +AUTH_ADMIN_PASSWORD= +# Base64 encoded secret (32 bytes or more) signing session cookies. +# Unset: generated once and persisted in MongoDB. +AUTH_SESSION_SECRET= +AUTH_SESSION_TTL=12h +# Public URL of the UI, used to decide whether cookies are Secure. +AUTH_PUBLIC_URL= +AUTH_COOKIE_SECURE=false +# Trust the last X-Forwarded-For entry for login rate limiting, only behind a +# reverse proxy you control that appends the peer address to the header. +AUTH_TRUST_PROXY=false diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index 7617eebc..96867217 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -11,6 +11,8 @@ on: - go.mod - go.sum - main.go + - server/** + - proto/** jobs: linting: @@ -42,11 +44,21 @@ jobs: - name: Run Gosec Security Scanner uses: securego/gosec@master with: - args: -exclude=G103,G115 ./... + args: -exclude=G103,G115 -exclude-generated ./... test: name: Go test runs-on: ubuntu-latest + services: + mongo: + image: mongo:7 + ports: + - 27017:27017 + options: >- + --health-cmd "mongosh --quiet --eval 'db.runCommand({ ping: 1 })'" + --health-interval 5s + --health-timeout 5s + --health-retries 10 steps: - name: Check out code into the Go module directory @@ -61,4 +73,6 @@ jobs: run: go mod download -x - name: Test + env: + MONGO_TEST_URI: mongodb://127.0.0.1:27017 run: go test -v ./... diff --git a/README.md b/README.md index e23370cd..1557927b 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,7 @@ npm run dev - [🚀 Installation Guide](./docs/INSTALLATION.md) - Complete installation instructions - [⚙️ Configuration Guide](./docs/CONFIGURATION.md) - Environment variables and settings - [🔧 Development Guide](./docs/DEVELOPMENT.md) - Set up development environment +- [🔐 Authentication](./docs/AUTHENTICATION.md) - Users, teams, permissions and API keys ### User Guides - [📖 User Guide](./docs/USER_GUIDE.md) - How to use Tracker diff --git a/cmd/serv.go b/cmd/serv.go index 6283c76b..95ab51d6 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -16,9 +16,13 @@ import ( "syscall" "time" + authv1 "github.com/bananaops/tracker/generated/proto/auth/v1alpha1" catalog "github.com/bananaops/tracker/generated/proto/catalog/v1alpha1" event "github.com/bananaops/tracker/generated/proto/event/v1alpha1" lock "github.com/bananaops/tracker/generated/proto/lock/v1alpha1" + "github.com/bananaops/tracker/internal/auth" + "github.com/bananaops/tracker/internal/auth/identity" + store "github.com/bananaops/tracker/internal/stores" "github.com/bananaops/tracker/server" "github.com/go-openapi/runtime/middleware" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" @@ -34,9 +38,60 @@ var serv = &cobra.Command{ Short: "Run tracker server", Run: func(cmd *cobra.Command, args []string) { + ctx := context.TODO() + + // Authentication: configuration, stores, bootstrap and principal resolver. + authCfg, err := auth.LoadConfig(os.LookupEnv) + if err != nil { + log.Fatalf("invalid authentication configuration: %v", err) + } + if authCfg.AnonymousDefaulted { + slog.Warn("AUTH_ANONYMOUS_PERMISSIONS is not set: anonymous callers keep every permission except access:manage. This default becomes empty in the next major release.") + } + userStore := store.NewAuthUserStore() + teamStore := store.NewAuthTeamStore() + keyStore := store.NewAuthAPIKeyStore() + settingsStore := store.NewAuthSettingsStore() + + sessionSecret := authCfg.SessionSecret + if sessionSecret == nil { + sessionSecret, err = settingsStore.SessionSecret(ctx) + if err != nil { + log.Fatalf("cannot load session secret: %v", err) + } + slog.Info("AUTH_SESSION_SECRET is not set, using the secret persisted in MongoDB") + } + sessions, err := auth.NewSessionManager(sessionSecret, authCfg.SessionTTL) + if err != nil { + log.Fatalf("cannot create session manager: %v", err) + } + + bootstrap, err := identity.Bootstrap(ctx, userStore, teamStore, authCfg.AdminPassword) + if err != nil { + log.Fatalf("authentication bootstrap failed: %v", err) + } + if bootstrap.AdminCreated { + if bootstrap.GeneratedPassword != "" { + slog.Warn("Initial admin account created with a generated password. Change it at first login.", "username", "admin", "password", bootstrap.GeneratedPassword) + } else { + slog.Info("Initial admin account created from AUTH_ADMIN_PASSWORD", "username", "admin") + } + } + + resolver := &identity.Resolver{ + Users: userStore, + Teams: teamStore, + Keys: keyStore, + Sessions: sessions, + AnonymousPermissions: authCfg.AnonymousPermissions, + } + // Set up gRPC server grpcServerEndpoint := "localhost:8765" - grpcServer := grpc.NewServer() + grpcServer := grpc.NewServer( + grpc.ChainUnaryInterceptor(auth.UnaryInterceptor(resolver)), + grpc.ChainStreamInterceptor(auth.StreamInterceptor(resolver)), + ) // register reflection API https://github.com/grpc/grpc/blob/master/doc/server-reflection.md reflection.Register(grpcServer) @@ -53,12 +108,14 @@ var serv = &cobra.Command{ catalogs := server.NewCatalog() catalog.RegisterCatalogServiceServer(grpcServer, catalogs) + // register auth service + authService := server.NewAuth(userStore, teamStore, keyStore, authCfg) + authv1.RegisterAuthServiceServer(grpcServer, authService) + // register health checK service //healthCheckService := &server.HealthCheckService{} //health.RegisterHealthServer(grpcServer, healthCheckService) - ctx := context.TODO() - // Initialiser les index MongoDB après la première connexion db := server.GetDatabaseConnection() if db != nil { @@ -69,7 +126,7 @@ var serv = &cobra.Command{ mux := runtime.NewServeMux() // Register generated routes to mux - err := event.RegisterEventServiceHandlerServer(ctx, mux, events) + err = event.RegisterEventServiceHandlerServer(ctx, mux, events) if err != nil { panic(err) } @@ -84,6 +141,14 @@ var serv = &cobra.Command{ panic(err) } + err = authv1.RegisterAuthServiceHandlerServer(ctx, mux, authService) + if err != nil { + panic(err) + } + + // Cookie based auth endpoints (login, logout, password change) + server.NewAuthHTTP(userStore, sessions, authCfg).Register(mux) + // Register Homer proxy endpoint server.RegisterHomerHandler(mux, os.Getenv("HOMER_URL")) @@ -228,6 +293,9 @@ var serv = &cobra.Command{ httpHandler = mux } + // Resolve the principal of every HTTP request (SPA, API and custom handlers) + httpHandler = auth.HTTPMiddleware(resolver, authCfg)(httpHandler) + httpServer := &http.Server{ Addr: "0.0.0.0:8080", ReadHeaderTimeout: 2 * time.Second, // Fix CWE-400 Potential Slowloris Attack because ReadHeaderTimeout is not configured in the http.Server diff --git a/docs/AUTHENTICATION.md b/docs/AUTHENTICATION.md new file mode 100644 index 00000000..19315127 --- /dev/null +++ b/docs/AUTHENTICATION.md @@ -0,0 +1,192 @@ +# Authentication and Access Control + +Tracker authenticates every request and authorizes it against a small set of +permissions. Rights are granted to teams; users and API keys inherit the +rights of their teams. + +## Permissions + +| Permission | Grants | +|------------|--------| +| `event:read` | Read events, stats and changelogs | +| `event:write` | Create, update and delete events | +| `catalog:read` | Read the service catalog | +| `catalog:write` | Create, update and delete catalog entries | +| `lock:read` | List and read locks | +| `lock:write` | Create, update and release locks | +| `links:read` | Read custom links and Homer links | +| `links:write` | Manage custom links | +| `access:manage` | Manage users, teams and API keys | + +`access:manage` is effectively full administrative control, not just user +management: a caller holding it can add itself to `Administrators` through +`UpdateUser`, or mint an API key on that team, and reach every permission +that way. Grant it as you would grant root. + +Every gRPC method and REST route maps to exactly one permission. A method +missing from the mapping is refused. An anonymous caller lacking the +permission receives `401 Unauthorized` (gRPC `UNAUTHENTICATED`); an +authenticated caller lacking it receives `403 Forbidden` +(`PERMISSION_DENIED`). + +## Anonymous access + +`AUTH_ANONYMOUS_PERMISSIONS` lists the permissions granted without +credentials. When it is set, its value is used as is, even when empty. When +it is unset, the default is the read-only set +`event:read,catalog:read,lock:read,links:read` if `DEMO_MODE=true`, otherwise +every permission except `access:manage` (transitional default, so existing +installations keep working; the server logs a warning at startup, and the +default becomes empty in the next major release). + +Set it to an empty value to require authentication everywhere: + +```bash +AUTH_ANONYMOUS_PERMISSIONS= +``` + +## Initial administrator + +On first start with an empty user collection, Tracker creates the built-in +team `Administrators` (every permission, every service) and a local user +`admin` in it. The password comes from `AUTH_ADMIN_PASSWORD`, or is generated +and printed once in the logs: + +``` +WARN Initial admin account created with a generated password. Change it at first login. username=admin password=... +``` + +The account is flagged `mustChangePassword`. Change it right away: + +```bash +curl -c jar -X POST http://localhost:8080/api/v1alpha1/auth/login \ + -H 'Content-Type: application/json' \ + -d '{"username":"admin","password":""}' +curl -b jar -c jar -X POST http://localhost:8080/api/v1alpha1/auth/password \ + -H 'Content-Type: application/json' \ + -d '{"currentPassword":"","newPassword":""}' +``` + +Passwords are hashed with Argon2id and must be 12 to 128 characters long. + +## Sessions + +Login sets an `HttpOnly`, `SameSite=Lax` cookie named `tracker_session` +valid for `AUTH_SESSION_TTL` (default 12 hours). The cookie is `Secure` when +`AUTH_PUBLIC_URL` starts with `https://` or `AUTH_COOKIE_SECURE=true`. +Changing a password, disabling a user or resetting its password invalidates +existing sessions. Five failed logins for the same username and IP within a +minute block further attempts for a minute. The client IP is the peer address +of the connection, unless `AUTH_TRUST_PROXY=true`, in which case it is the last +entry of `X-Forwarded-For`, the one appended by the reverse proxy. The earlier +entries are client controlled and must not be trusted. + +A session token that no longer resolves, because it expired, was signed with +another secret, or its user was disabled or bumped, is refused with `401` when +it arrives in an `Authorization: Bearer` header. In the `tracker_session` +cookie it falls back to anonymous instead: the cookie is ambient, a browser +keeps sending a stale one on its own, and a `401` there would also cover the +SPA and the login page the user needs to recover. + +Logout is stateless: it only clears the cookie, so a session token stolen +beforehand stays valid until its own expiry (`AUTH_SESSION_TTL`, 12 hours by +default). Changing the user's password, disabling the user or resetting its +password bumps the session version and is the only way to revoke a token +early. + +| Endpoint | Description | +|----------|-------------| +| `POST /api/v1alpha1/auth/login` | Body `{"username","password"}`. `204` and cookie on success, `401` otherwise, `429` when rate limited. | +| `POST /api/v1alpha1/auth/logout` | Clears the cookie. | +| `POST /api/v1alpha1/auth/password` | Body `{"currentPassword","newPassword"}`. Requires a session. | +| `GET /api/v1alpha1/auth/me` | Identity, teams and effective permissions of the caller. Public. | +| `GET /api/v1alpha1/auth/config` | Login options and anonymous permissions. Public. | + +### Browser cross-site requests + +`SameSite=Lax` still lets a browser attach the session cookie to a top level +cross-site `GET` navigation, and the API keeps a write behind a `GET` binding +(`GET /api/v1alpha1/unlock/{id}`). A request is therefore treated as +cross-site when `Sec-Fetch-Site` is anything other than `same-origin`, +`same-site` or `none`, or, when that header is missing, when the `Origin` +header does not match `AUTH_PUBLIC_URL` (or the request scheme and `Host` +when `AUTH_PUBLIC_URL` is unset). + +On such a request the session cookie is ignored and the caller is anonymous, +so it gets whatever `AUTH_ANONYMOUS_PERMISSIONS` grants and nothing more. +`POST /api/v1alpha1/auth/login` goes further and answers `403` before doing +any password work. API keys and `Authorization: Bearer` tokens are explicit +credentials, not ambient ones, and are never dropped. Requests carrying +neither header, which is every non browser client, are unaffected. + +## Teams + +A team carries a list of permissions, an optional list of catalog services +(empty means every service; per-service filtering is enforced in a later +release) and optional OIDC group names (used once OIDC lands). Users belong +to any number of teams and get the union of their rights. The built-in +`Administrators` team cannot be renamed, deleted or stripped of permissions. + +| Endpoint | Permission | +|----------|------------| +| `GET/POST /api/v1alpha1/auth/teams` | `access:manage` | +| `PUT/DELETE /api/v1alpha1/auth/teams/{id}` | `access:manage` | +| `GET/POST /api/v1alpha1/auth/users` | `access:manage` | +| `PUT /api/v1alpha1/auth/users/{id}` | `access:manage` | + +Deleting a team detaches its users and revokes its API keys. The last +enabled member of `Administrators` cannot be disabled or removed from the +team, and nobody can disable their own account. + +## API keys + +API keys are meant for automation (CI, the MCP server, scripts). A key +belongs to a team and inherits its rights, or is global (every permission) +when created without a team, which only members of `Administrators` may do. +A global API key is a full administrator credential. + +| Endpoint | Permission | +|----------|------------| +| `GET /api/v1alpha1/auth/api-keys` | `access:manage` | +| `POST /api/v1alpha1/auth/api-keys` | `access:manage` | +| `DELETE /api/v1alpha1/auth/api-keys/{id}` | `access:manage` | + +```bash +curl -b jar -X POST http://localhost:8080/api/v1alpha1/auth/api-keys \ + -H 'Content-Type: application/json' \ + -d '{"name":"ci","teamId":"","expiresAt":"2027-01-01T00:00:00Z"}' +``` + +The response contains the secret exactly once. Keys look like +`trk__`; only a SHA-256 hash is stored. Present the key in +either header: + +``` +X-Api-Key: trk_... +Authorization: Bearer trk_... +``` + +For gRPC, send the same value in the `x-api-key` or `authorization` +metadata. + +An API key that is malformed, unknown, revoked or expired is refused with +`401 Unauthorized` (gRPC `UNAUTHENTICATED`) on every route, public ones +included. It does **not** fall back to the anonymous principal: that would +hand a dead credential whatever `AUTH_ANONYMOUS_PERMISSIONS` grants, which +under the transitional default is wider than most keys carry. Revoking a key +limited to `event:read` would then silently promote it to `event:write` +instead of shutting it down. + +Presenting no credential at all is unchanged: the caller is anonymous and gets +the anonymous permissions. + +## Metrics + +`tracker_auth_requests_total{principal,result}` counts authorization +decisions, with `principal` in `anonymous`, `user`, `apikey` and `result` +in `allowed`, `unauthenticated`, `denied`. + +`tracker_auth_logins_total{method,result}` counts login attempts, with +`method` in `local` (`oidc` once it lands) and `result` in `success`, +`failure`, `rate_limited`. Malformed bodies, cross-site refusals and internal +errors are not login attempts and are not counted. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 6370be46..2023cbbf 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -58,6 +58,29 @@ DEMO_MODE=true BUY_ME_COFFEE_URL=https://www.buymeacoffee.com/yourname ``` +### Authentication + +| Variable | Default | Description | +|----------|---------|-------------| +| `AUTH_ANONYMOUS_PERMISSIONS` | all except `access:manage` | Comma separated permissions granted to unauthenticated callers. Set it to an empty value to require authentication everywhere. The default becomes empty in the next major release. | +| `AUTH_ADMIN_PASSWORD` | generated | Password of the initial `admin` account. Only used when no user exists yet. When unset, a random password is printed once in the logs. | +| `AUTH_SESSION_SECRET` | persisted in MongoDB | Base64 secret (32 bytes minimum) signing session cookies. Set it explicitly when running several replicas without a shared database secret. | +| `AUTH_SESSION_TTL` | `12h` | Session lifetime. | +| `AUTH_PUBLIC_URL` | - | Public URL of the UI. An `https` URL makes cookies `Secure`. | +| `AUTH_COOKIE_SECURE` | `false` | Force the `Secure` flag on cookies. | +| `AUTH_TRUST_PROXY` | `false` | Use the last entry of `X-Forwarded-For` as client IP for login rate limiting, and `X-Forwarded-Proto` to decide the request scheme. Only enable it behind a reverse proxy that appends the peer address to the header. | + +When `AUTH_ANONYMOUS_PERMISSIONS` is set, its value is used as is, even when empty. When it is unset, the default is the read-only set `event:read,catalog:read,lock:read,links:read` if `DEMO_MODE=true`, otherwise every permission except `access:manage` (transitional default, with a startup warning). + +See [AUTHENTICATION.md](AUTHENTICATION.md) for permissions, teams and API keys. + +**Example:** +```bash +AUTH_ANONYMOUS_PERMISSIONS=event:read,catalog:read +AUTH_ADMIN_PASSWORD=change-me-at-first-login +AUTH_PUBLIC_URL=https://tracker.example.com +``` + ### Slack Integration | Variable | Default | Description | diff --git a/docs/EVENTS.md b/docs/EVENTS.md index 5272f837..85dd17c3 100644 --- a/docs/EVENTS.md +++ b/docs/EVENTS.md @@ -177,6 +177,19 @@ DELETE /api/v1alpha1/event/{id} curl -X DELETE http://localhost:8080/api/v1alpha1/event/507f1f77bcf86cd799439011 ``` +> **Behaviour change.** Up to and including `0.21.x` this route never deleted +> anything. The server method was named `DeleteEvent` while the generated +> `EventServiceServer` interface declares `DeleteEvents`, so the implementation +> never satisfied the interface and the embedded unimplemented stub answered +> every call with `501 Unimplemented`. The method is now named `DeleteEvents` +> and the route deletes the event for real. +> +> Check anything that calls it, a cleanup script, a CI job, a crawler, before +> upgrading: a request that used to be a harmless no-op now destroys data. +> Deletion requires `event:write`, which the transitional +> `AUTH_ANONYMOUS_PERMISSIONS` default grants to anonymous callers; set that +> variable to restrict it. See [Authentication](./AUTHENTICATION.md). + ### List Events ```bash diff --git a/docs/index.md b/docs/index.md index 121bfd2d..7057f1ca 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,6 +9,7 @@ Tracker est une API de gestion d'événements, de catalogues et de verrous const ### 📚 Documentation générale - [README](./README.md) - Vue d'ensemble et architecture - [Spécification API](./api-specification.md) - OpenAPI et Protobuf +- [Authentication](./AUTHENTICATION.md) - Users, teams, permissions and API keys ### 🔧 APIs par service - [Events API](./events.md) - Gestion des événements diff --git a/generated/openapiv2/apidocs.swagger.json b/generated/openapiv2/apidocs.swagger.json index ae414566..65213605 100644 --- a/generated/openapiv2/apidocs.swagger.json +++ b/generated/openapiv2/apidocs.swagger.json @@ -1,10 +1,14 @@ { "swagger": "2.0", "info": { - "title": "proto/catalog/v1alpha1/catalog.proto", + "title": "proto/auth/v1alpha1/auth.proto", "version": "version not set" }, "tags": [ + { + "name": "AuthService", + "description": "AuthService exposes the current identity and the administration of users,\nteams and API keys. Login, logout and password change are plain HTTP\nhandlers because they set cookies." + }, { "name": "CatalogService" }, @@ -22,6 +26,340 @@ "application/json" ], "paths": { + "/api/v1alpha1/auth/api-keys": { + "get": { + "operationId": "AuthService_ListApiKeys", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1ListApiKeysResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googleRpcStatus" + } + } + }, + "tags": [ + "AuthService" + ] + }, + "post": { + "operationId": "AuthService_CreateApiKey", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1CreateApiKeyResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googleRpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1alpha1CreateApiKeyRequest" + } + } + ], + "tags": [ + "AuthService" + ] + } + }, + "/api/v1alpha1/auth/api-keys/{id}": { + "delete": { + "operationId": "AuthService_RevokeApiKey", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1RevokeApiKeyResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googleRpcStatus" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "AuthService" + ] + } + }, + "/api/v1alpha1/auth/config": { + "get": { + "operationId": "AuthService_GetAuthConfig", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1GetAuthConfigResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googleRpcStatus" + } + } + }, + "tags": [ + "AuthService" + ] + } + }, + "/api/v1alpha1/auth/me": { + "get": { + "operationId": "AuthService_Me", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1MeResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googleRpcStatus" + } + } + }, + "tags": [ + "AuthService" + ] + } + }, + "/api/v1alpha1/auth/teams": { + "get": { + "operationId": "AuthService_ListTeams", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1ListTeamsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googleRpcStatus" + } + } + }, + "tags": [ + "AuthService" + ] + }, + "post": { + "operationId": "AuthService_CreateTeam", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1CreateTeamResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googleRpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1alpha1CreateTeamRequest" + } + } + ], + "tags": [ + "AuthService" + ] + } + }, + "/api/v1alpha1/auth/teams/{id}": { + "delete": { + "operationId": "AuthService_DeleteTeam", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1DeleteTeamResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googleRpcStatus" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "AuthService" + ] + }, + "put": { + "operationId": "AuthService_UpdateTeam", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1UpdateTeamResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googleRpcStatus" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AuthServiceUpdateTeamBody" + } + } + ], + "tags": [ + "AuthService" + ] + } + }, + "/api/v1alpha1/auth/users": { + "get": { + "operationId": "AuthService_ListUsers", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1ListUsersResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googleRpcStatus" + } + } + }, + "tags": [ + "AuthService" + ] + }, + "post": { + "operationId": "AuthService_CreateUser", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1CreateUserResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googleRpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1alpha1CreateUserRequest" + } + } + ], + "tags": [ + "AuthService" + ] + } + }, + "/api/v1alpha1/auth/users/{id}": { + "put": { + "operationId": "AuthService_UpdateUser", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1UpdateUserResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googleRpcStatus" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AuthServiceUpdateUserBody" + } + } + ], + "tags": [ + "AuthService" + ] + } + }, "/api/v1alpha1/catalog": { "get": { "operationId": "CatalogService_GetCatalog", @@ -634,7 +972,8 @@ "close", "done", "in_progress", - "planned" + "planned", + "waiting_approval" ], "default": "STATUS_UNSPECIFIED" }, @@ -810,7 +1149,8 @@ "close", "done", "in_progress", - "planned" + "planned", + "waiting_approval" ] }, "collectionFormat": "multi" @@ -951,7 +1291,8 @@ "close", "done", "in_progress", - "planned" + "planned", + "waiting_approval" ] }, "collectionFormat": "multi" @@ -1187,6 +1528,62 @@ } }, "definitions": { + "AuthServiceUpdateTeamBody": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "scope_all": { + "type": "boolean" + }, + "scope_services": { + "type": "array", + "items": { + "type": "string" + } + }, + "oidc_groups": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "AuthServiceUpdateUserBody": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "team_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "disabled": { + "type": "boolean" + }, + "new_password": { + "type": "string" + } + }, + "description": "UpdateUserRequest replaces email, display_name, team_ids and disabled.\nnew_password, when set, resets the password and invalidates sessions." + }, "CatalogServiceUpdateDependenciesBody": { "type": "object", "properties": { @@ -1284,7 +1681,8 @@ "close", "done", "in_progress", - "planned" + "planned", + "waiting_approval" ], "default": "STATUS_UNSPECIFIED" }, @@ -1357,8 +1755,45 @@ "event": { "$ref": "#/definitions/v1alpha1Event" } - }, - "title": "Response returns the updated event" + }, + "title": "Response returns the updated event" + }, + "v1alpha1ApiKey": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "prefix": { + "type": "string" + }, + "name": { + "type": "string" + }, + "team_id": { + "type": "string", + "description": "Empty for a global key." + }, + "created_by": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "expires_at": { + "type": "string", + "format": "date-time" + }, + "last_used_at": { + "type": "string", + "format": "date-time" + }, + "revoked_at": { + "type": "string", + "format": "date-time" + } + } }, "v1alpha1Catalog": { "type": "object", @@ -1545,7 +1980,7 @@ "telegram" ], "default": "COMMUNICATION_TYPE_UNSPECIFIED", - "title": "- slack: Slack channel\n - teams: Microsoft Teams channel\n - email: Email address\n - discord: Discord channel\n - mattermost: Mattermost channel\n - telegram: Telegram group/channel" + "description": "- slack: Slack channel\n - teams: Microsoft Teams channel\n - email: Email address\n - discord: Discord channel\n - mattermost: Mattermost channel\n - telegram: Telegram group/channel" }, "v1alpha1ComplianceSummary": { "type": "object", @@ -1575,6 +2010,34 @@ } } }, + "v1alpha1CreateApiKeyRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "team_id": { + "type": "string", + "description": "Empty creates a global key, allowed only for Administrators members." + }, + "expires_at": { + "type": "string", + "format": "date-time" + } + } + }, + "v1alpha1CreateApiKeyResponse": { + "type": "object", + "properties": { + "api_key": { + "$ref": "#/definitions/v1alpha1ApiKey" + }, + "secret": { + "type": "string", + "description": "Shown once, never stored." + } + } + }, "v1alpha1CreateEventRequest": { "type": "object", "properties": { @@ -1628,6 +2091,46 @@ } } }, + "v1alpha1CreateTeamRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "scope_all": { + "type": "boolean" + }, + "scope_services": { + "type": "array", + "items": { + "type": "string" + } + }, + "oidc_groups": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "v1alpha1CreateTeamResponse": { + "type": "object", + "properties": { + "team": { + "$ref": "#/definitions/v1alpha1Team" + } + } + }, "v1alpha1CreateUpdateCatalogRequest": { "type": "object", "properties": { @@ -1729,6 +2232,38 @@ } } }, + "v1alpha1CreateUserRequest": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "email": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "password": { + "type": "string", + "description": "Temporary password, the user must change it at first login." + }, + "team_ids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "v1alpha1CreateUserResponse": { + "type": "object", + "properties": { + "user": { + "$ref": "#/definitions/v1alpha1User" + } + } + }, "v1alpha1DashboardLink": { "type": "object", "properties": { @@ -1766,7 +2301,7 @@ "custom" ], "default": "DASHBOARD_TYPE_UNSPECIFIED", - "title": "- grafana: Grafana dashboard\n - datadog: Datadog dashboard\n - newrelic: New Relic dashboard\n - prometheus: Prometheus dashboard\n - kibana: Kibana dashboard\n - splunk: Splunk dashboard\n - dynatrace: Dynatrace dashboard\n - appdynamics: AppDynamics dashboard\n - custom: Custom dashboard platform" + "description": "- grafana: Grafana dashboard\n - datadog: Datadog dashboard\n - newrelic: New Relic dashboard\n - prometheus: Prometheus dashboard\n - kibana: Kibana dashboard\n - splunk: Splunk dashboard\n - dynatrace: Dynatrace dashboard\n - appdynamics: AppDynamics dashboard\n - custom: Custom dashboard platform" }, "v1alpha1DeleteCatalogResponse": { "type": "object", @@ -1790,6 +2325,9 @@ } } }, + "v1alpha1DeleteTeamResponse": { + "type": "object" + }, "v1alpha1DeliverableComplianceStats": { "type": "object", "properties": { @@ -1972,6 +2510,29 @@ } } }, + "v1alpha1GetAuthConfigResponse": { + "type": "object", + "properties": { + "local_login_enabled": { + "type": "boolean" + }, + "oidc_enabled": { + "type": "boolean" + }, + "oidc_button_label": { + "type": "string" + }, + "anonymous_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "demo_mode": { + "type": "boolean" + } + } + }, "v1alpha1GetCatalogResponse": { "type": "object", "properties": { @@ -2174,6 +2735,18 @@ ], "default": "LANGUAGES_UNSPECIFIED" }, + "v1alpha1ListApiKeysResponse": { + "type": "object", + "properties": { + "api_keys": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1alpha1ApiKey" + } + } + } + }, "v1alpha1ListCatalogsResponse": { "type": "object", "properties": { @@ -2222,6 +2795,30 @@ } } }, + "v1alpha1ListTeamsResponse": { + "type": "object", + "properties": { + "teams": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1alpha1Team" + } + } + } + }, + "v1alpha1ListUsersResponse": { + "type": "object", + "properties": { + "users": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1alpha1User" + } + } + } + }, "v1alpha1Lock": { "type": "object", "properties": { @@ -2252,6 +2849,59 @@ } } }, + "v1alpha1MeResponse": { + "type": "object", + "properties": { + "authenticated": { + "type": "boolean" + }, + "kind": { + "type": "string", + "title": "\"anonymous\", \"user\" or \"apikey\"" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "source": { + "type": "string", + "title": "\"local\" or \"oidc\", empty for API keys and anonymous" + }, + "teams": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1alpha1TeamRef" + } + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "scope_all": { + "type": "boolean" + }, + "scope_services": { + "type": "array", + "items": { + "type": "string" + } + }, + "must_change_password": { + "type": "boolean" + }, + "is_admin": { + "type": "boolean" + } + } + }, "v1alpha1MonthlyStats": { "type": "object", "properties": { @@ -2340,6 +2990,9 @@ } } }, + "v1alpha1RevokeApiKeyResponse": { + "type": "object" + }, "v1alpha1SLA": { "type": "object", "properties": { @@ -2386,6 +3039,55 @@ } } }, + "v1alpha1Team": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "scope_all": { + "type": "boolean" + }, + "scope_services": { + "type": "array", + "items": { + "type": "string" + } + }, + "oidc_groups": { + "type": "array", + "items": { + "type": "string" + } + }, + "builtin": { + "type": "boolean" + } + } + }, + "v1alpha1TeamRef": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "v1alpha1TodayEventsResponse": { "type": "object", "properties": { @@ -2462,6 +3164,22 @@ } } }, + "v1alpha1UpdateTeamResponse": { + "type": "object", + "properties": { + "team": { + "$ref": "#/definitions/v1alpha1Team" + } + } + }, + "v1alpha1UpdateUserResponse": { + "type": "object", + "properties": { + "user": { + "$ref": "#/definitions/v1alpha1User" + } + } + }, "v1alpha1UpdateVersionsResponse": { "type": "object", "properties": { @@ -2493,6 +3211,46 @@ }, "title": "Used deliverable in a project" }, + "v1alpha1User": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "email": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "source": { + "type": "string" + }, + "team_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "disabled": { + "type": "boolean" + }, + "must_change_password": { + "type": "boolean" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "last_login_at": { + "type": "string", + "format": "date-time" + } + } + }, "v1alpha1VulnerabilitySource": { "type": "object", "properties": { diff --git a/generated/proto/auth/v1alpha1/auth.pb.go b/generated/proto/auth/v1alpha1/auth.pb.go new file mode 100644 index 00000000..ac5fcfb5 --- /dev/null +++ b/generated/proto/auth/v1alpha1/auth.pb.go @@ -0,0 +1,1977 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc (unknown) +// source: proto/auth/v1alpha1/auth.proto + +package v1alpha1 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + 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) +) + +type GetAuthConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAuthConfigRequest) Reset() { + *x = GetAuthConfigRequest{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAuthConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAuthConfigRequest) ProtoMessage() {} + +func (x *GetAuthConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_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 GetAuthConfigRequest.ProtoReflect.Descriptor instead. +func (*GetAuthConfigRequest) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{0} +} + +type GetAuthConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + LocalLoginEnabled bool `protobuf:"varint,1,opt,name=local_login_enabled,json=localLoginEnabled,proto3" json:"local_login_enabled,omitempty"` + OidcEnabled bool `protobuf:"varint,2,opt,name=oidc_enabled,json=oidcEnabled,proto3" json:"oidc_enabled,omitempty"` + OidcButtonLabel string `protobuf:"bytes,3,opt,name=oidc_button_label,json=oidcButtonLabel,proto3" json:"oidc_button_label,omitempty"` + AnonymousPermissions []string `protobuf:"bytes,4,rep,name=anonymous_permissions,json=anonymousPermissions,proto3" json:"anonymous_permissions,omitempty"` + DemoMode bool `protobuf:"varint,5,opt,name=demo_mode,json=demoMode,proto3" json:"demo_mode,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAuthConfigResponse) Reset() { + *x = GetAuthConfigResponse{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAuthConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAuthConfigResponse) ProtoMessage() {} + +func (x *GetAuthConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_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 GetAuthConfigResponse.ProtoReflect.Descriptor instead. +func (*GetAuthConfigResponse) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{1} +} + +func (x *GetAuthConfigResponse) GetLocalLoginEnabled() bool { + if x != nil { + return x.LocalLoginEnabled + } + return false +} + +func (x *GetAuthConfigResponse) GetOidcEnabled() bool { + if x != nil { + return x.OidcEnabled + } + return false +} + +func (x *GetAuthConfigResponse) GetOidcButtonLabel() string { + if x != nil { + return x.OidcButtonLabel + } + return "" +} + +func (x *GetAuthConfigResponse) GetAnonymousPermissions() []string { + if x != nil { + return x.AnonymousPermissions + } + return nil +} + +func (x *GetAuthConfigResponse) GetDemoMode() bool { + if x != nil { + return x.DemoMode + } + return false +} + +type MeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MeRequest) Reset() { + *x = MeRequest{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MeRequest) ProtoMessage() {} + +func (x *MeRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_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 MeRequest.ProtoReflect.Descriptor instead. +func (*MeRequest) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{2} +} + +type TeamRef struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TeamRef) Reset() { + *x = TeamRef{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TeamRef) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TeamRef) ProtoMessage() {} + +func (x *TeamRef) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_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 TeamRef.ProtoReflect.Descriptor instead. +func (*TeamRef) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{3} +} + +func (x *TeamRef) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *TeamRef) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type MeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Authenticated bool `protobuf:"varint,1,opt,name=authenticated,proto3" json:"authenticated,omitempty"` + // "anonymous", "user" or "apikey" + Kind string `protobuf:"bytes,2,opt,name=kind,proto3" json:"kind,omitempty"` + UserId string `protobuf:"bytes,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Username string `protobuf:"bytes,4,opt,name=username,proto3" json:"username,omitempty"` + DisplayName string `protobuf:"bytes,5,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + // "local" or "oidc", empty for API keys and anonymous + Source string `protobuf:"bytes,6,opt,name=source,proto3" json:"source,omitempty"` + Teams []*TeamRef `protobuf:"bytes,7,rep,name=teams,proto3" json:"teams,omitempty"` + Permissions []string `protobuf:"bytes,8,rep,name=permissions,proto3" json:"permissions,omitempty"` + ScopeAll bool `protobuf:"varint,9,opt,name=scope_all,json=scopeAll,proto3" json:"scope_all,omitempty"` + ScopeServices []string `protobuf:"bytes,10,rep,name=scope_services,json=scopeServices,proto3" json:"scope_services,omitempty"` + MustChangePassword bool `protobuf:"varint,11,opt,name=must_change_password,json=mustChangePassword,proto3" json:"must_change_password,omitempty"` + IsAdmin bool `protobuf:"varint,12,opt,name=is_admin,json=isAdmin,proto3" json:"is_admin,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MeResponse) Reset() { + *x = MeResponse{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MeResponse) ProtoMessage() {} + +func (x *MeResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_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 MeResponse.ProtoReflect.Descriptor instead. +func (*MeResponse) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{4} +} + +func (x *MeResponse) GetAuthenticated() bool { + if x != nil { + return x.Authenticated + } + return false +} + +func (x *MeResponse) GetKind() string { + if x != nil { + return x.Kind + } + return "" +} + +func (x *MeResponse) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *MeResponse) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *MeResponse) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *MeResponse) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *MeResponse) GetTeams() []*TeamRef { + if x != nil { + return x.Teams + } + return nil +} + +func (x *MeResponse) GetPermissions() []string { + if x != nil { + return x.Permissions + } + return nil +} + +func (x *MeResponse) GetScopeAll() bool { + if x != nil { + return x.ScopeAll + } + return false +} + +func (x *MeResponse) GetScopeServices() []string { + if x != nil { + return x.ScopeServices + } + return nil +} + +func (x *MeResponse) GetMustChangePassword() bool { + if x != nil { + return x.MustChangePassword + } + return false +} + +func (x *MeResponse) GetIsAdmin() bool { + if x != nil { + return x.IsAdmin + } + return false +} + +type User struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` + DisplayName string `protobuf:"bytes,4,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + Source string `protobuf:"bytes,5,opt,name=source,proto3" json:"source,omitempty"` + TeamIds []string `protobuf:"bytes,6,rep,name=team_ids,json=teamIds,proto3" json:"team_ids,omitempty"` + Disabled bool `protobuf:"varint,7,opt,name=disabled,proto3" json:"disabled,omitempty"` + MustChangePassword bool `protobuf:"varint,8,opt,name=must_change_password,json=mustChangePassword,proto3" json:"must_change_password,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + LastLoginAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=last_login_at,json=lastLoginAt,proto3" json:"last_login_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *User) Reset() { + *x = User{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_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 User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{5} +} + +func (x *User) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *User) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *User) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *User) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *User) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *User) GetTeamIds() []string { + if x != nil { + return x.TeamIds + } + return nil +} + +func (x *User) GetDisabled() bool { + if x != nil { + return x.Disabled + } + return false +} + +func (x *User) GetMustChangePassword() bool { + if x != nil { + return x.MustChangePassword + } + return false +} + +func (x *User) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *User) GetLastLoginAt() *timestamppb.Timestamp { + if x != nil { + return x.LastLoginAt + } + return nil +} + +type ListUsersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUsersRequest) Reset() { + *x = ListUsersRequest{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUsersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUsersRequest) ProtoMessage() {} + +func (x *ListUsersRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_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 ListUsersRequest.ProtoReflect.Descriptor instead. +func (*ListUsersRequest) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{6} +} + +type ListUsersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUsersResponse) Reset() { + *x = ListUsersResponse{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUsersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUsersResponse) ProtoMessage() {} + +func (x *ListUsersResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_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 ListUsersResponse.ProtoReflect.Descriptor instead. +func (*ListUsersResponse) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{7} +} + +func (x *ListUsersResponse) GetUsers() []*User { + if x != nil { + return x.Users + } + return nil +} + +type CreateUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` + DisplayName string `protobuf:"bytes,3,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + // Temporary password, the user must change it at first login. + Password string `protobuf:"bytes,4,opt,name=password,proto3" json:"password,omitempty"` + TeamIds []string `protobuf:"bytes,5,rep,name=team_ids,json=teamIds,proto3" json:"team_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateUserRequest) Reset() { + *x = CreateUserRequest{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateUserRequest) ProtoMessage() {} + +func (x *CreateUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_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 CreateUserRequest.ProtoReflect.Descriptor instead. +func (*CreateUserRequest) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{8} +} + +func (x *CreateUserRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *CreateUserRequest) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *CreateUserRequest) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *CreateUserRequest) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *CreateUserRequest) GetTeamIds() []string { + if x != nil { + return x.TeamIds + } + return nil +} + +type CreateUserResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateUserResponse) Reset() { + *x = CreateUserResponse{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateUserResponse) ProtoMessage() {} + +func (x *CreateUserResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_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 CreateUserResponse.ProtoReflect.Descriptor instead. +func (*CreateUserResponse) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{9} +} + +func (x *CreateUserResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +// UpdateUserRequest replaces email, display_name, team_ids and disabled. +// new_password, when set, resets the password and invalidates sessions. +type UpdateUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` + DisplayName string `protobuf:"bytes,3,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + TeamIds []string `protobuf:"bytes,4,rep,name=team_ids,json=teamIds,proto3" json:"team_ids,omitempty"` + Disabled bool `protobuf:"varint,5,opt,name=disabled,proto3" json:"disabled,omitempty"` + NewPassword string `protobuf:"bytes,6,opt,name=new_password,json=newPassword,proto3" json:"new_password,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateUserRequest) Reset() { + *x = UpdateUserRequest{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateUserRequest) ProtoMessage() {} + +func (x *UpdateUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_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 UpdateUserRequest.ProtoReflect.Descriptor instead. +func (*UpdateUserRequest) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{10} +} + +func (x *UpdateUserRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *UpdateUserRequest) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *UpdateUserRequest) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *UpdateUserRequest) GetTeamIds() []string { + if x != nil { + return x.TeamIds + } + return nil +} + +func (x *UpdateUserRequest) GetDisabled() bool { + if x != nil { + return x.Disabled + } + return false +} + +func (x *UpdateUserRequest) GetNewPassword() string { + if x != nil { + return x.NewPassword + } + return "" +} + +type UpdateUserResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateUserResponse) Reset() { + *x = UpdateUserResponse{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateUserResponse) ProtoMessage() {} + +func (x *UpdateUserResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[11] + 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 UpdateUserResponse.ProtoReflect.Descriptor instead. +func (*UpdateUserResponse) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{11} +} + +func (x *UpdateUserResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +type Team struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + Permissions []string `protobuf:"bytes,4,rep,name=permissions,proto3" json:"permissions,omitempty"` + ScopeAll bool `protobuf:"varint,5,opt,name=scope_all,json=scopeAll,proto3" json:"scope_all,omitempty"` + ScopeServices []string `protobuf:"bytes,6,rep,name=scope_services,json=scopeServices,proto3" json:"scope_services,omitempty"` + OidcGroups []string `protobuf:"bytes,7,rep,name=oidc_groups,json=oidcGroups,proto3" json:"oidc_groups,omitempty"` + Builtin bool `protobuf:"varint,8,opt,name=builtin,proto3" json:"builtin,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Team) Reset() { + *x = Team{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Team) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Team) ProtoMessage() {} + +func (x *Team) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[12] + 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 Team.ProtoReflect.Descriptor instead. +func (*Team) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{12} +} + +func (x *Team) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Team) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Team) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Team) GetPermissions() []string { + if x != nil { + return x.Permissions + } + return nil +} + +func (x *Team) GetScopeAll() bool { + if x != nil { + return x.ScopeAll + } + return false +} + +func (x *Team) GetScopeServices() []string { + if x != nil { + return x.ScopeServices + } + return nil +} + +func (x *Team) GetOidcGroups() []string { + if x != nil { + return x.OidcGroups + } + return nil +} + +func (x *Team) GetBuiltin() bool { + if x != nil { + return x.Builtin + } + return false +} + +type ListTeamsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTeamsRequest) Reset() { + *x = ListTeamsRequest{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTeamsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTeamsRequest) ProtoMessage() {} + +func (x *ListTeamsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[13] + 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 ListTeamsRequest.ProtoReflect.Descriptor instead. +func (*ListTeamsRequest) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{13} +} + +type ListTeamsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Teams []*Team `protobuf:"bytes,1,rep,name=teams,proto3" json:"teams,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTeamsResponse) Reset() { + *x = ListTeamsResponse{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTeamsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTeamsResponse) ProtoMessage() {} + +func (x *ListTeamsResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[14] + 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 ListTeamsResponse.ProtoReflect.Descriptor instead. +func (*ListTeamsResponse) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{14} +} + +func (x *ListTeamsResponse) GetTeams() []*Team { + if x != nil { + return x.Teams + } + return nil +} + +type CreateTeamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + Permissions []string `protobuf:"bytes,3,rep,name=permissions,proto3" json:"permissions,omitempty"` + ScopeAll bool `protobuf:"varint,4,opt,name=scope_all,json=scopeAll,proto3" json:"scope_all,omitempty"` + ScopeServices []string `protobuf:"bytes,5,rep,name=scope_services,json=scopeServices,proto3" json:"scope_services,omitempty"` + OidcGroups []string `protobuf:"bytes,6,rep,name=oidc_groups,json=oidcGroups,proto3" json:"oidc_groups,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateTeamRequest) Reset() { + *x = CreateTeamRequest{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateTeamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateTeamRequest) ProtoMessage() {} + +func (x *CreateTeamRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[15] + 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 CreateTeamRequest.ProtoReflect.Descriptor instead. +func (*CreateTeamRequest) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{15} +} + +func (x *CreateTeamRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateTeamRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *CreateTeamRequest) GetPermissions() []string { + if x != nil { + return x.Permissions + } + return nil +} + +func (x *CreateTeamRequest) GetScopeAll() bool { + if x != nil { + return x.ScopeAll + } + return false +} + +func (x *CreateTeamRequest) GetScopeServices() []string { + if x != nil { + return x.ScopeServices + } + return nil +} + +func (x *CreateTeamRequest) GetOidcGroups() []string { + if x != nil { + return x.OidcGroups + } + return nil +} + +type CreateTeamResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Team *Team `protobuf:"bytes,1,opt,name=team,proto3" json:"team,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateTeamResponse) Reset() { + *x = CreateTeamResponse{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateTeamResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateTeamResponse) ProtoMessage() {} + +func (x *CreateTeamResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[16] + 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 CreateTeamResponse.ProtoReflect.Descriptor instead. +func (*CreateTeamResponse) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{16} +} + +func (x *CreateTeamResponse) GetTeam() *Team { + if x != nil { + return x.Team + } + return nil +} + +type UpdateTeamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + Permissions []string `protobuf:"bytes,4,rep,name=permissions,proto3" json:"permissions,omitempty"` + ScopeAll bool `protobuf:"varint,5,opt,name=scope_all,json=scopeAll,proto3" json:"scope_all,omitempty"` + ScopeServices []string `protobuf:"bytes,6,rep,name=scope_services,json=scopeServices,proto3" json:"scope_services,omitempty"` + OidcGroups []string `protobuf:"bytes,7,rep,name=oidc_groups,json=oidcGroups,proto3" json:"oidc_groups,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateTeamRequest) Reset() { + *x = UpdateTeamRequest{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateTeamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateTeamRequest) ProtoMessage() {} + +func (x *UpdateTeamRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[17] + 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 UpdateTeamRequest.ProtoReflect.Descriptor instead. +func (*UpdateTeamRequest) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{17} +} + +func (x *UpdateTeamRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *UpdateTeamRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UpdateTeamRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *UpdateTeamRequest) GetPermissions() []string { + if x != nil { + return x.Permissions + } + return nil +} + +func (x *UpdateTeamRequest) GetScopeAll() bool { + if x != nil { + return x.ScopeAll + } + return false +} + +func (x *UpdateTeamRequest) GetScopeServices() []string { + if x != nil { + return x.ScopeServices + } + return nil +} + +func (x *UpdateTeamRequest) GetOidcGroups() []string { + if x != nil { + return x.OidcGroups + } + return nil +} + +type UpdateTeamResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Team *Team `protobuf:"bytes,1,opt,name=team,proto3" json:"team,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateTeamResponse) Reset() { + *x = UpdateTeamResponse{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateTeamResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateTeamResponse) ProtoMessage() {} + +func (x *UpdateTeamResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[18] + 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 UpdateTeamResponse.ProtoReflect.Descriptor instead. +func (*UpdateTeamResponse) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{18} +} + +func (x *UpdateTeamResponse) GetTeam() *Team { + if x != nil { + return x.Team + } + return nil +} + +type DeleteTeamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteTeamRequest) Reset() { + *x = DeleteTeamRequest{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteTeamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteTeamRequest) ProtoMessage() {} + +func (x *DeleteTeamRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[19] + 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 DeleteTeamRequest.ProtoReflect.Descriptor instead. +func (*DeleteTeamRequest) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{19} +} + +func (x *DeleteTeamRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type DeleteTeamResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteTeamResponse) Reset() { + *x = DeleteTeamResponse{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteTeamResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteTeamResponse) ProtoMessage() {} + +func (x *DeleteTeamResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[20] + 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 DeleteTeamResponse.ProtoReflect.Descriptor instead. +func (*DeleteTeamResponse) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{20} +} + +type ApiKey struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Prefix string `protobuf:"bytes,2,opt,name=prefix,proto3" json:"prefix,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + // Empty for a global key. + TeamId string `protobuf:"bytes,4,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + CreatedBy string `protobuf:"bytes,5,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + LastUsedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=last_used_at,json=lastUsedAt,proto3" json:"last_used_at,omitempty"` + RevokedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=revoked_at,json=revokedAt,proto3" json:"revoked_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApiKey) Reset() { + *x = ApiKey{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApiKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApiKey) ProtoMessage() {} + +func (x *ApiKey) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[21] + 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 ApiKey.ProtoReflect.Descriptor instead. +func (*ApiKey) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{21} +} + +func (x *ApiKey) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ApiKey) GetPrefix() string { + if x != nil { + return x.Prefix + } + return "" +} + +func (x *ApiKey) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ApiKey) GetTeamId() string { + if x != nil { + return x.TeamId + } + return "" +} + +func (x *ApiKey) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +func (x *ApiKey) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *ApiKey) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +func (x *ApiKey) GetLastUsedAt() *timestamppb.Timestamp { + if x != nil { + return x.LastUsedAt + } + return nil +} + +func (x *ApiKey) GetRevokedAt() *timestamppb.Timestamp { + if x != nil { + return x.RevokedAt + } + return nil +} + +type ListApiKeysRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListApiKeysRequest) Reset() { + *x = ListApiKeysRequest{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListApiKeysRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListApiKeysRequest) ProtoMessage() {} + +func (x *ListApiKeysRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[22] + 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 ListApiKeysRequest.ProtoReflect.Descriptor instead. +func (*ListApiKeysRequest) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{22} +} + +type ListApiKeysResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ApiKeys []*ApiKey `protobuf:"bytes,1,rep,name=api_keys,json=apiKeys,proto3" json:"api_keys,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListApiKeysResponse) Reset() { + *x = ListApiKeysResponse{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListApiKeysResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListApiKeysResponse) ProtoMessage() {} + +func (x *ListApiKeysResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[23] + 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 ListApiKeysResponse.ProtoReflect.Descriptor instead. +func (*ListApiKeysResponse) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{23} +} + +func (x *ListApiKeysResponse) GetApiKeys() []*ApiKey { + if x != nil { + return x.ApiKeys + } + return nil +} + +type CreateApiKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Empty creates a global key, allowed only for Administrators members. + TeamId string `protobuf:"bytes,2,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateApiKeyRequest) Reset() { + *x = CreateApiKeyRequest{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateApiKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateApiKeyRequest) ProtoMessage() {} + +func (x *CreateApiKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[24] + 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 CreateApiKeyRequest.ProtoReflect.Descriptor instead. +func (*CreateApiKeyRequest) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{24} +} + +func (x *CreateApiKeyRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateApiKeyRequest) GetTeamId() string { + if x != nil { + return x.TeamId + } + return "" +} + +func (x *CreateApiKeyRequest) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +type CreateApiKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ApiKey *ApiKey `protobuf:"bytes,1,opt,name=api_key,json=apiKey,proto3" json:"api_key,omitempty"` + // Shown once, never stored. + Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateApiKeyResponse) Reset() { + *x = CreateApiKeyResponse{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateApiKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateApiKeyResponse) ProtoMessage() {} + +func (x *CreateApiKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[25] + 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 CreateApiKeyResponse.ProtoReflect.Descriptor instead. +func (*CreateApiKeyResponse) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{25} +} + +func (x *CreateApiKeyResponse) GetApiKey() *ApiKey { + if x != nil { + return x.ApiKey + } + return nil +} + +func (x *CreateApiKeyResponse) GetSecret() string { + if x != nil { + return x.Secret + } + return "" +} + +type RevokeApiKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeApiKeyRequest) Reset() { + *x = RevokeApiKeyRequest{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeApiKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeApiKeyRequest) ProtoMessage() {} + +func (x *RevokeApiKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[26] + 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 RevokeApiKeyRequest.ProtoReflect.Descriptor instead. +func (*RevokeApiKeyRequest) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{26} +} + +func (x *RevokeApiKeyRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type RevokeApiKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeApiKeyResponse) Reset() { + *x = RevokeApiKeyResponse{} + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeApiKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeApiKeyResponse) ProtoMessage() {} + +func (x *RevokeApiKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_auth_v1alpha1_auth_proto_msgTypes[27] + 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 RevokeApiKeyResponse.ProtoReflect.Descriptor instead. +func (*RevokeApiKeyResponse) Descriptor() ([]byte, []int) { + return file_proto_auth_v1alpha1_auth_proto_rawDescGZIP(), []int{27} +} + +var File_proto_auth_v1alpha1_auth_proto protoreflect.FileDescriptor + +const file_proto_auth_v1alpha1_auth_proto_rawDesc = "" + + "\n" + + "\x1eproto/auth/v1alpha1/auth.proto\x12\x15tracker.auth.v1alpha1\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x16\n" + + "\x14GetAuthConfigRequest\"\xe8\x01\n" + + "\x15GetAuthConfigResponse\x12.\n" + + "\x13local_login_enabled\x18\x01 \x01(\bR\x11localLoginEnabled\x12!\n" + + "\foidc_enabled\x18\x02 \x01(\bR\voidcEnabled\x12*\n" + + "\x11oidc_button_label\x18\x03 \x01(\tR\x0foidcButtonLabel\x123\n" + + "\x15anonymous_permissions\x18\x04 \x03(\tR\x14anonymousPermissions\x12\x1b\n" + + "\tdemo_mode\x18\x05 \x01(\bR\bdemoMode\"\v\n" + + "\tMeRequest\"-\n" + + "\aTeamRef\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\"\x9f\x03\n" + + "\n" + + "MeResponse\x12$\n" + + "\rauthenticated\x18\x01 \x01(\bR\rauthenticated\x12\x12\n" + + "\x04kind\x18\x02 \x01(\tR\x04kind\x12\x17\n" + + "\auser_id\x18\x03 \x01(\tR\x06userId\x12\x1a\n" + + "\busername\x18\x04 \x01(\tR\busername\x12!\n" + + "\fdisplay_name\x18\x05 \x01(\tR\vdisplayName\x12\x16\n" + + "\x06source\x18\x06 \x01(\tR\x06source\x124\n" + + "\x05teams\x18\a \x03(\v2\x1e.tracker.auth.v1alpha1.TeamRefR\x05teams\x12 \n" + + "\vpermissions\x18\b \x03(\tR\vpermissions\x12\x1b\n" + + "\tscope_all\x18\t \x01(\bR\bscopeAll\x12%\n" + + "\x0escope_services\x18\n" + + " \x03(\tR\rscopeServices\x120\n" + + "\x14must_change_password\x18\v \x01(\bR\x12mustChangePassword\x12\x19\n" + + "\bis_admin\x18\f \x01(\bR\aisAdmin\"\xe7\x02\n" + + "\x04User\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1a\n" + + "\busername\x18\x02 \x01(\tR\busername\x12\x14\n" + + "\x05email\x18\x03 \x01(\tR\x05email\x12!\n" + + "\fdisplay_name\x18\x04 \x01(\tR\vdisplayName\x12\x16\n" + + "\x06source\x18\x05 \x01(\tR\x06source\x12\x19\n" + + "\bteam_ids\x18\x06 \x03(\tR\ateamIds\x12\x1a\n" + + "\bdisabled\x18\a \x01(\bR\bdisabled\x120\n" + + "\x14must_change_password\x18\b \x01(\bR\x12mustChangePassword\x129\n" + + "\n" + + "created_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12>\n" + + "\rlast_login_at\x18\n" + + " \x01(\v2\x1a.google.protobuf.TimestampR\vlastLoginAt\"\x12\n" + + "\x10ListUsersRequest\"F\n" + + "\x11ListUsersResponse\x121\n" + + "\x05users\x18\x01 \x03(\v2\x1b.tracker.auth.v1alpha1.UserR\x05users\"\x9f\x01\n" + + "\x11CreateUserRequest\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12\x14\n" + + "\x05email\x18\x02 \x01(\tR\x05email\x12!\n" + + "\fdisplay_name\x18\x03 \x01(\tR\vdisplayName\x12\x1a\n" + + "\bpassword\x18\x04 \x01(\tR\bpassword\x12\x19\n" + + "\bteam_ids\x18\x05 \x03(\tR\ateamIds\"E\n" + + "\x12CreateUserResponse\x12/\n" + + "\x04user\x18\x01 \x01(\v2\x1b.tracker.auth.v1alpha1.UserR\x04user\"\xb6\x01\n" + + "\x11UpdateUserRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + + "\x05email\x18\x02 \x01(\tR\x05email\x12!\n" + + "\fdisplay_name\x18\x03 \x01(\tR\vdisplayName\x12\x19\n" + + "\bteam_ids\x18\x04 \x03(\tR\ateamIds\x12\x1a\n" + + "\bdisabled\x18\x05 \x01(\bR\bdisabled\x12!\n" + + "\fnew_password\x18\x06 \x01(\tR\vnewPassword\"E\n" + + "\x12UpdateUserResponse\x12/\n" + + "\x04user\x18\x01 \x01(\v2\x1b.tracker.auth.v1alpha1.UserR\x04user\"\xed\x01\n" + + "\x04Team\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12 \n" + + "\vpermissions\x18\x04 \x03(\tR\vpermissions\x12\x1b\n" + + "\tscope_all\x18\x05 \x01(\bR\bscopeAll\x12%\n" + + "\x0escope_services\x18\x06 \x03(\tR\rscopeServices\x12\x1f\n" + + "\voidc_groups\x18\a \x03(\tR\n" + + "oidcGroups\x12\x18\n" + + "\abuiltin\x18\b \x01(\bR\abuiltin\"\x12\n" + + "\x10ListTeamsRequest\"F\n" + + "\x11ListTeamsResponse\x121\n" + + "\x05teams\x18\x01 \x03(\v2\x1b.tracker.auth.v1alpha1.TeamR\x05teams\"\xd0\x01\n" + + "\x11CreateTeamRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12 \n" + + "\vpermissions\x18\x03 \x03(\tR\vpermissions\x12\x1b\n" + + "\tscope_all\x18\x04 \x01(\bR\bscopeAll\x12%\n" + + "\x0escope_services\x18\x05 \x03(\tR\rscopeServices\x12\x1f\n" + + "\voidc_groups\x18\x06 \x03(\tR\n" + + "oidcGroups\"E\n" + + "\x12CreateTeamResponse\x12/\n" + + "\x04team\x18\x01 \x01(\v2\x1b.tracker.auth.v1alpha1.TeamR\x04team\"\xe0\x01\n" + + "\x11UpdateTeamRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12 \n" + + "\vpermissions\x18\x04 \x03(\tR\vpermissions\x12\x1b\n" + + "\tscope_all\x18\x05 \x01(\bR\bscopeAll\x12%\n" + + "\x0escope_services\x18\x06 \x03(\tR\rscopeServices\x12\x1f\n" + + "\voidc_groups\x18\a \x03(\tR\n" + + "oidcGroups\"E\n" + + "\x12UpdateTeamResponse\x12/\n" + + "\x04team\x18\x01 \x01(\v2\x1b.tracker.auth.v1alpha1.TeamR\x04team\"#\n" + + "\x11DeleteTeamRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"\x14\n" + + "\x12DeleteTeamResponse\"\xeb\x02\n" + + "\x06ApiKey\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" + + "\x06prefix\x18\x02 \x01(\tR\x06prefix\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x17\n" + + "\ateam_id\x18\x04 \x01(\tR\x06teamId\x12\x1d\n" + + "\n" + + "created_by\x18\x05 \x01(\tR\tcreatedBy\x129\n" + + "\n" + + "created_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + + "\n" + + "expires_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12<\n" + + "\flast_used_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "lastUsedAt\x129\n" + + "\n" + + "revoked_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\trevokedAt\"\x14\n" + + "\x12ListApiKeysRequest\"O\n" + + "\x13ListApiKeysResponse\x128\n" + + "\bapi_keys\x18\x01 \x03(\v2\x1d.tracker.auth.v1alpha1.ApiKeyR\aapiKeys\"}\n" + + "\x13CreateApiKeyRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x17\n" + + "\ateam_id\x18\x02 \x01(\tR\x06teamId\x129\n" + + "\n" + + "expires_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"f\n" + + "\x14CreateApiKeyResponse\x126\n" + + "\aapi_key\x18\x01 \x01(\v2\x1d.tracker.auth.v1alpha1.ApiKeyR\x06apiKey\x12\x16\n" + + "\x06secret\x18\x02 \x01(\tR\x06secret\"%\n" + + "\x13RevokeApiKeyRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"\x16\n" + + "\x14RevokeApiKeyResponse2\xf8\f\n" + + "\vAuthService\x12\x8d\x01\n" + + "\rGetAuthConfig\x12+.tracker.auth.v1alpha1.GetAuthConfigRequest\x1a,.tracker.auth.v1alpha1.GetAuthConfigResponse\"!\x82\xd3\xe4\x93\x02\x1b\x12\x19/api/v1alpha1/auth/config\x12h\n" + + "\x02Me\x12 .tracker.auth.v1alpha1.MeRequest\x1a!.tracker.auth.v1alpha1.MeResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\x12\x15/api/v1alpha1/auth/me\x12\x80\x01\n" + + "\tListUsers\x12'.tracker.auth.v1alpha1.ListUsersRequest\x1a(.tracker.auth.v1alpha1.ListUsersResponse\" \x82\xd3\xe4\x93\x02\x1a\x12\x18/api/v1alpha1/auth/users\x12\x86\x01\n" + + "\n" + + "CreateUser\x12(.tracker.auth.v1alpha1.CreateUserRequest\x1a).tracker.auth.v1alpha1.CreateUserResponse\"#\x82\xd3\xe4\x93\x02\x1d:\x01*\"\x18/api/v1alpha1/auth/users\x12\x8b\x01\n" + + "\n" + + "UpdateUser\x12(.tracker.auth.v1alpha1.UpdateUserRequest\x1a).tracker.auth.v1alpha1.UpdateUserResponse\"(\x82\xd3\xe4\x93\x02\":\x01*\x1a\x1d/api/v1alpha1/auth/users/{id}\x12\x80\x01\n" + + "\tListTeams\x12'.tracker.auth.v1alpha1.ListTeamsRequest\x1a(.tracker.auth.v1alpha1.ListTeamsResponse\" \x82\xd3\xe4\x93\x02\x1a\x12\x18/api/v1alpha1/auth/teams\x12\x86\x01\n" + + "\n" + + "CreateTeam\x12(.tracker.auth.v1alpha1.CreateTeamRequest\x1a).tracker.auth.v1alpha1.CreateTeamResponse\"#\x82\xd3\xe4\x93\x02\x1d:\x01*\"\x18/api/v1alpha1/auth/teams\x12\x8b\x01\n" + + "\n" + + "UpdateTeam\x12(.tracker.auth.v1alpha1.UpdateTeamRequest\x1a).tracker.auth.v1alpha1.UpdateTeamResponse\"(\x82\xd3\xe4\x93\x02\":\x01*\x1a\x1d/api/v1alpha1/auth/teams/{id}\x12\x88\x01\n" + + "\n" + + "DeleteTeam\x12(.tracker.auth.v1alpha1.DeleteTeamRequest\x1a).tracker.auth.v1alpha1.DeleteTeamResponse\"%\x82\xd3\xe4\x93\x02\x1f*\x1d/api/v1alpha1/auth/teams/{id}\x12\x89\x01\n" + + "\vListApiKeys\x12).tracker.auth.v1alpha1.ListApiKeysRequest\x1a*.tracker.auth.v1alpha1.ListApiKeysResponse\"#\x82\xd3\xe4\x93\x02\x1d\x12\x1b/api/v1alpha1/auth/api-keys\x12\x8f\x01\n" + + "\fCreateApiKey\x12*.tracker.auth.v1alpha1.CreateApiKeyRequest\x1a+.tracker.auth.v1alpha1.CreateApiKeyResponse\"&\x82\xd3\xe4\x93\x02 :\x01*\"\x1b/api/v1alpha1/auth/api-keys\x12\x91\x01\n" + + "\fRevokeApiKey\x12*.tracker.auth.v1alpha1.RevokeApiKeyRequest\x1a+.tracker.auth.v1alpha1.RevokeApiKeyResponse\"(\x82\xd3\xe4\x93\x02\"* /api/v1alpha1/auth/api-keys/{id}B\x15Z\x13proto/auth/v1alpha1b\x06proto3" + +var ( + file_proto_auth_v1alpha1_auth_proto_rawDescOnce sync.Once + file_proto_auth_v1alpha1_auth_proto_rawDescData []byte +) + +func file_proto_auth_v1alpha1_auth_proto_rawDescGZIP() []byte { + file_proto_auth_v1alpha1_auth_proto_rawDescOnce.Do(func() { + file_proto_auth_v1alpha1_auth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_auth_v1alpha1_auth_proto_rawDesc), len(file_proto_auth_v1alpha1_auth_proto_rawDesc))) + }) + return file_proto_auth_v1alpha1_auth_proto_rawDescData +} + +var file_proto_auth_v1alpha1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 28) +var file_proto_auth_v1alpha1_auth_proto_goTypes = []any{ + (*GetAuthConfigRequest)(nil), // 0: tracker.auth.v1alpha1.GetAuthConfigRequest + (*GetAuthConfigResponse)(nil), // 1: tracker.auth.v1alpha1.GetAuthConfigResponse + (*MeRequest)(nil), // 2: tracker.auth.v1alpha1.MeRequest + (*TeamRef)(nil), // 3: tracker.auth.v1alpha1.TeamRef + (*MeResponse)(nil), // 4: tracker.auth.v1alpha1.MeResponse + (*User)(nil), // 5: tracker.auth.v1alpha1.User + (*ListUsersRequest)(nil), // 6: tracker.auth.v1alpha1.ListUsersRequest + (*ListUsersResponse)(nil), // 7: tracker.auth.v1alpha1.ListUsersResponse + (*CreateUserRequest)(nil), // 8: tracker.auth.v1alpha1.CreateUserRequest + (*CreateUserResponse)(nil), // 9: tracker.auth.v1alpha1.CreateUserResponse + (*UpdateUserRequest)(nil), // 10: tracker.auth.v1alpha1.UpdateUserRequest + (*UpdateUserResponse)(nil), // 11: tracker.auth.v1alpha1.UpdateUserResponse + (*Team)(nil), // 12: tracker.auth.v1alpha1.Team + (*ListTeamsRequest)(nil), // 13: tracker.auth.v1alpha1.ListTeamsRequest + (*ListTeamsResponse)(nil), // 14: tracker.auth.v1alpha1.ListTeamsResponse + (*CreateTeamRequest)(nil), // 15: tracker.auth.v1alpha1.CreateTeamRequest + (*CreateTeamResponse)(nil), // 16: tracker.auth.v1alpha1.CreateTeamResponse + (*UpdateTeamRequest)(nil), // 17: tracker.auth.v1alpha1.UpdateTeamRequest + (*UpdateTeamResponse)(nil), // 18: tracker.auth.v1alpha1.UpdateTeamResponse + (*DeleteTeamRequest)(nil), // 19: tracker.auth.v1alpha1.DeleteTeamRequest + (*DeleteTeamResponse)(nil), // 20: tracker.auth.v1alpha1.DeleteTeamResponse + (*ApiKey)(nil), // 21: tracker.auth.v1alpha1.ApiKey + (*ListApiKeysRequest)(nil), // 22: tracker.auth.v1alpha1.ListApiKeysRequest + (*ListApiKeysResponse)(nil), // 23: tracker.auth.v1alpha1.ListApiKeysResponse + (*CreateApiKeyRequest)(nil), // 24: tracker.auth.v1alpha1.CreateApiKeyRequest + (*CreateApiKeyResponse)(nil), // 25: tracker.auth.v1alpha1.CreateApiKeyResponse + (*RevokeApiKeyRequest)(nil), // 26: tracker.auth.v1alpha1.RevokeApiKeyRequest + (*RevokeApiKeyResponse)(nil), // 27: tracker.auth.v1alpha1.RevokeApiKeyResponse + (*timestamppb.Timestamp)(nil), // 28: google.protobuf.Timestamp +} +var file_proto_auth_v1alpha1_auth_proto_depIdxs = []int32{ + 3, // 0: tracker.auth.v1alpha1.MeResponse.teams:type_name -> tracker.auth.v1alpha1.TeamRef + 28, // 1: tracker.auth.v1alpha1.User.created_at:type_name -> google.protobuf.Timestamp + 28, // 2: tracker.auth.v1alpha1.User.last_login_at:type_name -> google.protobuf.Timestamp + 5, // 3: tracker.auth.v1alpha1.ListUsersResponse.users:type_name -> tracker.auth.v1alpha1.User + 5, // 4: tracker.auth.v1alpha1.CreateUserResponse.user:type_name -> tracker.auth.v1alpha1.User + 5, // 5: tracker.auth.v1alpha1.UpdateUserResponse.user:type_name -> tracker.auth.v1alpha1.User + 12, // 6: tracker.auth.v1alpha1.ListTeamsResponse.teams:type_name -> tracker.auth.v1alpha1.Team + 12, // 7: tracker.auth.v1alpha1.CreateTeamResponse.team:type_name -> tracker.auth.v1alpha1.Team + 12, // 8: tracker.auth.v1alpha1.UpdateTeamResponse.team:type_name -> tracker.auth.v1alpha1.Team + 28, // 9: tracker.auth.v1alpha1.ApiKey.created_at:type_name -> google.protobuf.Timestamp + 28, // 10: tracker.auth.v1alpha1.ApiKey.expires_at:type_name -> google.protobuf.Timestamp + 28, // 11: tracker.auth.v1alpha1.ApiKey.last_used_at:type_name -> google.protobuf.Timestamp + 28, // 12: tracker.auth.v1alpha1.ApiKey.revoked_at:type_name -> google.protobuf.Timestamp + 21, // 13: tracker.auth.v1alpha1.ListApiKeysResponse.api_keys:type_name -> tracker.auth.v1alpha1.ApiKey + 28, // 14: tracker.auth.v1alpha1.CreateApiKeyRequest.expires_at:type_name -> google.protobuf.Timestamp + 21, // 15: tracker.auth.v1alpha1.CreateApiKeyResponse.api_key:type_name -> tracker.auth.v1alpha1.ApiKey + 0, // 16: tracker.auth.v1alpha1.AuthService.GetAuthConfig:input_type -> tracker.auth.v1alpha1.GetAuthConfigRequest + 2, // 17: tracker.auth.v1alpha1.AuthService.Me:input_type -> tracker.auth.v1alpha1.MeRequest + 6, // 18: tracker.auth.v1alpha1.AuthService.ListUsers:input_type -> tracker.auth.v1alpha1.ListUsersRequest + 8, // 19: tracker.auth.v1alpha1.AuthService.CreateUser:input_type -> tracker.auth.v1alpha1.CreateUserRequest + 10, // 20: tracker.auth.v1alpha1.AuthService.UpdateUser:input_type -> tracker.auth.v1alpha1.UpdateUserRequest + 13, // 21: tracker.auth.v1alpha1.AuthService.ListTeams:input_type -> tracker.auth.v1alpha1.ListTeamsRequest + 15, // 22: tracker.auth.v1alpha1.AuthService.CreateTeam:input_type -> tracker.auth.v1alpha1.CreateTeamRequest + 17, // 23: tracker.auth.v1alpha1.AuthService.UpdateTeam:input_type -> tracker.auth.v1alpha1.UpdateTeamRequest + 19, // 24: tracker.auth.v1alpha1.AuthService.DeleteTeam:input_type -> tracker.auth.v1alpha1.DeleteTeamRequest + 22, // 25: tracker.auth.v1alpha1.AuthService.ListApiKeys:input_type -> tracker.auth.v1alpha1.ListApiKeysRequest + 24, // 26: tracker.auth.v1alpha1.AuthService.CreateApiKey:input_type -> tracker.auth.v1alpha1.CreateApiKeyRequest + 26, // 27: tracker.auth.v1alpha1.AuthService.RevokeApiKey:input_type -> tracker.auth.v1alpha1.RevokeApiKeyRequest + 1, // 28: tracker.auth.v1alpha1.AuthService.GetAuthConfig:output_type -> tracker.auth.v1alpha1.GetAuthConfigResponse + 4, // 29: tracker.auth.v1alpha1.AuthService.Me:output_type -> tracker.auth.v1alpha1.MeResponse + 7, // 30: tracker.auth.v1alpha1.AuthService.ListUsers:output_type -> tracker.auth.v1alpha1.ListUsersResponse + 9, // 31: tracker.auth.v1alpha1.AuthService.CreateUser:output_type -> tracker.auth.v1alpha1.CreateUserResponse + 11, // 32: tracker.auth.v1alpha1.AuthService.UpdateUser:output_type -> tracker.auth.v1alpha1.UpdateUserResponse + 14, // 33: tracker.auth.v1alpha1.AuthService.ListTeams:output_type -> tracker.auth.v1alpha1.ListTeamsResponse + 16, // 34: tracker.auth.v1alpha1.AuthService.CreateTeam:output_type -> tracker.auth.v1alpha1.CreateTeamResponse + 18, // 35: tracker.auth.v1alpha1.AuthService.UpdateTeam:output_type -> tracker.auth.v1alpha1.UpdateTeamResponse + 20, // 36: tracker.auth.v1alpha1.AuthService.DeleteTeam:output_type -> tracker.auth.v1alpha1.DeleteTeamResponse + 23, // 37: tracker.auth.v1alpha1.AuthService.ListApiKeys:output_type -> tracker.auth.v1alpha1.ListApiKeysResponse + 25, // 38: tracker.auth.v1alpha1.AuthService.CreateApiKey:output_type -> tracker.auth.v1alpha1.CreateApiKeyResponse + 27, // 39: tracker.auth.v1alpha1.AuthService.RevokeApiKey:output_type -> tracker.auth.v1alpha1.RevokeApiKeyResponse + 28, // [28:40] is the sub-list for method output_type + 16, // [16:28] is the sub-list for method input_type + 16, // [16:16] is the sub-list for extension type_name + 16, // [16:16] is the sub-list for extension extendee + 0, // [0:16] is the sub-list for field type_name +} + +func init() { file_proto_auth_v1alpha1_auth_proto_init() } +func file_proto_auth_v1alpha1_auth_proto_init() { + if File_proto_auth_v1alpha1_auth_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_auth_v1alpha1_auth_proto_rawDesc), len(file_proto_auth_v1alpha1_auth_proto_rawDesc)), + NumEnums: 0, + NumMessages: 28, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_auth_v1alpha1_auth_proto_goTypes, + DependencyIndexes: file_proto_auth_v1alpha1_auth_proto_depIdxs, + MessageInfos: file_proto_auth_v1alpha1_auth_proto_msgTypes, + }.Build() + File_proto_auth_v1alpha1_auth_proto = out.File + file_proto_auth_v1alpha1_auth_proto_goTypes = nil + file_proto_auth_v1alpha1_auth_proto_depIdxs = nil +} diff --git a/generated/proto/auth/v1alpha1/auth.pb.gw.go b/generated/proto/auth/v1alpha1/auth.pb.gw.go new file mode 100644 index 00000000..292f5078 --- /dev/null +++ b/generated/proto/auth/v1alpha1/auth.pb.gw.go @@ -0,0 +1,913 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: proto/auth/v1alpha1/auth.proto + +/* +Package v1alpha1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package v1alpha1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +func request_AuthService_GetAuthConfig_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetAuthConfigRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.GetAuthConfig(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_GetAuthConfig_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetAuthConfigRequest + metadata runtime.ServerMetadata + ) + msg, err := server.GetAuthConfig(ctx, &protoReq) + return msg, metadata, err +} + +func request_AuthService_Me_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq MeRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.Me(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_Me_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq MeRequest + metadata runtime.ServerMetadata + ) + msg, err := server.Me(ctx, &protoReq) + return msg, metadata, err +} + +func request_AuthService_ListUsers_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListUsersRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.ListUsers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_ListUsers_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListUsersRequest + metadata runtime.ServerMetadata + ) + msg, err := server.ListUsers(ctx, &protoReq) + return msg, metadata, err +} + +func request_AuthService_CreateUser_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateUserRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.CreateUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_CreateUser_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateUserRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CreateUser(ctx, &protoReq) + return msg, metadata, err +} + +func request_AuthService_UpdateUser_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateUserRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + msg, err := client.UpdateUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_UpdateUser_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateUserRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + msg, err := server.UpdateUser(ctx, &protoReq) + return msg, metadata, err +} + +func request_AuthService_ListTeams_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListTeamsRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.ListTeams(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_ListTeams_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListTeamsRequest + metadata runtime.ServerMetadata + ) + msg, err := server.ListTeams(ctx, &protoReq) + return msg, metadata, err +} + +func request_AuthService_CreateTeam_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateTeamRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.CreateTeam(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_CreateTeam_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateTeamRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CreateTeam(ctx, &protoReq) + return msg, metadata, err +} + +func request_AuthService_UpdateTeam_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateTeamRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + msg, err := client.UpdateTeam(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_UpdateTeam_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateTeamRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + msg, err := server.UpdateTeam(ctx, &protoReq) + return msg, metadata, err +} + +func request_AuthService_DeleteTeam_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteTeamRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + msg, err := client.DeleteTeam(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_DeleteTeam_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteTeamRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + msg, err := server.DeleteTeam(ctx, &protoReq) + return msg, metadata, err +} + +func request_AuthService_ListApiKeys_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListApiKeysRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.ListApiKeys(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_ListApiKeys_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListApiKeysRequest + metadata runtime.ServerMetadata + ) + msg, err := server.ListApiKeys(ctx, &protoReq) + return msg, metadata, err +} + +func request_AuthService_CreateApiKey_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateApiKeyRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.CreateApiKey(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_CreateApiKey_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateApiKeyRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CreateApiKey(ctx, &protoReq) + return msg, metadata, err +} + +func request_AuthService_RevokeApiKey_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq RevokeApiKeyRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + msg, err := client.RevokeApiKey(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_RevokeApiKey_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq RevokeApiKeyRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + msg, err := server.RevokeApiKey(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterAuthServiceHandlerServer registers the http handlers for service AuthService to "mux". +// UnaryRPC :call AuthServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAuthServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AuthServiceServer) error { + mux.Handle(http.MethodGet, pattern_AuthService_GetAuthConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/GetAuthConfig", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/config")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_GetAuthConfig_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_GetAuthConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_AuthService_Me_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/Me", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/me")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_Me_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_Me_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_AuthService_ListUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/ListUsers", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/users")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_ListUsers_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_ListUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_AuthService_CreateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/CreateUser", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/users")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_CreateUser_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_CreateUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPut, pattern_AuthService_UpdateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/UpdateUser", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/users/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_UpdateUser_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_UpdateUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_AuthService_ListTeams_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/ListTeams", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/teams")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_ListTeams_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_ListTeams_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_AuthService_CreateTeam_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/CreateTeam", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/teams")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_CreateTeam_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_CreateTeam_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPut, pattern_AuthService_UpdateTeam_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/UpdateTeam", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/teams/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_UpdateTeam_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_UpdateTeam_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_AuthService_DeleteTeam_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/DeleteTeam", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/teams/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_DeleteTeam_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_DeleteTeam_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_AuthService_ListApiKeys_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/ListApiKeys", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/api-keys")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_ListApiKeys_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_ListApiKeys_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_AuthService_CreateApiKey_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/CreateApiKey", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/api-keys")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_CreateApiKey_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_CreateApiKey_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_AuthService_RevokeApiKey_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/RevokeApiKey", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/api-keys/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_RevokeApiKey_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_RevokeApiKey_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterAuthServiceHandlerFromEndpoint is same as RegisterAuthServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterAuthServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterAuthServiceHandler(ctx, mux, conn) +} + +// RegisterAuthServiceHandler registers the http handlers for service AuthService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterAuthServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterAuthServiceHandlerClient(ctx, mux, NewAuthServiceClient(conn)) +} + +// RegisterAuthServiceHandlerClient registers the http handlers for service AuthService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AuthServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AuthServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "AuthServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterAuthServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AuthServiceClient) error { + mux.Handle(http.MethodGet, pattern_AuthService_GetAuthConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/GetAuthConfig", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/config")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_GetAuthConfig_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_GetAuthConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_AuthService_Me_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/Me", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/me")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_Me_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_Me_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_AuthService_ListUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/ListUsers", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/users")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_ListUsers_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_ListUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_AuthService_CreateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/CreateUser", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/users")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_CreateUser_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_CreateUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPut, pattern_AuthService_UpdateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/UpdateUser", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/users/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_UpdateUser_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_UpdateUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_AuthService_ListTeams_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/ListTeams", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/teams")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_ListTeams_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_ListTeams_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_AuthService_CreateTeam_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/CreateTeam", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/teams")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_CreateTeam_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_CreateTeam_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPut, pattern_AuthService_UpdateTeam_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/UpdateTeam", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/teams/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_UpdateTeam_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_UpdateTeam_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_AuthService_DeleteTeam_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/DeleteTeam", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/teams/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_DeleteTeam_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_DeleteTeam_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_AuthService_ListApiKeys_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/ListApiKeys", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/api-keys")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_ListApiKeys_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_ListApiKeys_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_AuthService_CreateApiKey_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/CreateApiKey", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/api-keys")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_CreateApiKey_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_CreateApiKey_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_AuthService_RevokeApiKey_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/tracker.auth.v1alpha1.AuthService/RevokeApiKey", runtime.WithHTTPPathPattern("/api/v1alpha1/auth/api-keys/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_RevokeApiKey_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_RevokeApiKey_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_AuthService_GetAuthConfig_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1alpha1", "auth", "config"}, "")) + pattern_AuthService_Me_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1alpha1", "auth", "me"}, "")) + pattern_AuthService_ListUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1alpha1", "auth", "users"}, "")) + pattern_AuthService_CreateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1alpha1", "auth", "users"}, "")) + pattern_AuthService_UpdateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"api", "v1alpha1", "auth", "users", "id"}, "")) + pattern_AuthService_ListTeams_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1alpha1", "auth", "teams"}, "")) + pattern_AuthService_CreateTeam_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1alpha1", "auth", "teams"}, "")) + pattern_AuthService_UpdateTeam_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"api", "v1alpha1", "auth", "teams", "id"}, "")) + pattern_AuthService_DeleteTeam_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"api", "v1alpha1", "auth", "teams", "id"}, "")) + pattern_AuthService_ListApiKeys_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1alpha1", "auth", "api-keys"}, "")) + pattern_AuthService_CreateApiKey_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1alpha1", "auth", "api-keys"}, "")) + pattern_AuthService_RevokeApiKey_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"api", "v1alpha1", "auth", "api-keys", "id"}, "")) +) + +var ( + forward_AuthService_GetAuthConfig_0 = runtime.ForwardResponseMessage + forward_AuthService_Me_0 = runtime.ForwardResponseMessage + forward_AuthService_ListUsers_0 = runtime.ForwardResponseMessage + forward_AuthService_CreateUser_0 = runtime.ForwardResponseMessage + forward_AuthService_UpdateUser_0 = runtime.ForwardResponseMessage + forward_AuthService_ListTeams_0 = runtime.ForwardResponseMessage + forward_AuthService_CreateTeam_0 = runtime.ForwardResponseMessage + forward_AuthService_UpdateTeam_0 = runtime.ForwardResponseMessage + forward_AuthService_DeleteTeam_0 = runtime.ForwardResponseMessage + forward_AuthService_ListApiKeys_0 = runtime.ForwardResponseMessage + forward_AuthService_CreateApiKey_0 = runtime.ForwardResponseMessage + forward_AuthService_RevokeApiKey_0 = runtime.ForwardResponseMessage +) diff --git a/generated/proto/auth/v1alpha1/auth.pb.validate.go b/generated/proto/auth/v1alpha1/auth.pb.validate.go new file mode 100644 index 00000000..2498d84c --- /dev/null +++ b/generated/proto/auth/v1alpha1/auth.pb.validate.go @@ -0,0 +1,3456 @@ +// Code generated by protoc-gen-validate. DO NOT EDIT. +// source: proto/auth/v1alpha1/auth.proto + +package v1alpha1 + +import ( + "bytes" + "errors" + "fmt" + "net" + "net/mail" + "net/url" + "regexp" + "sort" + "strings" + "time" + "unicode/utf8" + + "google.golang.org/protobuf/types/known/anypb" +) + +// ensure the imports are used +var ( + _ = bytes.MinRead + _ = errors.New("") + _ = fmt.Print + _ = utf8.UTFMax + _ = (*regexp.Regexp)(nil) + _ = (*strings.Reader)(nil) + _ = net.IPv4len + _ = time.Duration(0) + _ = (*url.URL)(nil) + _ = (*mail.Address)(nil) + _ = anypb.Any{} + _ = sort.Sort +) + +// Validate checks the field values on GetAuthConfigRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *GetAuthConfigRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on GetAuthConfigRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// GetAuthConfigRequestMultiError, or nil if none found. +func (m *GetAuthConfigRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *GetAuthConfigRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if len(errors) > 0 { + return GetAuthConfigRequestMultiError(errors) + } + + return nil +} + +// GetAuthConfigRequestMultiError is an error wrapping multiple validation +// errors returned by GetAuthConfigRequest.ValidateAll() if the designated +// constraints aren't met. +type GetAuthConfigRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m GetAuthConfigRequestMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m GetAuthConfigRequestMultiError) AllErrors() []error { return m } + +// GetAuthConfigRequestValidationError is the validation error returned by +// GetAuthConfigRequest.Validate if the designated constraints aren't met. +type GetAuthConfigRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e GetAuthConfigRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e GetAuthConfigRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e GetAuthConfigRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e GetAuthConfigRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e GetAuthConfigRequestValidationError) ErrorName() string { + return "GetAuthConfigRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e GetAuthConfigRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sGetAuthConfigRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = GetAuthConfigRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = GetAuthConfigRequestValidationError{} + +// Validate checks the field values on GetAuthConfigResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *GetAuthConfigResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on GetAuthConfigResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// GetAuthConfigResponseMultiError, or nil if none found. +func (m *GetAuthConfigResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *GetAuthConfigResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for LocalLoginEnabled + + // no validation rules for OidcEnabled + + // no validation rules for OidcButtonLabel + + // no validation rules for DemoMode + + if len(errors) > 0 { + return GetAuthConfigResponseMultiError(errors) + } + + return nil +} + +// GetAuthConfigResponseMultiError is an error wrapping multiple validation +// errors returned by GetAuthConfigResponse.ValidateAll() if the designated +// constraints aren't met. +type GetAuthConfigResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m GetAuthConfigResponseMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m GetAuthConfigResponseMultiError) AllErrors() []error { return m } + +// GetAuthConfigResponseValidationError is the validation error returned by +// GetAuthConfigResponse.Validate if the designated constraints aren't met. +type GetAuthConfigResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e GetAuthConfigResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e GetAuthConfigResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e GetAuthConfigResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e GetAuthConfigResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e GetAuthConfigResponseValidationError) ErrorName() string { + return "GetAuthConfigResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e GetAuthConfigResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sGetAuthConfigResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = GetAuthConfigResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = GetAuthConfigResponseValidationError{} + +// Validate checks the field values on MeRequest with the rules defined in the +// proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *MeRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on MeRequest with the rules defined in +// the proto definition for this message. If any rules are violated, the +// result is a list of violation errors wrapped in MeRequestMultiError, or nil +// if none found. +func (m *MeRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *MeRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if len(errors) > 0 { + return MeRequestMultiError(errors) + } + + return nil +} + +// MeRequestMultiError is an error wrapping multiple validation errors returned +// by MeRequest.ValidateAll() if the designated constraints aren't met. +type MeRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m MeRequestMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m MeRequestMultiError) AllErrors() []error { return m } + +// MeRequestValidationError is the validation error returned by +// MeRequest.Validate if the designated constraints aren't met. +type MeRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e MeRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e MeRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e MeRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e MeRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e MeRequestValidationError) ErrorName() string { return "MeRequestValidationError" } + +// Error satisfies the builtin error interface +func (e MeRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sMeRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = MeRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = MeRequestValidationError{} + +// Validate checks the field values on TeamRef with the rules defined in the +// proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *TeamRef) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on TeamRef with the rules defined in the +// proto definition for this message. If any rules are violated, the result is +// a list of violation errors wrapped in TeamRefMultiError, or nil if none found. +func (m *TeamRef) ValidateAll() error { + return m.validate(true) +} + +func (m *TeamRef) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Id + + // no validation rules for Name + + if len(errors) > 0 { + return TeamRefMultiError(errors) + } + + return nil +} + +// TeamRefMultiError is an error wrapping multiple validation errors returned +// by TeamRef.ValidateAll() if the designated constraints aren't met. +type TeamRefMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m TeamRefMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m TeamRefMultiError) AllErrors() []error { return m } + +// TeamRefValidationError is the validation error returned by TeamRef.Validate +// if the designated constraints aren't met. +type TeamRefValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e TeamRefValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e TeamRefValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e TeamRefValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e TeamRefValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e TeamRefValidationError) ErrorName() string { return "TeamRefValidationError" } + +// Error satisfies the builtin error interface +func (e TeamRefValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sTeamRef.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = TeamRefValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = TeamRefValidationError{} + +// Validate checks the field values on MeResponse with the rules defined in the +// proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *MeResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on MeResponse with the rules defined in +// the proto definition for this message. If any rules are violated, the +// result is a list of violation errors wrapped in MeResponseMultiError, or +// nil if none found. +func (m *MeResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *MeResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Authenticated + + // no validation rules for Kind + + // no validation rules for UserId + + // no validation rules for Username + + // no validation rules for DisplayName + + // no validation rules for Source + + for idx, item := range m.GetTeams() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, MeResponseValidationError{ + field: fmt.Sprintf("Teams[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, MeResponseValidationError{ + field: fmt.Sprintf("Teams[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return MeResponseValidationError{ + field: fmt.Sprintf("Teams[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + // no validation rules for ScopeAll + + // no validation rules for MustChangePassword + + // no validation rules for IsAdmin + + if len(errors) > 0 { + return MeResponseMultiError(errors) + } + + return nil +} + +// MeResponseMultiError is an error wrapping multiple validation errors +// returned by MeResponse.ValidateAll() if the designated constraints aren't met. +type MeResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m MeResponseMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m MeResponseMultiError) AllErrors() []error { return m } + +// MeResponseValidationError is the validation error returned by +// MeResponse.Validate if the designated constraints aren't met. +type MeResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e MeResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e MeResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e MeResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e MeResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e MeResponseValidationError) ErrorName() string { return "MeResponseValidationError" } + +// Error satisfies the builtin error interface +func (e MeResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sMeResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = MeResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = MeResponseValidationError{} + +// Validate checks the field values on User with the rules defined in the proto +// definition for this message. If any rules are violated, the first error +// encountered is returned, or nil if there are no violations. +func (m *User) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on User with the rules defined in the +// proto definition for this message. If any rules are violated, the result is +// a list of violation errors wrapped in UserMultiError, or nil if none found. +func (m *User) ValidateAll() error { + return m.validate(true) +} + +func (m *User) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Id + + // no validation rules for Username + + // no validation rules for Email + + // no validation rules for DisplayName + + // no validation rules for Source + + // no validation rules for Disabled + + // no validation rules for MustChangePassword + + if all { + switch v := interface{}(m.GetCreatedAt()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, UserValidationError{ + field: "CreatedAt", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, UserValidationError{ + field: "CreatedAt", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetCreatedAt()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return UserValidationError{ + field: "CreatedAt", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if all { + switch v := interface{}(m.GetLastLoginAt()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, UserValidationError{ + field: "LastLoginAt", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, UserValidationError{ + field: "LastLoginAt", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetLastLoginAt()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return UserValidationError{ + field: "LastLoginAt", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if len(errors) > 0 { + return UserMultiError(errors) + } + + return nil +} + +// UserMultiError is an error wrapping multiple validation errors returned by +// User.ValidateAll() if the designated constraints aren't met. +type UserMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m UserMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m UserMultiError) AllErrors() []error { return m } + +// UserValidationError is the validation error returned by User.Validate if the +// designated constraints aren't met. +type UserValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e UserValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e UserValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e UserValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e UserValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e UserValidationError) ErrorName() string { return "UserValidationError" } + +// Error satisfies the builtin error interface +func (e UserValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sUser.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = UserValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = UserValidationError{} + +// Validate checks the field values on ListUsersRequest with the rules defined +// in the proto definition for this message. If any rules are violated, the +// first error encountered is returned, or nil if there are no violations. +func (m *ListUsersRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on ListUsersRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// ListUsersRequestMultiError, or nil if none found. +func (m *ListUsersRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *ListUsersRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if len(errors) > 0 { + return ListUsersRequestMultiError(errors) + } + + return nil +} + +// ListUsersRequestMultiError is an error wrapping multiple validation errors +// returned by ListUsersRequest.ValidateAll() if the designated constraints +// aren't met. +type ListUsersRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m ListUsersRequestMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m ListUsersRequestMultiError) AllErrors() []error { return m } + +// ListUsersRequestValidationError is the validation error returned by +// ListUsersRequest.Validate if the designated constraints aren't met. +type ListUsersRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ListUsersRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ListUsersRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ListUsersRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ListUsersRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ListUsersRequestValidationError) ErrorName() string { return "ListUsersRequestValidationError" } + +// Error satisfies the builtin error interface +func (e ListUsersRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sListUsersRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ListUsersRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ListUsersRequestValidationError{} + +// Validate checks the field values on ListUsersResponse with the rules defined +// in the proto definition for this message. If any rules are violated, the +// first error encountered is returned, or nil if there are no violations. +func (m *ListUsersResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on ListUsersResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// ListUsersResponseMultiError, or nil if none found. +func (m *ListUsersResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *ListUsersResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + for idx, item := range m.GetUsers() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, ListUsersResponseValidationError{ + field: fmt.Sprintf("Users[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, ListUsersResponseValidationError{ + field: fmt.Sprintf("Users[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return ListUsersResponseValidationError{ + field: fmt.Sprintf("Users[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + if len(errors) > 0 { + return ListUsersResponseMultiError(errors) + } + + return nil +} + +// ListUsersResponseMultiError is an error wrapping multiple validation errors +// returned by ListUsersResponse.ValidateAll() if the designated constraints +// aren't met. +type ListUsersResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m ListUsersResponseMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m ListUsersResponseMultiError) AllErrors() []error { return m } + +// ListUsersResponseValidationError is the validation error returned by +// ListUsersResponse.Validate if the designated constraints aren't met. +type ListUsersResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ListUsersResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ListUsersResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ListUsersResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ListUsersResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ListUsersResponseValidationError) ErrorName() string { + return "ListUsersResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e ListUsersResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sListUsersResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ListUsersResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ListUsersResponseValidationError{} + +// Validate checks the field values on CreateUserRequest with the rules defined +// in the proto definition for this message. If any rules are violated, the +// first error encountered is returned, or nil if there are no violations. +func (m *CreateUserRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on CreateUserRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// CreateUserRequestMultiError, or nil if none found. +func (m *CreateUserRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *CreateUserRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Username + + // no validation rules for Email + + // no validation rules for DisplayName + + // no validation rules for Password + + if len(errors) > 0 { + return CreateUserRequestMultiError(errors) + } + + return nil +} + +// CreateUserRequestMultiError is an error wrapping multiple validation errors +// returned by CreateUserRequest.ValidateAll() if the designated constraints +// aren't met. +type CreateUserRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m CreateUserRequestMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m CreateUserRequestMultiError) AllErrors() []error { return m } + +// CreateUserRequestValidationError is the validation error returned by +// CreateUserRequest.Validate if the designated constraints aren't met. +type CreateUserRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e CreateUserRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e CreateUserRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e CreateUserRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e CreateUserRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e CreateUserRequestValidationError) ErrorName() string { + return "CreateUserRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e CreateUserRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sCreateUserRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = CreateUserRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = CreateUserRequestValidationError{} + +// Validate checks the field values on CreateUserResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *CreateUserResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on CreateUserResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// CreateUserResponseMultiError, or nil if none found. +func (m *CreateUserResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *CreateUserResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if all { + switch v := interface{}(m.GetUser()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, CreateUserResponseValidationError{ + field: "User", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, CreateUserResponseValidationError{ + field: "User", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetUser()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return CreateUserResponseValidationError{ + field: "User", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if len(errors) > 0 { + return CreateUserResponseMultiError(errors) + } + + return nil +} + +// CreateUserResponseMultiError is an error wrapping multiple validation errors +// returned by CreateUserResponse.ValidateAll() if the designated constraints +// aren't met. +type CreateUserResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m CreateUserResponseMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m CreateUserResponseMultiError) AllErrors() []error { return m } + +// CreateUserResponseValidationError is the validation error returned by +// CreateUserResponse.Validate if the designated constraints aren't met. +type CreateUserResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e CreateUserResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e CreateUserResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e CreateUserResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e CreateUserResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e CreateUserResponseValidationError) ErrorName() string { + return "CreateUserResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e CreateUserResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sCreateUserResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = CreateUserResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = CreateUserResponseValidationError{} + +// Validate checks the field values on UpdateUserRequest with the rules defined +// in the proto definition for this message. If any rules are violated, the +// first error encountered is returned, or nil if there are no violations. +func (m *UpdateUserRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on UpdateUserRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// UpdateUserRequestMultiError, or nil if none found. +func (m *UpdateUserRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *UpdateUserRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Id + + // no validation rules for Email + + // no validation rules for DisplayName + + // no validation rules for Disabled + + // no validation rules for NewPassword + + if len(errors) > 0 { + return UpdateUserRequestMultiError(errors) + } + + return nil +} + +// UpdateUserRequestMultiError is an error wrapping multiple validation errors +// returned by UpdateUserRequest.ValidateAll() if the designated constraints +// aren't met. +type UpdateUserRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m UpdateUserRequestMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m UpdateUserRequestMultiError) AllErrors() []error { return m } + +// UpdateUserRequestValidationError is the validation error returned by +// UpdateUserRequest.Validate if the designated constraints aren't met. +type UpdateUserRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e UpdateUserRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e UpdateUserRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e UpdateUserRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e UpdateUserRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e UpdateUserRequestValidationError) ErrorName() string { + return "UpdateUserRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e UpdateUserRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sUpdateUserRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = UpdateUserRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = UpdateUserRequestValidationError{} + +// Validate checks the field values on UpdateUserResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *UpdateUserResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on UpdateUserResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// UpdateUserResponseMultiError, or nil if none found. +func (m *UpdateUserResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *UpdateUserResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if all { + switch v := interface{}(m.GetUser()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, UpdateUserResponseValidationError{ + field: "User", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, UpdateUserResponseValidationError{ + field: "User", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetUser()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return UpdateUserResponseValidationError{ + field: "User", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if len(errors) > 0 { + return UpdateUserResponseMultiError(errors) + } + + return nil +} + +// UpdateUserResponseMultiError is an error wrapping multiple validation errors +// returned by UpdateUserResponse.ValidateAll() if the designated constraints +// aren't met. +type UpdateUserResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m UpdateUserResponseMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m UpdateUserResponseMultiError) AllErrors() []error { return m } + +// UpdateUserResponseValidationError is the validation error returned by +// UpdateUserResponse.Validate if the designated constraints aren't met. +type UpdateUserResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e UpdateUserResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e UpdateUserResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e UpdateUserResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e UpdateUserResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e UpdateUserResponseValidationError) ErrorName() string { + return "UpdateUserResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e UpdateUserResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sUpdateUserResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = UpdateUserResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = UpdateUserResponseValidationError{} + +// Validate checks the field values on Team with the rules defined in the proto +// definition for this message. If any rules are violated, the first error +// encountered is returned, or nil if there are no violations. +func (m *Team) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on Team with the rules defined in the +// proto definition for this message. If any rules are violated, the result is +// a list of violation errors wrapped in TeamMultiError, or nil if none found. +func (m *Team) ValidateAll() error { + return m.validate(true) +} + +func (m *Team) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Id + + // no validation rules for Name + + // no validation rules for Description + + // no validation rules for ScopeAll + + // no validation rules for Builtin + + if len(errors) > 0 { + return TeamMultiError(errors) + } + + return nil +} + +// TeamMultiError is an error wrapping multiple validation errors returned by +// Team.ValidateAll() if the designated constraints aren't met. +type TeamMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m TeamMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m TeamMultiError) AllErrors() []error { return m } + +// TeamValidationError is the validation error returned by Team.Validate if the +// designated constraints aren't met. +type TeamValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e TeamValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e TeamValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e TeamValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e TeamValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e TeamValidationError) ErrorName() string { return "TeamValidationError" } + +// Error satisfies the builtin error interface +func (e TeamValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sTeam.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = TeamValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = TeamValidationError{} + +// Validate checks the field values on ListTeamsRequest with the rules defined +// in the proto definition for this message. If any rules are violated, the +// first error encountered is returned, or nil if there are no violations. +func (m *ListTeamsRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on ListTeamsRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// ListTeamsRequestMultiError, or nil if none found. +func (m *ListTeamsRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *ListTeamsRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if len(errors) > 0 { + return ListTeamsRequestMultiError(errors) + } + + return nil +} + +// ListTeamsRequestMultiError is an error wrapping multiple validation errors +// returned by ListTeamsRequest.ValidateAll() if the designated constraints +// aren't met. +type ListTeamsRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m ListTeamsRequestMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m ListTeamsRequestMultiError) AllErrors() []error { return m } + +// ListTeamsRequestValidationError is the validation error returned by +// ListTeamsRequest.Validate if the designated constraints aren't met. +type ListTeamsRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ListTeamsRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ListTeamsRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ListTeamsRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ListTeamsRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ListTeamsRequestValidationError) ErrorName() string { return "ListTeamsRequestValidationError" } + +// Error satisfies the builtin error interface +func (e ListTeamsRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sListTeamsRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ListTeamsRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ListTeamsRequestValidationError{} + +// Validate checks the field values on ListTeamsResponse with the rules defined +// in the proto definition for this message. If any rules are violated, the +// first error encountered is returned, or nil if there are no violations. +func (m *ListTeamsResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on ListTeamsResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// ListTeamsResponseMultiError, or nil if none found. +func (m *ListTeamsResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *ListTeamsResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + for idx, item := range m.GetTeams() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, ListTeamsResponseValidationError{ + field: fmt.Sprintf("Teams[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, ListTeamsResponseValidationError{ + field: fmt.Sprintf("Teams[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return ListTeamsResponseValidationError{ + field: fmt.Sprintf("Teams[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + if len(errors) > 0 { + return ListTeamsResponseMultiError(errors) + } + + return nil +} + +// ListTeamsResponseMultiError is an error wrapping multiple validation errors +// returned by ListTeamsResponse.ValidateAll() if the designated constraints +// aren't met. +type ListTeamsResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m ListTeamsResponseMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m ListTeamsResponseMultiError) AllErrors() []error { return m } + +// ListTeamsResponseValidationError is the validation error returned by +// ListTeamsResponse.Validate if the designated constraints aren't met. +type ListTeamsResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ListTeamsResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ListTeamsResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ListTeamsResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ListTeamsResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ListTeamsResponseValidationError) ErrorName() string { + return "ListTeamsResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e ListTeamsResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sListTeamsResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ListTeamsResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ListTeamsResponseValidationError{} + +// Validate checks the field values on CreateTeamRequest with the rules defined +// in the proto definition for this message. If any rules are violated, the +// first error encountered is returned, or nil if there are no violations. +func (m *CreateTeamRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on CreateTeamRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// CreateTeamRequestMultiError, or nil if none found. +func (m *CreateTeamRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *CreateTeamRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Name + + // no validation rules for Description + + // no validation rules for ScopeAll + + if len(errors) > 0 { + return CreateTeamRequestMultiError(errors) + } + + return nil +} + +// CreateTeamRequestMultiError is an error wrapping multiple validation errors +// returned by CreateTeamRequest.ValidateAll() if the designated constraints +// aren't met. +type CreateTeamRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m CreateTeamRequestMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m CreateTeamRequestMultiError) AllErrors() []error { return m } + +// CreateTeamRequestValidationError is the validation error returned by +// CreateTeamRequest.Validate if the designated constraints aren't met. +type CreateTeamRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e CreateTeamRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e CreateTeamRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e CreateTeamRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e CreateTeamRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e CreateTeamRequestValidationError) ErrorName() string { + return "CreateTeamRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e CreateTeamRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sCreateTeamRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = CreateTeamRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = CreateTeamRequestValidationError{} + +// Validate checks the field values on CreateTeamResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *CreateTeamResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on CreateTeamResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// CreateTeamResponseMultiError, or nil if none found. +func (m *CreateTeamResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *CreateTeamResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if all { + switch v := interface{}(m.GetTeam()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, CreateTeamResponseValidationError{ + field: "Team", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, CreateTeamResponseValidationError{ + field: "Team", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetTeam()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return CreateTeamResponseValidationError{ + field: "Team", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if len(errors) > 0 { + return CreateTeamResponseMultiError(errors) + } + + return nil +} + +// CreateTeamResponseMultiError is an error wrapping multiple validation errors +// returned by CreateTeamResponse.ValidateAll() if the designated constraints +// aren't met. +type CreateTeamResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m CreateTeamResponseMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m CreateTeamResponseMultiError) AllErrors() []error { return m } + +// CreateTeamResponseValidationError is the validation error returned by +// CreateTeamResponse.Validate if the designated constraints aren't met. +type CreateTeamResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e CreateTeamResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e CreateTeamResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e CreateTeamResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e CreateTeamResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e CreateTeamResponseValidationError) ErrorName() string { + return "CreateTeamResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e CreateTeamResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sCreateTeamResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = CreateTeamResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = CreateTeamResponseValidationError{} + +// Validate checks the field values on UpdateTeamRequest with the rules defined +// in the proto definition for this message. If any rules are violated, the +// first error encountered is returned, or nil if there are no violations. +func (m *UpdateTeamRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on UpdateTeamRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// UpdateTeamRequestMultiError, or nil if none found. +func (m *UpdateTeamRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *UpdateTeamRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Id + + // no validation rules for Name + + // no validation rules for Description + + // no validation rules for ScopeAll + + if len(errors) > 0 { + return UpdateTeamRequestMultiError(errors) + } + + return nil +} + +// UpdateTeamRequestMultiError is an error wrapping multiple validation errors +// returned by UpdateTeamRequest.ValidateAll() if the designated constraints +// aren't met. +type UpdateTeamRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m UpdateTeamRequestMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m UpdateTeamRequestMultiError) AllErrors() []error { return m } + +// UpdateTeamRequestValidationError is the validation error returned by +// UpdateTeamRequest.Validate if the designated constraints aren't met. +type UpdateTeamRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e UpdateTeamRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e UpdateTeamRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e UpdateTeamRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e UpdateTeamRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e UpdateTeamRequestValidationError) ErrorName() string { + return "UpdateTeamRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e UpdateTeamRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sUpdateTeamRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = UpdateTeamRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = UpdateTeamRequestValidationError{} + +// Validate checks the field values on UpdateTeamResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *UpdateTeamResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on UpdateTeamResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// UpdateTeamResponseMultiError, or nil if none found. +func (m *UpdateTeamResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *UpdateTeamResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if all { + switch v := interface{}(m.GetTeam()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, UpdateTeamResponseValidationError{ + field: "Team", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, UpdateTeamResponseValidationError{ + field: "Team", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetTeam()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return UpdateTeamResponseValidationError{ + field: "Team", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if len(errors) > 0 { + return UpdateTeamResponseMultiError(errors) + } + + return nil +} + +// UpdateTeamResponseMultiError is an error wrapping multiple validation errors +// returned by UpdateTeamResponse.ValidateAll() if the designated constraints +// aren't met. +type UpdateTeamResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m UpdateTeamResponseMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m UpdateTeamResponseMultiError) AllErrors() []error { return m } + +// UpdateTeamResponseValidationError is the validation error returned by +// UpdateTeamResponse.Validate if the designated constraints aren't met. +type UpdateTeamResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e UpdateTeamResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e UpdateTeamResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e UpdateTeamResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e UpdateTeamResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e UpdateTeamResponseValidationError) ErrorName() string { + return "UpdateTeamResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e UpdateTeamResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sUpdateTeamResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = UpdateTeamResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = UpdateTeamResponseValidationError{} + +// Validate checks the field values on DeleteTeamRequest with the rules defined +// in the proto definition for this message. If any rules are violated, the +// first error encountered is returned, or nil if there are no violations. +func (m *DeleteTeamRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on DeleteTeamRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// DeleteTeamRequestMultiError, or nil if none found. +func (m *DeleteTeamRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *DeleteTeamRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Id + + if len(errors) > 0 { + return DeleteTeamRequestMultiError(errors) + } + + return nil +} + +// DeleteTeamRequestMultiError is an error wrapping multiple validation errors +// returned by DeleteTeamRequest.ValidateAll() if the designated constraints +// aren't met. +type DeleteTeamRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m DeleteTeamRequestMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m DeleteTeamRequestMultiError) AllErrors() []error { return m } + +// DeleteTeamRequestValidationError is the validation error returned by +// DeleteTeamRequest.Validate if the designated constraints aren't met. +type DeleteTeamRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e DeleteTeamRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e DeleteTeamRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e DeleteTeamRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e DeleteTeamRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e DeleteTeamRequestValidationError) ErrorName() string { + return "DeleteTeamRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e DeleteTeamRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sDeleteTeamRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = DeleteTeamRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = DeleteTeamRequestValidationError{} + +// Validate checks the field values on DeleteTeamResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *DeleteTeamResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on DeleteTeamResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// DeleteTeamResponseMultiError, or nil if none found. +func (m *DeleteTeamResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *DeleteTeamResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if len(errors) > 0 { + return DeleteTeamResponseMultiError(errors) + } + + return nil +} + +// DeleteTeamResponseMultiError is an error wrapping multiple validation errors +// returned by DeleteTeamResponse.ValidateAll() if the designated constraints +// aren't met. +type DeleteTeamResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m DeleteTeamResponseMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m DeleteTeamResponseMultiError) AllErrors() []error { return m } + +// DeleteTeamResponseValidationError is the validation error returned by +// DeleteTeamResponse.Validate if the designated constraints aren't met. +type DeleteTeamResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e DeleteTeamResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e DeleteTeamResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e DeleteTeamResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e DeleteTeamResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e DeleteTeamResponseValidationError) ErrorName() string { + return "DeleteTeamResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e DeleteTeamResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sDeleteTeamResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = DeleteTeamResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = DeleteTeamResponseValidationError{} + +// Validate checks the field values on ApiKey with the rules defined in the +// proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *ApiKey) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on ApiKey with the rules defined in the +// proto definition for this message. If any rules are violated, the result is +// a list of violation errors wrapped in ApiKeyMultiError, or nil if none found. +func (m *ApiKey) ValidateAll() error { + return m.validate(true) +} + +func (m *ApiKey) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Id + + // no validation rules for Prefix + + // no validation rules for Name + + // no validation rules for TeamId + + // no validation rules for CreatedBy + + if all { + switch v := interface{}(m.GetCreatedAt()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, ApiKeyValidationError{ + field: "CreatedAt", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, ApiKeyValidationError{ + field: "CreatedAt", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetCreatedAt()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return ApiKeyValidationError{ + field: "CreatedAt", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if all { + switch v := interface{}(m.GetExpiresAt()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, ApiKeyValidationError{ + field: "ExpiresAt", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, ApiKeyValidationError{ + field: "ExpiresAt", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetExpiresAt()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return ApiKeyValidationError{ + field: "ExpiresAt", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if all { + switch v := interface{}(m.GetLastUsedAt()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, ApiKeyValidationError{ + field: "LastUsedAt", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, ApiKeyValidationError{ + field: "LastUsedAt", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetLastUsedAt()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return ApiKeyValidationError{ + field: "LastUsedAt", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if all { + switch v := interface{}(m.GetRevokedAt()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, ApiKeyValidationError{ + field: "RevokedAt", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, ApiKeyValidationError{ + field: "RevokedAt", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetRevokedAt()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return ApiKeyValidationError{ + field: "RevokedAt", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if len(errors) > 0 { + return ApiKeyMultiError(errors) + } + + return nil +} + +// ApiKeyMultiError is an error wrapping multiple validation errors returned by +// ApiKey.ValidateAll() if the designated constraints aren't met. +type ApiKeyMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m ApiKeyMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m ApiKeyMultiError) AllErrors() []error { return m } + +// ApiKeyValidationError is the validation error returned by ApiKey.Validate if +// the designated constraints aren't met. +type ApiKeyValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ApiKeyValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ApiKeyValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ApiKeyValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ApiKeyValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ApiKeyValidationError) ErrorName() string { return "ApiKeyValidationError" } + +// Error satisfies the builtin error interface +func (e ApiKeyValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sApiKey.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ApiKeyValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ApiKeyValidationError{} + +// Validate checks the field values on ListApiKeysRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *ListApiKeysRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on ListApiKeysRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// ListApiKeysRequestMultiError, or nil if none found. +func (m *ListApiKeysRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *ListApiKeysRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if len(errors) > 0 { + return ListApiKeysRequestMultiError(errors) + } + + return nil +} + +// ListApiKeysRequestMultiError is an error wrapping multiple validation errors +// returned by ListApiKeysRequest.ValidateAll() if the designated constraints +// aren't met. +type ListApiKeysRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m ListApiKeysRequestMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m ListApiKeysRequestMultiError) AllErrors() []error { return m } + +// ListApiKeysRequestValidationError is the validation error returned by +// ListApiKeysRequest.Validate if the designated constraints aren't met. +type ListApiKeysRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ListApiKeysRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ListApiKeysRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ListApiKeysRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ListApiKeysRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ListApiKeysRequestValidationError) ErrorName() string { + return "ListApiKeysRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e ListApiKeysRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sListApiKeysRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ListApiKeysRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ListApiKeysRequestValidationError{} + +// Validate checks the field values on ListApiKeysResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *ListApiKeysResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on ListApiKeysResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// ListApiKeysResponseMultiError, or nil if none found. +func (m *ListApiKeysResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *ListApiKeysResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + for idx, item := range m.GetApiKeys() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, ListApiKeysResponseValidationError{ + field: fmt.Sprintf("ApiKeys[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, ListApiKeysResponseValidationError{ + field: fmt.Sprintf("ApiKeys[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return ListApiKeysResponseValidationError{ + field: fmt.Sprintf("ApiKeys[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + if len(errors) > 0 { + return ListApiKeysResponseMultiError(errors) + } + + return nil +} + +// ListApiKeysResponseMultiError is an error wrapping multiple validation +// errors returned by ListApiKeysResponse.ValidateAll() if the designated +// constraints aren't met. +type ListApiKeysResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m ListApiKeysResponseMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m ListApiKeysResponseMultiError) AllErrors() []error { return m } + +// ListApiKeysResponseValidationError is the validation error returned by +// ListApiKeysResponse.Validate if the designated constraints aren't met. +type ListApiKeysResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ListApiKeysResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ListApiKeysResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ListApiKeysResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ListApiKeysResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ListApiKeysResponseValidationError) ErrorName() string { + return "ListApiKeysResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e ListApiKeysResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sListApiKeysResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ListApiKeysResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ListApiKeysResponseValidationError{} + +// Validate checks the field values on CreateApiKeyRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *CreateApiKeyRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on CreateApiKeyRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// CreateApiKeyRequestMultiError, or nil if none found. +func (m *CreateApiKeyRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *CreateApiKeyRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Name + + // no validation rules for TeamId + + if all { + switch v := interface{}(m.GetExpiresAt()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, CreateApiKeyRequestValidationError{ + field: "ExpiresAt", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, CreateApiKeyRequestValidationError{ + field: "ExpiresAt", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetExpiresAt()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return CreateApiKeyRequestValidationError{ + field: "ExpiresAt", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if len(errors) > 0 { + return CreateApiKeyRequestMultiError(errors) + } + + return nil +} + +// CreateApiKeyRequestMultiError is an error wrapping multiple validation +// errors returned by CreateApiKeyRequest.ValidateAll() if the designated +// constraints aren't met. +type CreateApiKeyRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m CreateApiKeyRequestMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m CreateApiKeyRequestMultiError) AllErrors() []error { return m } + +// CreateApiKeyRequestValidationError is the validation error returned by +// CreateApiKeyRequest.Validate if the designated constraints aren't met. +type CreateApiKeyRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e CreateApiKeyRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e CreateApiKeyRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e CreateApiKeyRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e CreateApiKeyRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e CreateApiKeyRequestValidationError) ErrorName() string { + return "CreateApiKeyRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e CreateApiKeyRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sCreateApiKeyRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = CreateApiKeyRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = CreateApiKeyRequestValidationError{} + +// Validate checks the field values on CreateApiKeyResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *CreateApiKeyResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on CreateApiKeyResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// CreateApiKeyResponseMultiError, or nil if none found. +func (m *CreateApiKeyResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *CreateApiKeyResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if all { + switch v := interface{}(m.GetApiKey()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, CreateApiKeyResponseValidationError{ + field: "ApiKey", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, CreateApiKeyResponseValidationError{ + field: "ApiKey", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetApiKey()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return CreateApiKeyResponseValidationError{ + field: "ApiKey", + reason: "embedded message failed validation", + cause: err, + } + } + } + + // no validation rules for Secret + + if len(errors) > 0 { + return CreateApiKeyResponseMultiError(errors) + } + + return nil +} + +// CreateApiKeyResponseMultiError is an error wrapping multiple validation +// errors returned by CreateApiKeyResponse.ValidateAll() if the designated +// constraints aren't met. +type CreateApiKeyResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m CreateApiKeyResponseMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m CreateApiKeyResponseMultiError) AllErrors() []error { return m } + +// CreateApiKeyResponseValidationError is the validation error returned by +// CreateApiKeyResponse.Validate if the designated constraints aren't met. +type CreateApiKeyResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e CreateApiKeyResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e CreateApiKeyResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e CreateApiKeyResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e CreateApiKeyResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e CreateApiKeyResponseValidationError) ErrorName() string { + return "CreateApiKeyResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e CreateApiKeyResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sCreateApiKeyResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = CreateApiKeyResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = CreateApiKeyResponseValidationError{} + +// Validate checks the field values on RevokeApiKeyRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *RevokeApiKeyRequest) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on RevokeApiKeyRequest with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// RevokeApiKeyRequestMultiError, or nil if none found. +func (m *RevokeApiKeyRequest) ValidateAll() error { + return m.validate(true) +} + +func (m *RevokeApiKeyRequest) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Id + + if len(errors) > 0 { + return RevokeApiKeyRequestMultiError(errors) + } + + return nil +} + +// RevokeApiKeyRequestMultiError is an error wrapping multiple validation +// errors returned by RevokeApiKeyRequest.ValidateAll() if the designated +// constraints aren't met. +type RevokeApiKeyRequestMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m RevokeApiKeyRequestMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m RevokeApiKeyRequestMultiError) AllErrors() []error { return m } + +// RevokeApiKeyRequestValidationError is the validation error returned by +// RevokeApiKeyRequest.Validate if the designated constraints aren't met. +type RevokeApiKeyRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e RevokeApiKeyRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e RevokeApiKeyRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e RevokeApiKeyRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e RevokeApiKeyRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e RevokeApiKeyRequestValidationError) ErrorName() string { + return "RevokeApiKeyRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e RevokeApiKeyRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sRevokeApiKeyRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = RevokeApiKeyRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = RevokeApiKeyRequestValidationError{} + +// Validate checks the field values on RevokeApiKeyResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *RevokeApiKeyResponse) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on RevokeApiKeyResponse with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// RevokeApiKeyResponseMultiError, or nil if none found. +func (m *RevokeApiKeyResponse) ValidateAll() error { + return m.validate(true) +} + +func (m *RevokeApiKeyResponse) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if len(errors) > 0 { + return RevokeApiKeyResponseMultiError(errors) + } + + return nil +} + +// RevokeApiKeyResponseMultiError is an error wrapping multiple validation +// errors returned by RevokeApiKeyResponse.ValidateAll() if the designated +// constraints aren't met. +type RevokeApiKeyResponseMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m RevokeApiKeyResponseMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m RevokeApiKeyResponseMultiError) AllErrors() []error { return m } + +// RevokeApiKeyResponseValidationError is the validation error returned by +// RevokeApiKeyResponse.Validate if the designated constraints aren't met. +type RevokeApiKeyResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e RevokeApiKeyResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e RevokeApiKeyResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e RevokeApiKeyResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e RevokeApiKeyResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e RevokeApiKeyResponseValidationError) ErrorName() string { + return "RevokeApiKeyResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e RevokeApiKeyResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sRevokeApiKeyResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = RevokeApiKeyResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = RevokeApiKeyResponseValidationError{} diff --git a/generated/proto/auth/v1alpha1/auth_grpc.pb.go b/generated/proto/auth/v1alpha1/auth_grpc.pb.go new file mode 100644 index 00000000..e4d2761b --- /dev/null +++ b/generated/proto/auth/v1alpha1/auth_grpc.pb.go @@ -0,0 +1,547 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: proto/auth/v1alpha1/auth.proto + +package v1alpha1 + +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 ( + AuthService_GetAuthConfig_FullMethodName = "/tracker.auth.v1alpha1.AuthService/GetAuthConfig" + AuthService_Me_FullMethodName = "/tracker.auth.v1alpha1.AuthService/Me" + AuthService_ListUsers_FullMethodName = "/tracker.auth.v1alpha1.AuthService/ListUsers" + AuthService_CreateUser_FullMethodName = "/tracker.auth.v1alpha1.AuthService/CreateUser" + AuthService_UpdateUser_FullMethodName = "/tracker.auth.v1alpha1.AuthService/UpdateUser" + AuthService_ListTeams_FullMethodName = "/tracker.auth.v1alpha1.AuthService/ListTeams" + AuthService_CreateTeam_FullMethodName = "/tracker.auth.v1alpha1.AuthService/CreateTeam" + AuthService_UpdateTeam_FullMethodName = "/tracker.auth.v1alpha1.AuthService/UpdateTeam" + AuthService_DeleteTeam_FullMethodName = "/tracker.auth.v1alpha1.AuthService/DeleteTeam" + AuthService_ListApiKeys_FullMethodName = "/tracker.auth.v1alpha1.AuthService/ListApiKeys" + AuthService_CreateApiKey_FullMethodName = "/tracker.auth.v1alpha1.AuthService/CreateApiKey" + AuthService_RevokeApiKey_FullMethodName = "/tracker.auth.v1alpha1.AuthService/RevokeApiKey" +) + +// AuthServiceClient is the client API for AuthService 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. +// +// AuthService exposes the current identity and the administration of users, +// teams and API keys. Login, logout and password change are plain HTTP +// handlers because they set cookies. +type AuthServiceClient interface { + GetAuthConfig(ctx context.Context, in *GetAuthConfigRequest, opts ...grpc.CallOption) (*GetAuthConfigResponse, error) + Me(ctx context.Context, in *MeRequest, opts ...grpc.CallOption) (*MeResponse, error) + ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) + CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserResponse, error) + UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*UpdateUserResponse, error) + ListTeams(ctx context.Context, in *ListTeamsRequest, opts ...grpc.CallOption) (*ListTeamsResponse, error) + CreateTeam(ctx context.Context, in *CreateTeamRequest, opts ...grpc.CallOption) (*CreateTeamResponse, error) + UpdateTeam(ctx context.Context, in *UpdateTeamRequest, opts ...grpc.CallOption) (*UpdateTeamResponse, error) + DeleteTeam(ctx context.Context, in *DeleteTeamRequest, opts ...grpc.CallOption) (*DeleteTeamResponse, error) + ListApiKeys(ctx context.Context, in *ListApiKeysRequest, opts ...grpc.CallOption) (*ListApiKeysResponse, error) + CreateApiKey(ctx context.Context, in *CreateApiKeyRequest, opts ...grpc.CallOption) (*CreateApiKeyResponse, error) + RevokeApiKey(ctx context.Context, in *RevokeApiKeyRequest, opts ...grpc.CallOption) (*RevokeApiKeyResponse, error) +} + +type authServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewAuthServiceClient(cc grpc.ClientConnInterface) AuthServiceClient { + return &authServiceClient{cc} +} + +func (c *authServiceClient) GetAuthConfig(ctx context.Context, in *GetAuthConfigRequest, opts ...grpc.CallOption) (*GetAuthConfigResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetAuthConfigResponse) + err := c.cc.Invoke(ctx, AuthService_GetAuthConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) Me(ctx context.Context, in *MeRequest, opts ...grpc.CallOption) (*MeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MeResponse) + err := c.cc.Invoke(ctx, AuthService_Me_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListUsersResponse) + err := c.cc.Invoke(ctx, AuthService_ListUsers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateUserResponse) + err := c.cc.Invoke(ctx, AuthService_CreateUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*UpdateUserResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateUserResponse) + err := c.cc.Invoke(ctx, AuthService_UpdateUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) ListTeams(ctx context.Context, in *ListTeamsRequest, opts ...grpc.CallOption) (*ListTeamsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListTeamsResponse) + err := c.cc.Invoke(ctx, AuthService_ListTeams_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) CreateTeam(ctx context.Context, in *CreateTeamRequest, opts ...grpc.CallOption) (*CreateTeamResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateTeamResponse) + err := c.cc.Invoke(ctx, AuthService_CreateTeam_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) UpdateTeam(ctx context.Context, in *UpdateTeamRequest, opts ...grpc.CallOption) (*UpdateTeamResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateTeamResponse) + err := c.cc.Invoke(ctx, AuthService_UpdateTeam_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) DeleteTeam(ctx context.Context, in *DeleteTeamRequest, opts ...grpc.CallOption) (*DeleteTeamResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteTeamResponse) + err := c.cc.Invoke(ctx, AuthService_DeleteTeam_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) ListApiKeys(ctx context.Context, in *ListApiKeysRequest, opts ...grpc.CallOption) (*ListApiKeysResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListApiKeysResponse) + err := c.cc.Invoke(ctx, AuthService_ListApiKeys_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) CreateApiKey(ctx context.Context, in *CreateApiKeyRequest, opts ...grpc.CallOption) (*CreateApiKeyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateApiKeyResponse) + err := c.cc.Invoke(ctx, AuthService_CreateApiKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) RevokeApiKey(ctx context.Context, in *RevokeApiKeyRequest, opts ...grpc.CallOption) (*RevokeApiKeyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RevokeApiKeyResponse) + err := c.cc.Invoke(ctx, AuthService_RevokeApiKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AuthServiceServer is the server API for AuthService service. +// All implementations must embed UnimplementedAuthServiceServer +// for forward compatibility. +// +// AuthService exposes the current identity and the administration of users, +// teams and API keys. Login, logout and password change are plain HTTP +// handlers because they set cookies. +type AuthServiceServer interface { + GetAuthConfig(context.Context, *GetAuthConfigRequest) (*GetAuthConfigResponse, error) + Me(context.Context, *MeRequest) (*MeResponse, error) + ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error) + CreateUser(context.Context, *CreateUserRequest) (*CreateUserResponse, error) + UpdateUser(context.Context, *UpdateUserRequest) (*UpdateUserResponse, error) + ListTeams(context.Context, *ListTeamsRequest) (*ListTeamsResponse, error) + CreateTeam(context.Context, *CreateTeamRequest) (*CreateTeamResponse, error) + UpdateTeam(context.Context, *UpdateTeamRequest) (*UpdateTeamResponse, error) + DeleteTeam(context.Context, *DeleteTeamRequest) (*DeleteTeamResponse, error) + ListApiKeys(context.Context, *ListApiKeysRequest) (*ListApiKeysResponse, error) + CreateApiKey(context.Context, *CreateApiKeyRequest) (*CreateApiKeyResponse, error) + RevokeApiKey(context.Context, *RevokeApiKeyRequest) (*RevokeApiKeyResponse, error) + mustEmbedUnimplementedAuthServiceServer() +} + +// UnimplementedAuthServiceServer 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 UnimplementedAuthServiceServer struct{} + +func (UnimplementedAuthServiceServer) GetAuthConfig(context.Context, *GetAuthConfigRequest) (*GetAuthConfigResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetAuthConfig not implemented") +} +func (UnimplementedAuthServiceServer) Me(context.Context, *MeRequest) (*MeResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Me not implemented") +} +func (UnimplementedAuthServiceServer) ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListUsers not implemented") +} +func (UnimplementedAuthServiceServer) CreateUser(context.Context, *CreateUserRequest) (*CreateUserResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented") +} +func (UnimplementedAuthServiceServer) UpdateUser(context.Context, *UpdateUserRequest) (*UpdateUserResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateUser not implemented") +} +func (UnimplementedAuthServiceServer) ListTeams(context.Context, *ListTeamsRequest) (*ListTeamsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListTeams not implemented") +} +func (UnimplementedAuthServiceServer) CreateTeam(context.Context, *CreateTeamRequest) (*CreateTeamResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateTeam not implemented") +} +func (UnimplementedAuthServiceServer) UpdateTeam(context.Context, *UpdateTeamRequest) (*UpdateTeamResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateTeam not implemented") +} +func (UnimplementedAuthServiceServer) DeleteTeam(context.Context, *DeleteTeamRequest) (*DeleteTeamResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteTeam not implemented") +} +func (UnimplementedAuthServiceServer) ListApiKeys(context.Context, *ListApiKeysRequest) (*ListApiKeysResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListApiKeys not implemented") +} +func (UnimplementedAuthServiceServer) CreateApiKey(context.Context, *CreateApiKeyRequest) (*CreateApiKeyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateApiKey not implemented") +} +func (UnimplementedAuthServiceServer) RevokeApiKey(context.Context, *RevokeApiKeyRequest) (*RevokeApiKeyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RevokeApiKey not implemented") +} +func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {} +func (UnimplementedAuthServiceServer) testEmbeddedByValue() {} + +// UnsafeAuthServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AuthServiceServer will +// result in compilation errors. +type UnsafeAuthServiceServer interface { + mustEmbedUnimplementedAuthServiceServer() +} + +func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) { + // If the following call pancis, it indicates UnimplementedAuthServiceServer 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(&AuthService_ServiceDesc, srv) +} + +func _AuthService_GetAuthConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAuthConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).GetAuthConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_GetAuthConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).GetAuthConfig(ctx, req.(*GetAuthConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_Me_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).Me(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_Me_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).Me(ctx, req.(*MeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_ListUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListUsersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).ListUsers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_ListUsers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).ListUsers(ctx, req.(*ListUsersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_CreateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).CreateUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_CreateUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).CreateUser(ctx, req.(*CreateUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_UpdateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).UpdateUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_UpdateUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).UpdateUser(ctx, req.(*UpdateUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_ListTeams_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListTeamsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).ListTeams(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_ListTeams_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).ListTeams(ctx, req.(*ListTeamsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_CreateTeam_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateTeamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).CreateTeam(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_CreateTeam_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).CreateTeam(ctx, req.(*CreateTeamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_UpdateTeam_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateTeamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).UpdateTeam(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_UpdateTeam_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).UpdateTeam(ctx, req.(*UpdateTeamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_DeleteTeam_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteTeamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).DeleteTeam(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_DeleteTeam_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).DeleteTeam(ctx, req.(*DeleteTeamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_ListApiKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListApiKeysRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).ListApiKeys(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_ListApiKeys_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).ListApiKeys(ctx, req.(*ListApiKeysRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_CreateApiKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateApiKeyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).CreateApiKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_CreateApiKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).CreateApiKey(ctx, req.(*CreateApiKeyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_RevokeApiKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RevokeApiKeyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).RevokeApiKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_RevokeApiKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).RevokeApiKey(ctx, req.(*RevokeApiKeyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var AuthService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "tracker.auth.v1alpha1.AuthService", + HandlerType: (*AuthServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetAuthConfig", + Handler: _AuthService_GetAuthConfig_Handler, + }, + { + MethodName: "Me", + Handler: _AuthService_Me_Handler, + }, + { + MethodName: "ListUsers", + Handler: _AuthService_ListUsers_Handler, + }, + { + MethodName: "CreateUser", + Handler: _AuthService_CreateUser_Handler, + }, + { + MethodName: "UpdateUser", + Handler: _AuthService_UpdateUser_Handler, + }, + { + MethodName: "ListTeams", + Handler: _AuthService_ListTeams_Handler, + }, + { + MethodName: "CreateTeam", + Handler: _AuthService_CreateTeam_Handler, + }, + { + MethodName: "UpdateTeam", + Handler: _AuthService_UpdateTeam_Handler, + }, + { + MethodName: "DeleteTeam", + Handler: _AuthService_DeleteTeam_Handler, + }, + { + MethodName: "ListApiKeys", + Handler: _AuthService_ListApiKeys_Handler, + }, + { + MethodName: "CreateApiKey", + Handler: _AuthService_CreateApiKey_Handler, + }, + { + MethodName: "RevokeApiKey", + Handler: _AuthService_RevokeApiKey_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/auth/v1alpha1/auth.proto", +} diff --git a/generated/proto/event/v1alpha1/event.pb.go b/generated/proto/event/v1alpha1/event.pb.go index ac08a9ff..85bddc27 100644 --- a/generated/proto/event/v1alpha1/event.pb.go +++ b/generated/proto/event/v1alpha1/event.pb.go @@ -2543,7 +2543,7 @@ const file_proto_event_v1alpha1_event_proto_rawDesc = "" + "\x02P2\x10\x02\x12\x06\n" + "\x02P3\x10\x03\x12\x06\n" + "\x02P4\x10\x04\x12\x06\n" + - "\x02P5\x10\x05*\xcd\x01\n" + + "\x02P5\x10\x05*\xe3\x01\n" + "\x06Status\x12\x16\n" + "\x12STATUS_UNSPECIFIED\x10\x00\x12\t\n" + "\x05start\x10\x01\x12\v\n" + @@ -2559,7 +2559,8 @@ const file_proto_event_v1alpha1_event_proto_rawDesc = "" + "\x12\b\n" + "\x04done\x10\v\x12\x0f\n" + "\vin_progress\x10\f\x12\v\n" + - "\aplanned\x10\r*\x97\x01\n" + + "\aplanned\x10\r\x12\x14\n" + + "\x10waiting_approval\x10\x0e*\x97\x01\n" + "\vEnvironment\x12\x1b\n" + "\x17ENVIRONMENT_UNSPECIFIED\x10\x00\x12\x0f\n" + "\vdevelopment\x10\x01\x12\x0f\n" + diff --git a/go.mod b/go.mod index 84eaa1dc..3ea86349 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,9 @@ go 1.26.1 require ( github.com/go-openapi/runtime v0.29.5 - google.golang.org/grpc v1.79.3 + github.com/golang-jwt/jwt/v5 v5.3.0 + golang.org/x/crypto v0.56.0 + google.golang.org/grpc v1.83.2 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) @@ -35,6 +37,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.7 // indirect github.com/kr/text v0.2.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect @@ -49,8 +52,7 @@ require ( github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.53.0 // indirect - golang.org/x/sync v0.21.0 // indirect + golang.org/x/sync v0.22.0 // indirect ) require ( @@ -61,9 +63,9 @@ require ( github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 go.mongodb.org/mongo-driver v1.17.9 - golang.org/x/net v0.56.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/text v0.38.0 // indirect + golang.org/x/net v0.58.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d // indirect ) diff --git a/go.sum b/go.sum index 577f1fda..552d4208 100644 --- a/go.sum +++ b/go.sum @@ -12,87 +12,52 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/analysis v0.25.0 h1:EnjAq1yO8wEO9HbPmY8vLPEIkdZuuFhCAKBPvCB7bCs= -github.com/go-openapi/analysis v0.25.0/go.mod h1:5WFTRE43WLkPG9r9OtlMfqkkvUTYLVVCIxLlEpyF8kE= github.com/go-openapi/analysis v0.25.3 h1:4zlcg85pd2xq3sEgjW887n1IpwCpCqTmqeT6dP9OxDw= github.com/go-openapi/analysis v0.25.3/go.mod h1:6PEmUIra9/rn6SPstzbrMkhFAsMB2qm7g6E+4DRFyCU= -github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= -github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= github.com/go-openapi/errors v0.22.8 h1:oP7sW7TWc3wFFjrzzj0nI83H2qMBkNjNfSd+XRejk/I= github.com/go-openapi/errors v0.22.8/go.mod h1:BuUoHcYrU6E7V9gfj1I5wLQqgtIHnup/alXZ8KdgQ0w= -github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= -github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= github.com/go-openapi/jsonpointer v0.23.2 h1:DK7R/3zAt4xTytxNkw7jARGPFI7rkaSsii58n8X45x0= github.com/go-openapi/jsonpointer v0.23.2/go.mod h1:noUOckXtq7b4bVkqw0sbHKieq9uEZRN7p6EF/dalc4w= -github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= -github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y= github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY= -github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= -github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= github.com/go-openapi/loads v0.23.4 h1:UMC8JClHQeASS+bh1Uc8ShGG6IrKt1kbM2DgFhx/vF0= github.com/go-openapi/loads v0.23.4/go.mod h1:oXw5oD+IGqI5BdfQgN7y9OXR8JhsAfEDpwWKxpGzeno= github.com/go-openapi/runtime v0.29.5 h1:uc5+/TtqLIfDBTUxnF3uppoGMt+9DzonwUWsviINlrY= github.com/go-openapi/runtime v0.29.5/go.mod h1:D9IUbWccdYv+km8QwmAm90FZvDcQk47vP2Y7y5as/D8= -github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= -github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= github.com/go-openapi/spec v0.22.6 h1:Tyy1pLaNCM8GBCFLoGYLonjJi6zykqyLCjXLc19ZPic= github.com/go-openapi/spec v0.22.6/go.mod h1:HZvTHat+iH0PALQRWhrqIHtU/PEqxqd89fu0MxGlMeM= -github.com/go-openapi/strfmt v0.26.2 h1:ysjheCh4i1rmFEo2LanhELDNucNzfWTZhUDKgWWPaFM= -github.com/go-openapi/strfmt v0.26.2/go.mod h1:fXh1e449cyUn2NYuz+wb3wARBUdMl7qPEZwX00nqivY= github.com/go-openapi/strfmt v0.26.4 h1:yI6IAEfcWow459BD5UzFY430KUwXZwBHrYusPFkhWlc= github.com/go-openapi/strfmt v0.26.4/go.mod h1:hNJi6nb5ETD6i7A1yRo03M9S6ZoTPPoWff1iUexmfUc= -github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= -github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= github.com/go-openapi/swag/conv v0.26.1 h1:slr5FVkg9Wc3Y5zcwenD8Sd/PQ94b2I/QJI7N7KTBpg= github.com/go-openapi/swag/conv v0.26.1/go.mod h1:mvQXgPptZk9GTrFgGwWvT4q+dN+zQej9JfmGwnipz1A= -github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU= -github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc= github.com/go-openapi/swag/fileutils v0.26.1 h1:K1XCM2CGhfNsc6YDt6v7Q5+1e59rftYWdcu/isZhvFw= github.com/go-openapi/swag/fileutils v0.26.1/go.mod h1:mYUgxQAKX4ShS3qvvySx+/9yrlUnDhjiD1CalaQl8lQ= -github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= -github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE= github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc= -github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= -github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= github.com/go-openapi/swag/jsonutils v0.26.1 h1:2hdBfFkHg+7Wrz2VsCbeyR6hzkRDs7AztnMR2u84yOY= github.com/go-openapi/swag/jsonutils v0.26.1/go.mod h1:U+RMJH3wa+6BRiphuRtIyI8fW9HPFqFQ4sHk2oRx0UQ= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1 h1:1CD7NiLLb/TXl3tOnFYU4b+mNfb5rtgHkaA+q7RMYYQ= -github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= -github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1/go.mod h1:ZWafc8nMdYzTE3uYY6W86f0n46+IF0g4uUyRhJw/kXc= github.com/go-openapi/swag/loading v0.26.1 h1:E9K4wqXeROlhjFQ13K9zMz6ojFGXIggGe+ad1odrK9w= github.com/go-openapi/swag/loading v0.26.1/go.mod h1:3qvRIlWzWdq1HvmldwmuJ2ohpcAryN6xVt2OTKd0/7E= -github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= -github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= github.com/go-openapi/swag/mangling v0.26.1 h1:gpYI4WuPKFJJVjV5cDLGlDVJhFIxYjQc7yN5eEb4CqM= github.com/go-openapi/swag/mangling v0.26.1/go.mod h1:POETDH01hqAdASXfw7ISEd9bCOE6xBHOt8NHmGZRmYM= -github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= -github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= github.com/go-openapi/swag/stringutils v0.26.1 h1:f88uYyTso7TnHrKM/bUBsQ5e2wKf37cpgo6pvbzd9yU= github.com/go-openapi/swag/stringutils v0.26.1/go.mod h1:Sc6d3bU8fgk5AyZR8/8jEQ+Is/Ald+TD/IIggPN8UJk= -github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= -github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= github.com/go-openapi/swag/typeutils v0.26.1 h1:yg42FgMzRR6PVQ3M3qHz1s+Y6/P4HoJ3cBarXa3OVnU= github.com/go-openapi/swag/typeutils v0.26.1/go.mod h1:VfnV+oUtSP2vCSCn2aJgnr8OevUYemyIzzS1VOzS10o= -github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= -github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= github.com/go-openapi/swag/yamlutils v0.26.1 h1:0TSLK+lXs9vfIhAWzBeI/lOzEnIoot6WTCO1aAeWFTk= github.com/go-openapi/swag/yamlutils v0.26.1/go.mod h1:7W5b7PRX9MxwL7TjeG7H8HkyBGRsIDRObhyMWFgBI2M= -github.com/go-openapi/testify/enable/yaml/v2 v2.5.0 h1:3hZD1fwydvCx/cc1R2uYNQirHqf2s6lqpKV3FcNTURA= -github.com/go-openapi/testify/enable/yaml/v2 v2.5.0/go.mod h1:TvDZKBH7ZbMaF3EqH2AwTvNQCmzyZq8K1agRjf1B+Nk= github.com/go-openapi/testify/enable/yaml/v2 v2.5.1 h1:q9NtHwK4qHF7yZziBPvZyv7zWAIk8ok88Gh2mR6Jpc8= -github.com/go-openapi/testify/v2 v2.5.0 h1:UOCr63aAsMIDydZbZGqo5Ev01D4eydItRbekDuZMJLw= -github.com/go-openapi/testify/v2 v2.5.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-openapi/testify/enable/yaml/v2 v2.5.1/go.mod h1:JW0MXIotCYps/XsgJnG3a8Q7rE5xAiBwoOD5OfaIQBk= github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= -github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= -github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= +github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-openapi/validate v0.25.3 h1:4nzAIavcJ7WveHK2+V1UAkZK3kWcjzxZCzjfZAfavKs= github.com/go-openapi/validate v0.25.3/go.mod h1:GemfuGMyYpIaBoKpX3z8sLywrmxpzWVOoJ7R0VeAVuk= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= @@ -101,14 +66,10 @@ 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 h1:kEISI/Gx67NzH3nJxAmY/dGac80kKZgZt134u7Y/k1s= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4/go.mod h1:6Nz966r3vQYCqIzWsuEl9d7cf7mRhtDmm++sOxlnfxI= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.8 h1:NpbJl/eVbvrGE0MJ6X16X9SAifesl6Fwxg/YmCvubRI= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.8/go.mod h1:mi7YA+gCzVem12exXy46ZespvGtX/lZmD/RLnQhVW7U= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= -github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw= github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -130,8 +91,6 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= -github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= @@ -159,82 +118,63 @@ go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5 go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= +golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E= -google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d h1:xr2lwHI91bn3UiXcnyzRMQjp2LRiM8wEHzwUaE0YhTs= google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d/go.mod h1:O0ZOWSrfWfJ+Z5HbwZ+wNtHsg/vk1k2C/w67eww8PfQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d h1:mpAgMyM9vQHxycBlDq50y1VHpfSfVwzXvrQKtYbXuUY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= 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= diff --git a/internal/auth/apikey.go b/internal/auth/apikey.go new file mode 100644 index 00000000..65c785c9 --- /dev/null +++ b/internal/auth/apikey.go @@ -0,0 +1,74 @@ +package auth + +import ( + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "encoding/hex" + "fmt" + "math/big" + "strings" +) + +const ( + apiKeyMarker = "trk_" + apiKeyPrefixLen = 8 + apiKeySecretLen = 32 + apiKeyAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789" +) + +// GeneratedAPIKey is a freshly created key. Secret is shown once to the user, +// only Prefix and Hash are persisted. +type GeneratedAPIKey struct { + Secret string + Prefix string + Hash string +} + +// GenerateAPIKey creates a key formatted as trk__. +func GenerateAPIKey() (GeneratedAPIKey, error) { + prefix := make([]byte, apiKeyPrefixLen) + for i := range prefix { + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(apiKeyAlphabet)))) + if err != nil { + return GeneratedAPIKey{}, fmt.Errorf("generate prefix: %w", err) + } + prefix[i] = apiKeyAlphabet[n.Int64()] + } + raw := make([]byte, apiKeySecretLen) + if _, err := rand.Read(raw); err != nil { + return GeneratedAPIKey{}, fmt.Errorf("generate secret: %w", err) + } + secret := apiKeyMarker + string(prefix) + "_" + base64.RawURLEncoding.EncodeToString(raw) + return GeneratedAPIKey{Secret: secret, Prefix: string(prefix), Hash: HashAPIKey(secret)}, nil +} + +// IsAPIKey reports whether the token looks like an API key rather than a session token. +func IsAPIKey(token string) bool { + return strings.HasPrefix(token, apiKeyMarker) +} + +// ParseAPIKeyPrefix extracts the lookup prefix from a full secret. +func ParseAPIKeyPrefix(secret string) (string, bool) { + if !IsAPIKey(secret) { + return "", false + } + rest := secret[len(apiKeyMarker):] + sep := strings.IndexByte(rest, '_') + if sep != apiKeyPrefixLen || len(rest) <= sep+1 { + return "", false + } + return rest[:sep], true +} + +// HashAPIKey returns the hex SHA-256 of the full secret. +func HashAPIKey(secret string) string { + sum := sha256.Sum256([]byte(secret)) + return hex.EncodeToString(sum[:]) +} + +// APIKeyMatches compares a stored hash with a presented secret in constant time. +func APIKeyMatches(hash, secret string) bool { + return subtle.ConstantTimeCompare([]byte(hash), []byte(HashAPIKey(secret))) == 1 +} diff --git a/internal/auth/apikey_test.go b/internal/auth/apikey_test.go new file mode 100644 index 00000000..d7172def --- /dev/null +++ b/internal/auth/apikey_test.go @@ -0,0 +1,36 @@ +package auth + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateAPIKey(t *testing.T) { + k, err := GenerateAPIKey() + require.NoError(t, err) + assert.Regexp(t, regexp.MustCompile(`^trk_[a-z0-9]{8}_[A-Za-z0-9_-]{43}$`), k.Secret) + assert.Len(t, k.Prefix, 8) + assert.Equal(t, HashAPIKey(k.Secret), k.Hash) + assert.True(t, APIKeyMatches(k.Hash, k.Secret)) + assert.False(t, APIKeyMatches(k.Hash, k.Secret+"x")) + + prefix, ok := ParseAPIKeyPrefix(k.Secret) + assert.True(t, ok) + assert.Equal(t, k.Prefix, prefix) + + other, err := GenerateAPIKey() + require.NoError(t, err) + assert.NotEqual(t, k.Secret, other.Secret) +} + +func TestParseAPIKeyPrefixRejectsMalformed(t *testing.T) { + for _, s := range []string{"", "trk_", "trk_short_abc", "trk_abcdefgh", "trk_abcdefgh_", "abc_abcdefgh_secret", "eyJhbGciOi"} { + _, ok := ParseAPIKeyPrefix(s) + assert.False(t, ok, s) + } + assert.True(t, IsAPIKey("trk_anything")) + assert.False(t, IsAPIKey("eyJhbGciOi")) +} diff --git a/internal/auth/authz/authz.go b/internal/auth/authz/authz.go new file mode 100644 index 00000000..586810a4 --- /dev/null +++ b/internal/auth/authz/authz.go @@ -0,0 +1,154 @@ +// Package authz decides whether a principal may call a method. It is the only +// place where permissions are checked, whatever the transport. +package authz + +import ( + "context" + "log/slog" + "net/http" + + "github.com/bananaops/tracker/internal/auth" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var authRequests = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "tracker_auth_requests_total", + Help: "Authorization decisions by principal kind and result", + }, + []string{"principal", "result"}, +) + +// AuthLogins counts login attempts. The method label names the authentication +// method (local for now, oidc once it lands) and the result label is one of +// LoginSuccess, LoginFailure or LoginRateLimited. Malformed bodies, cross-site +// refusals and internal errors are not login attempts and are not counted. +// It is exported so the login handler, which lives in the server package, can +// increment it and tests can read it back. +var AuthLogins = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "tracker_auth_logins_total", + Help: "Login attempts by authentication method and result", + }, + []string{"method", "result"}, +) + +// Values of the AuthLogins labels. +const ( + LoginMethodLocal = "local" + LoginSuccess = "success" + LoginFailure = "failure" + LoginRateLimited = "rate_limited" +) + +func init() { + prometheus.MustRegister(authRequests, AuthLogins) +} + +// MethodFromContext returns the full RPC method name, on gRPC or through the +// gateway. runtime.RPCMethod is tried first: the generated *.pb.gw.go code +// (HandlerServer mode) injects a dummy runtime.ServerTransportStream into the +// context purely to let grpc.SendHeader/SetTrailer work outside of a real +// gRPC server, and that stream's Method() always returns "". Since +// grpc.Method(ctx) reports ok=true as soon as any transport stream is +// present, checking it first would silently treat every gateway request as +// having no method, denying it as unauthorized. grpc.Method is used only as +// a fallback, and only when it actually returns a non-empty name. +func MethodFromContext(ctx context.Context) string { + if m, ok := runtime.RPCMethod(ctx); ok { + return m + } + if m, ok := grpc.Method(ctx); ok && m != "" { + return m + } + return "" +} + +// Authorize checks the current principal against the current RPC method. +// Call it as the first statement of every service method. +func Authorize(ctx context.Context) error { + p, ok := auth.FromContext(ctx) + if !ok { + p = auth.Anonymous(nil) + } + method := MethodFromContext(ctx) + err := Check(p, method) + observe(p, method, err) + return err +} + +// Check is the pure decision for a principal and a method. +func Check(p auth.Principal, method string) error { + perm, ok := MethodPermissions[method] + if !ok { + slog.Error("authz: method not in permission table, denying", "method", method) + return status.Error(codes.PermissionDenied, "method not authorized") + } + return CheckPermission(p, perm) +} + +// CheckPermission is the pure decision for a principal and a permission. +func CheckPermission(p auth.Principal, perm auth.Permission) error { + // A credential that was presented and refused loses even public routes. + // The caller asked to be identified and could not be, so it must hear + // about it rather than be quietly served as an anonymous visitor: see + // auth.RejectedCredential. + if p.CredentialRejected { + return status.Error(codes.Unauthenticated, "invalid or expired credentials") + } + switch perm { + case auth.PermPublic: + return nil + case auth.PermAuthenticated: + if p.IsAuthenticated() { + return nil + } + return status.Error(codes.Unauthenticated, "authentication required") + } + if p.Has(perm) { + return nil + } + if !p.IsAuthenticated() { + return status.Error(codes.Unauthenticated, "authentication required") + } + return status.Errorf(codes.PermissionDenied, "permission %s required", perm) +} + +// RequireHTTP guards a grpc-gateway custom handler with a permission. +func RequireHTTP(perm auth.Permission, next runtime.HandlerFunc) runtime.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request, params map[string]string) { + p, ok := auth.FromContext(r.Context()) + if !ok { + p = auth.Anonymous(nil) + } + err := CheckPermission(p, perm) + observe(p, r.Method+" "+r.URL.Path, err) + if err != nil { + code := http.StatusForbidden + if status.Code(err) == codes.Unauthenticated { + code = http.StatusUnauthorized + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + _, _ = w.Write([]byte(`{"error":"` + status.Convert(err).Message() + `"}`)) + return + } + next(w, r, params) + } +} + +func observe(p auth.Principal, method string, err error) { + result := "allowed" + if err != nil { + result = "denied" + if status.Code(err) == codes.Unauthenticated { + result = "unauthenticated" + } + slog.Warn("authz denied", "method", method, "principal", p.Username, "kind", p.Kind, "reason", status.Convert(err).Message()) + } + authRequests.WithLabelValues(string(p.Kind), result).Inc() +} diff --git a/internal/auth/authz/authz_test.go b/internal/auth/authz/authz_test.go new file mode 100644 index 00000000..709be2cc --- /dev/null +++ b/internal/auth/authz/authz_test.go @@ -0,0 +1,142 @@ +package authz + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/bananaops/tracker/internal/auth" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +const listEvents = "/tracker.event.v1alpha1.EventService/ListEvents" + +const getAuthConfig = "/tracker.auth.v1alpha1.AuthService/GetAuthConfig" + +// fakeTransportStream is a minimal grpc.ServerTransportStream, as used by a +// real gRPC server for a direct (non-gateway) call. +type fakeTransportStream struct{ method string } + +func (s fakeTransportStream) Method() string { return s.method } +func (fakeTransportStream) SetHeader(metadata.MD) error { return nil } +func (fakeTransportStream) SendHeader(metadata.MD) error { return nil } +func (fakeTransportStream) SetTrailer(metadata.MD) error { return nil } + +// TestMethodFromContextThroughGRPC covers a direct gRPC call: the real +// transport stream reports the real method name. +func TestMethodFromContextThroughGRPC(t *testing.T) { + ctx := grpc.NewContextWithServerTransportStream(context.Background(), fakeTransportStream{method: listEvents}) + assert.Equal(t, listEvents, MethodFromContext(ctx)) +} + +// TestMethodFromContextThroughGateway mirrors exactly what the generated +// *.pb.gw.go code does in HandlerServer mode (see for example +// RegisterAuthServiceHandlerServer in generated/proto/auth/v1alpha1/auth.pb.gw.go): +// it injects a dummy runtime.ServerTransportStream purely to let +// grpc.SendHeader/SetTrailer work outside of a real gRPC server, then +// annotates the context with the real RPC method name via +// runtime.AnnotateIncomingContext. runtime.ServerTransportStream.Method() +// always returns "", so grpc.Method(ctx) must not be trusted just because it +// reports ok=true. +func TestMethodFromContextThroughGateway(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/v1alpha1/auth/config", nil) + ctx := grpc.NewContextWithServerTransportStream(context.Background(), &runtime.ServerTransportStream{}) + mux := runtime.NewServeMux() + annotated, err := runtime.AnnotateIncomingContext(ctx, mux, req, getAuthConfig, runtime.WithHTTPPathPattern("/api/v1alpha1/auth/config")) + require.NoError(t, err) + + assert.Equal(t, getAuthConfig, MethodFromContext(annotated)) + + err = Authorize(auth.WithPrincipal(annotated, auth.Anonymous(nil))) + assert.NoError(t, err, "GetAuthConfig is public") +} + +func TestCheck(t *testing.T) { + anon := auth.Anonymous(nil) + reader := auth.Principal{Kind: auth.KindUser, Username: "r", Permissions: auth.NewPermissionSet(auth.PermEventRead)} + + assert.Equal(t, codes.Unauthenticated, status.Code(Check(anon, listEvents))) + assert.NoError(t, Check(reader, listEvents)) + assert.Equal(t, codes.PermissionDenied, status.Code(Check(reader, "/tracker.event.v1alpha1.EventService/CreateEvent"))) + assert.Equal(t, codes.PermissionDenied, status.Code(Check(reader, "/tracker.nope.v1/Svc/Method")), "unmapped method is denied") + + anonReader := auth.Anonymous([]auth.Permission{auth.PermEventRead}) + assert.NoError(t, Check(anonReader, listEvents)) + + assert.NoError(t, Check(anon, "/tracker.auth.v1alpha1.AuthService/GetAuthConfig"), "public") + assert.NoError(t, Check(anon, "/tracker.auth.v1alpha1.AuthService/Me"), "public") + assert.Equal(t, codes.Unauthenticated, status.Code(Check(anon, "/tracker.auth.v1alpha1.AuthService/ListUsers"))) + assert.Equal(t, codes.PermissionDenied, status.Code(Check(reader, "/tracker.auth.v1alpha1.AuthService/ListUsers"))) +} + +func TestAuthorizeWithoutPrincipalIsAnonymous(t *testing.T) { + err := Authorize(context.Background()) + assert.Equal(t, codes.PermissionDenied, status.Code(err), "no method in context: unmapped, denied") +} + +func TestRequireHTTP(t *testing.T) { + called := false + h := RequireHTTP(auth.PermLinksWrite, func(w http.ResponseWriter, r *http.Request, _ map[string]string) { called = true }) + + rec := httptest.NewRecorder() + h(rec, httptest.NewRequest(http.MethodPost, "/api/links", nil), nil) + assert.Equal(t, http.StatusUnauthorized, rec.Code) + assert.False(t, called) + + ctx := auth.WithPrincipal(context.Background(), auth.Principal{Kind: auth.KindUser, Permissions: auth.NewPermissionSet(auth.PermLinksRead)}) + rec = httptest.NewRecorder() + h(rec, httptest.NewRequest(http.MethodPost, "/api/links", nil).WithContext(ctx), nil) + assert.Equal(t, http.StatusForbidden, rec.Code) + assert.False(t, called) + + ctx = auth.WithPrincipal(context.Background(), auth.Principal{Kind: auth.KindUser, Permissions: auth.NewPermissionSet(auth.PermLinksWrite)}) + rec = httptest.NewRecorder() + h(rec, httptest.NewRequest(http.MethodPost, "/api/links", nil).WithContext(ctx), nil) + assert.Equal(t, http.StatusOK, rec.Code) + assert.True(t, called) +} + +// A credential that was presented and refused is denied everywhere, including +// on the public routes and on permissions the anonymous principal would hold. +// Otherwise revoking a narrow API key would widen it to +// AUTH_ANONYMOUS_PERMISSIONS instead of shutting it down. +func TestRejectedCredentialIsAlwaysUnauthenticated(t *testing.T) { + rejected := auth.RejectedCredential() + + for _, perm := range []auth.Permission{auth.PermPublic, auth.PermAuthenticated, auth.PermEventRead} { + err := CheckPermission(rejected, perm) + assert.Equal(t, codes.Unauthenticated, status.Code(err), "permission %s", perm) + } + + assert.Equal(t, codes.Unauthenticated, status.Code(Check(rejected, getAuthConfig))) + assert.Equal(t, codes.Unauthenticated, status.Code(Check(rejected, listEvents))) + + // The same principal carrying no rejection is served normally. + anon := auth.Anonymous([]auth.Permission{auth.PermEventRead}) + require.NoError(t, CheckPermission(anon, auth.PermPublic)) + require.NoError(t, CheckPermission(anon, auth.PermEventRead)) +} + +// RequireHTTP guards the hand-written routes (/api/links, /api/homer-links), +// so it must refuse a rejected credential with a 401 too. +func TestRequireHTTPRefusesRejectedCredential(t *testing.T) { + called := false + h := RequireHTTP(auth.PermLinksRead, func(w http.ResponseWriter, r *http.Request, _ map[string]string) { + called = true + }) + + r := httptest.NewRequest(http.MethodGet, "/api/links", nil) + r = r.WithContext(auth.WithPrincipal(r.Context(), auth.RejectedCredential())) + w := httptest.NewRecorder() + h(w, r, nil) + + assert.Equal(t, http.StatusUnauthorized, w.Code) + assert.False(t, called, "the handler must not run") +} diff --git a/internal/auth/authz/methods.go b/internal/auth/authz/methods.go new file mode 100644 index 00000000..30de59f6 --- /dev/null +++ b/internal/auth/authz/methods.go @@ -0,0 +1,52 @@ +package authz + +import "github.com/bananaops/tracker/internal/auth" + +// MethodPermissions maps every RPC full name to the permission it requires. +// A method missing from this table is denied. methods_test.go enforces that +// every RPC declared in the protos has an entry. +var MethodPermissions = map[string]auth.Permission{ + // EventService + "/tracker.event.v1alpha1.EventService/CreateEvent": auth.PermEventWrite, + "/tracker.event.v1alpha1.EventService/UpdateEvent": auth.PermEventWrite, + "/tracker.event.v1alpha1.EventService/DeleteEvents": auth.PermEventWrite, + "/tracker.event.v1alpha1.EventService/GetEvent": auth.PermEventRead, + "/tracker.event.v1alpha1.EventService/SearchEvents": auth.PermEventRead, + "/tracker.event.v1alpha1.EventService/ListEvents": auth.PermEventRead, + "/tracker.event.v1alpha1.EventService/TodayEvents": auth.PermEventRead, + "/tracker.event.v1alpha1.EventService/AddChangelogEntry": auth.PermEventWrite, + "/tracker.event.v1alpha1.EventService/GetEventChangelog": auth.PermEventRead, + "/tracker.event.v1alpha1.EventService/AddSlackId": auth.PermEventWrite, + "/tracker.event.v1alpha1.EventService/GetEventStats": auth.PermEventRead, + "/tracker.event.v1alpha1.EventService/GetEventStatsByMonth": auth.PermEventRead, + + // CatalogService + "/tracker.catalog.v1alpha1.CatalogService/CreateUpdateCatalog": auth.PermCatalogWrite, + "/tracker.catalog.v1alpha1.CatalogService/GetCatalog": auth.PermCatalogRead, + "/tracker.catalog.v1alpha1.CatalogService/DeleteCatalog": auth.PermCatalogWrite, + "/tracker.catalog.v1alpha1.CatalogService/ListCatalogs": auth.PermCatalogRead, + "/tracker.catalog.v1alpha1.CatalogService/GetVersionCompliance": auth.PermCatalogRead, + "/tracker.catalog.v1alpha1.CatalogService/UpdateVersions": auth.PermCatalogWrite, + "/tracker.catalog.v1alpha1.CatalogService/UpdateDependencies": auth.PermCatalogWrite, + + // LockService + "/tracker.lock.v1alpha1.LockService/CreateLock": auth.PermLockWrite, + "/tracker.lock.v1alpha1.LockService/GetLock": auth.PermLockRead, + "/tracker.lock.v1alpha1.LockService/UpdateLock": auth.PermLockWrite, + "/tracker.lock.v1alpha1.LockService/UnLock": auth.PermLockWrite, + "/tracker.lock.v1alpha1.LockService/ListLocks": auth.PermLockRead, + + // AuthService (proto added in Task 12) + "/tracker.auth.v1alpha1.AuthService/GetAuthConfig": auth.PermPublic, + "/tracker.auth.v1alpha1.AuthService/Me": auth.PermPublic, + "/tracker.auth.v1alpha1.AuthService/ListUsers": auth.PermAccessManage, + "/tracker.auth.v1alpha1.AuthService/CreateUser": auth.PermAccessManage, + "/tracker.auth.v1alpha1.AuthService/UpdateUser": auth.PermAccessManage, + "/tracker.auth.v1alpha1.AuthService/ListTeams": auth.PermAccessManage, + "/tracker.auth.v1alpha1.AuthService/CreateTeam": auth.PermAccessManage, + "/tracker.auth.v1alpha1.AuthService/UpdateTeam": auth.PermAccessManage, + "/tracker.auth.v1alpha1.AuthService/DeleteTeam": auth.PermAccessManage, + "/tracker.auth.v1alpha1.AuthService/ListApiKeys": auth.PermAccessManage, + "/tracker.auth.v1alpha1.AuthService/CreateApiKey": auth.PermAccessManage, + "/tracker.auth.v1alpha1.AuthService/RevokeApiKey": auth.PermAccessManage, +} diff --git a/internal/auth/authz/methods_test.go b/internal/auth/authz/methods_test.go new file mode 100644 index 00000000..4df4e68c --- /dev/null +++ b/internal/auth/authz/methods_test.go @@ -0,0 +1,69 @@ +package authz + +import ( + "fmt" + "strings" + "testing" + + "github.com/bananaops/tracker/internal/auth" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" + + _ "github.com/bananaops/tracker/generated/proto/auth/v1alpha1" + _ "github.com/bananaops/tracker/generated/proto/catalog/v1alpha1" + _ "github.com/bananaops/tracker/generated/proto/event/v1alpha1" + _ "github.com/bananaops/tracker/generated/proto/lock/v1alpha1" +) + +const ( + PermPublicAlias = auth.PermPublic + PermAuthenticatedAlias = auth.PermAuthenticated +) + +func isGrantable(p auth.Permission) bool { return auth.IsValidPermission(p) } + +func registeredMethods() map[string]struct{} { + out := map[string]struct{}{} + protoregistry.GlobalFiles.RangeFiles(func(fd protoreflect.FileDescriptor) bool { + if !strings.HasPrefix(string(fd.Package()), "tracker.") { + return true + } + services := fd.Services() + for i := 0; i < services.Len(); i++ { + svc := services.Get(i) + methods := svc.Methods() + for j := 0; j < methods.Len(); j++ { + out[fmt.Sprintf("/%s/%s", svc.FullName(), methods.Get(j).Name())] = struct{}{} + } + } + return true + }) + return out +} + +func TestEveryRPCMethodIsMapped(t *testing.T) { + registered := registeredMethods() + assert.NotEmpty(t, registered) + for name := range registered { + _, ok := MethodPermissions[name] + assert.True(t, ok, "RPC %s has no entry in authz.MethodPermissions", name) + } +} + +func TestEveryTableEntryIsARegisteredRPC(t *testing.T) { + registered := registeredMethods() + for name := range MethodPermissions { + _, ok := registered[name] + assert.True(t, ok, "authz.MethodPermissions entry %s does not match any RPC (typo?)", name) + } +} + +func TestTableOnlyContainsGrantableOrPseudoPermissions(t *testing.T) { + for name, perm := range MethodPermissions { + if perm == PermPublicAlias || perm == PermAuthenticatedAlias { + continue + } + assert.True(t, isGrantable(perm), "%s maps to unknown permission %q", name, perm) + } +} diff --git a/internal/auth/config.go b/internal/auth/config.go new file mode 100644 index 00000000..317d5451 --- /dev/null +++ b/internal/auth/config.go @@ -0,0 +1,100 @@ +package auth + +import ( + "encoding/base64" + "fmt" + "strings" + "time" +) + +// Config is the authentication configuration read from the environment. +type Config struct { + // SessionSecret is nil when AUTH_SESSION_SECRET is not set; the caller then + // loads or generates a persisted secret. + SessionSecret []byte + SessionTTL time.Duration + AnonymousPermissions []Permission + // AnonymousDefaulted is true when the transitional default was applied + // because AUTH_ANONYMOUS_PERMISSIONS is not set. + AnonymousDefaulted bool + AdminPassword string + PublicURL string + CookieSecure bool + TrustProxy bool + DemoMode bool +} + +// LookupEnv has the signature of os.LookupEnv. +type LookupEnv func(key string) (string, bool) + +// ReadOnlyPermissions is what anonymous visitors get in demo mode. +func ReadOnlyPermissions() []Permission { + return []Permission{PermEventRead, PermCatalogRead, PermLockRead, PermLinksRead} +} + +// TransitionalAnonymousPermissions is the default applied while authentication +// is being rolled out: everything but access management. It becomes empty in +// the next major release. +func TransitionalAnonymousPermissions() []Permission { + out := []Permission{} + for _, p := range AllPermissions() { + if p != PermAccessManage { + out = append(out, p) + } + } + return out +} + +// LoadConfig reads and validates the AUTH_* variables. +func LoadConfig(lookup LookupEnv) (Config, error) { + get := func(key string) string { + v, _ := lookup(key) + return strings.TrimSpace(v) + } + cfg := Config{SessionTTL: 12 * time.Hour} + cfg.DemoMode = get("DEMO_MODE") == "true" + + if v := get("AUTH_SESSION_SECRET"); v != "" { + secret, err := base64.StdEncoding.DecodeString(v) + if err != nil { + return Config{}, fmt.Errorf("AUTH_SESSION_SECRET must be base64: %w", err) + } + if len(secret) < SessionSecretLength { + return Config{}, fmt.Errorf("AUTH_SESSION_SECRET must decode to at least %d bytes", SessionSecretLength) + } + cfg.SessionSecret = secret + } + + if v := get("AUTH_SESSION_TTL"); v != "" { + ttl, err := time.ParseDuration(v) + if err != nil || ttl <= 0 { + return Config{}, fmt.Errorf("AUTH_SESSION_TTL must be a positive duration such as 12h, got %q", v) + } + cfg.SessionTTL = ttl + } + + if raw, ok := lookup("AUTH_ANONYMOUS_PERMISSIONS"); ok { + perms, err := ParsePermissions(raw) + if err != nil { + return Config{}, fmt.Errorf("AUTH_ANONYMOUS_PERMISSIONS: %w", err) + } + cfg.AnonymousPermissions = perms + } else if cfg.DemoMode { + cfg.AnonymousPermissions = ReadOnlyPermissions() + } else { + cfg.AnonymousPermissions = TransitionalAnonymousPermissions() + cfg.AnonymousDefaulted = true + } + + cfg.AdminPassword = get("AUTH_ADMIN_PASSWORD") + if cfg.AdminPassword != "" { + if err := ValidatePasswordPolicy(cfg.AdminPassword); err != nil { + return Config{}, fmt.Errorf("AUTH_ADMIN_PASSWORD: %w", err) + } + } + + cfg.PublicURL = strings.TrimRight(get("AUTH_PUBLIC_URL"), "/") + cfg.CookieSecure = strings.HasPrefix(cfg.PublicURL, "https://") || get("AUTH_COOKIE_SECURE") == "true" + cfg.TrustProxy = get("AUTH_TRUST_PROXY") == "true" + return cfg, nil +} diff --git a/internal/auth/config_test.go b/internal/auth/config_test.go new file mode 100644 index 00000000..10f10a9c --- /dev/null +++ b/internal/auth/config_test.go @@ -0,0 +1,68 @@ +package auth + +import ( + "bytes" + "encoding/base64" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func envOf(m map[string]string) LookupEnv { + return func(k string) (string, bool) { v, ok := m[k]; return v, ok } +} + +func TestLoadConfigDefaults(t *testing.T) { + cfg, err := LoadConfig(envOf(map[string]string{})) + require.NoError(t, err) + assert.Nil(t, cfg.SessionSecret) + assert.Equal(t, 12*time.Hour, cfg.SessionTTL) + assert.True(t, cfg.AnonymousDefaulted) + assert.ElementsMatch(t, TransitionalAnonymousPermissions(), cfg.AnonymousPermissions) + assert.NotContains(t, cfg.AnonymousPermissions, PermAccessManage) + assert.False(t, cfg.CookieSecure) + assert.False(t, cfg.DemoMode) +} + +func TestLoadConfigDemoMode(t *testing.T) { + cfg, err := LoadConfig(envOf(map[string]string{"DEMO_MODE": "true"})) + require.NoError(t, err) + assert.True(t, cfg.DemoMode) + assert.False(t, cfg.AnonymousDefaulted) + assert.ElementsMatch(t, ReadOnlyPermissions(), cfg.AnonymousPermissions) +} + +func TestLoadConfigExplicit(t *testing.T) { + secret := bytes.Repeat([]byte{1}, 32) + cfg, err := LoadConfig(envOf(map[string]string{ + "AUTH_SESSION_SECRET": base64.StdEncoding.EncodeToString(secret), + "AUTH_SESSION_TTL": "30m", + "AUTH_ANONYMOUS_PERMISSIONS": "", + "AUTH_ADMIN_PASSWORD": "a-long-enough-password", + "AUTH_PUBLIC_URL": "https://tracker.example.com/", + "AUTH_TRUST_PROXY": "true", + })) + require.NoError(t, err) + assert.Equal(t, secret, cfg.SessionSecret) + assert.Equal(t, 30*time.Minute, cfg.SessionTTL) + assert.Empty(t, cfg.AnonymousPermissions) + assert.False(t, cfg.AnonymousDefaulted) + assert.Equal(t, "https://tracker.example.com", cfg.PublicURL) + assert.True(t, cfg.CookieSecure) + assert.True(t, cfg.TrustProxy) +} + +func TestLoadConfigErrors(t *testing.T) { + _, err := LoadConfig(envOf(map[string]string{"AUTH_SESSION_SECRET": "not-base64!"})) + assert.Error(t, err) + _, err = LoadConfig(envOf(map[string]string{"AUTH_SESSION_SECRET": base64.StdEncoding.EncodeToString([]byte("short"))})) + assert.Error(t, err) + _, err = LoadConfig(envOf(map[string]string{"AUTH_SESSION_TTL": "soon"})) + assert.Error(t, err) + _, err = LoadConfig(envOf(map[string]string{"AUTH_ANONYMOUS_PERMISSIONS": "event:read,nope"})) + assert.Error(t, err) + _, err = LoadConfig(envOf(map[string]string{"AUTH_ADMIN_PASSWORD": "short"})) + assert.ErrorIs(t, err, ErrPasswordPolicy) +} diff --git a/internal/auth/credentials.go b/internal/auth/credentials.go new file mode 100644 index 00000000..7a59112f --- /dev/null +++ b/internal/auth/credentials.go @@ -0,0 +1,134 @@ +package auth + +import ( + "net" + "net/http" + "strings" + "time" + + "google.golang.org/grpc/metadata" +) + +const ( + // SessionCookieName carries the session token for the SPA. + SessionCookieName = "tracker_session" + // APIKeyHeader carries an API key. + APIKeyHeader = "X-Api-Key" // #nosec G101 -- header name, not a credential +) + +// Credentials are the raw secrets found on a request, before any lookup. +type Credentials struct { + APIKey string + SessionToken string + // FromCookie is true when the session token came from the browser cookie + // rather than an explicit header. Only that source is ambient, so only + // that source needs the cross-site guard. See IsCrossSite. + FromCookie bool +} + +// Empty reports whether no credential was presented. +func (c Credentials) Empty() bool { return c.APIKey == "" && c.SessionToken == "" } + +// CredentialsFromHTTP extracts credentials in priority order: +// X-Api-Key header, Authorization bearer, session cookie. +func CredentialsFromHTTP(r *http.Request) Credentials { + if v := strings.TrimSpace(r.Header.Get(APIKeyHeader)); v != "" { + return Credentials{APIKey: v} + } + if c, ok := fromBearer(r.Header.Get("Authorization")); ok { + return c + } + if ck, err := r.Cookie(SessionCookieName); err == nil && ck.Value != "" { + return Credentials{SessionToken: ck.Value, FromCookie: true} + } + return Credentials{} +} + +// CredentialsFromMetadata extracts credentials from gRPC metadata. +func CredentialsFromMetadata(md metadata.MD) Credentials { + first := func(key string) string { + if v := md.Get(key); len(v) > 0 { + return strings.TrimSpace(v[0]) + } + return "" + } + if v := first("x-api-key"); v != "" { + return Credentials{APIKey: v} + } + if c, ok := fromBearer(first("authorization")); ok { + return c + } + return Credentials{} +} + +func fromBearer(header string) (Credentials, bool) { + const prefix = "bearer " + if len(header) <= len(prefix) || !strings.EqualFold(header[:len(prefix)], prefix) { + return Credentials{}, false + } + token := strings.TrimSpace(header[len(prefix):]) + if token == "" { + return Credentials{}, false + } + if IsAPIKey(token) { + return Credentials{APIKey: token}, true + } + return Credentials{SessionToken: token}, true +} + +// SessionCookie builds the cookie carrying a session token. +func SessionCookie(token string, expires time.Time, secure bool) *http.Cookie { + // Secure follows AUTH_COOKIE_SECURE or an https AUTH_PUBLIC_URL; a plain + // http deployment cannot set it or the browser drops the cookie. + return &http.Cookie{ // #nosec G124 -- Secure is configuration driven, HttpOnly and SameSite are set + Name: SessionCookieName, + Value: token, + Path: "/", + HttpOnly: true, + Secure: secure, + SameSite: http.SameSiteLaxMode, + Expires: expires, + MaxAge: int(time.Until(expires).Seconds()), + } +} + +// ClearSessionCookie builds the cookie that removes the session. +func ClearSessionCookie(secure bool) *http.Cookie { + return &http.Cookie{ // #nosec G124 -- Secure is configuration driven, HttpOnly and SameSite are set + Name: SessionCookieName, + Value: "", + Path: "/", + HttpOnly: true, + Secure: secure, + SameSite: http.SameSiteLaxMode, + Expires: time.Unix(0, 0), + MaxAge: -1, + } +} + +// ClientIP returns the peer address, honouring X-Forwarded-For only when the +// deployment declares a trusted reverse proxy. +// +// The LAST entry of the header is used, not the first. Every mainstream +// ingress (nginx $proxy_add_x_forwarded_for, Traefik, HAProxy) appends the +// address it saw to whatever the client sent, so the last entry is the only +// one the trusted proxy wrote. Reading the first entry would let a client +// forge X-Forwarded-For and get a fresh rate limiting key on every request, +// which defeats the per (username, IP) login limit. +func ClientIP(r *http.Request, trustProxy bool) string { + if trustProxy { + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + parts := strings.Split(xff, ",") + for i := len(parts) - 1; i >= 0; i-- { + if ip := strings.TrimSpace(parts[i]); ip != "" { + return ip + } + } + } + } + host, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return r.RemoteAddr + } + return host +} diff --git a/internal/auth/credentials_test.go b/internal/auth/credentials_test.go new file mode 100644 index 00000000..c510c3c0 --- /dev/null +++ b/internal/auth/credentials_test.go @@ -0,0 +1,84 @@ +package auth + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/metadata" +) + +func TestCredentialsFromHTTP(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/", nil) + assert.True(t, CredentialsFromHTTP(r).Empty()) + + r.Header.Set("X-Api-Key", " trk_abcdefgh_secret ") + assert.Equal(t, Credentials{APIKey: "trk_abcdefgh_secret"}, CredentialsFromHTTP(r)) + + r = httptest.NewRequest(http.MethodGet, "/", nil) + r.Header.Set("Authorization", "Bearer trk_abcdefgh_secret") + assert.Equal(t, Credentials{APIKey: "trk_abcdefgh_secret"}, CredentialsFromHTTP(r)) + + r = httptest.NewRequest(http.MethodGet, "/", nil) + r.Header.Set("Authorization", "bearer eyJ.jwt") + assert.Equal(t, Credentials{SessionToken: "eyJ.jwt"}, CredentialsFromHTTP(r)) + + r = httptest.NewRequest(http.MethodGet, "/", nil) + r.AddCookie(&http.Cookie{Name: SessionCookieName, Value: "cookie.jwt"}) + assert.Equal(t, Credentials{SessionToken: "cookie.jwt", FromCookie: true}, CredentialsFromHTTP(r)) + + // Header wins over cookie. + r.Header.Set("X-Api-Key", "trk_abcdefgh_secret") + assert.Equal(t, Credentials{APIKey: "trk_abcdefgh_secret"}, CredentialsFromHTTP(r)) +} + +func TestCredentialsFromMetadata(t *testing.T) { + assert.True(t, CredentialsFromMetadata(metadata.MD{}).Empty()) + md := metadata.Pairs("authorization", "Bearer eyJ.jwt") + assert.Equal(t, Credentials{SessionToken: "eyJ.jwt"}, CredentialsFromMetadata(md)) + md = metadata.Pairs("x-api-key", "trk_abcdefgh_secret") + assert.Equal(t, Credentials{APIKey: "trk_abcdefgh_secret"}, CredentialsFromMetadata(md)) +} + +func TestSessionCookies(t *testing.T) { + expires := time.Now().Add(time.Hour) + c := SessionCookie("tok", expires, true) + assert.Equal(t, SessionCookieName, c.Name) + assert.True(t, c.HttpOnly) + assert.True(t, c.Secure) + assert.Equal(t, http.SameSiteLaxMode, c.SameSite) + assert.Equal(t, "/", c.Path) + assert.Greater(t, c.MaxAge, 3500) + + cleared := ClearSessionCookie(false) + assert.Equal(t, -1, cleared.MaxAge) + assert.Empty(t, cleared.Value) +} + +func TestClientIP(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.RemoteAddr = "10.0.0.5:4444" + r.Header.Set("X-Forwarded-For", "203.0.113.9, 10.0.0.1") + assert.Equal(t, "10.0.0.5", ClientIP(r, false), "the header is ignored without a trusted proxy") + assert.Equal(t, "10.0.0.1", ClientIP(r, true), "the last entry is the one appended by the trusted proxy") + + // A client that forges the header cannot change its rate limiting key: the + // trusted proxy appends the real peer address after whatever was sent. + spoofed := httptest.NewRequest(http.MethodGet, "/", nil) + spoofed.RemoteAddr = "10.0.0.5:4444" + spoofed.Header.Set("X-Forwarded-For", "1.2.3.4, 5.6.7.8 , 198.51.100.7") + assert.Equal(t, "198.51.100.7", ClientIP(spoofed, true)) + + single := httptest.NewRequest(http.MethodGet, "/", nil) + single.RemoteAddr = "10.0.0.5:4444" + single.Header.Set("X-Forwarded-For", " 198.51.100.7 ") + assert.Equal(t, "198.51.100.7", ClientIP(single, true)) + + // An empty or blank header falls back to the peer address. + blank := httptest.NewRequest(http.MethodGet, "/", nil) + blank.RemoteAddr = "10.0.0.5:4444" + blank.Header.Set("X-Forwarded-For", " , ") + assert.Equal(t, "10.0.0.5", ClientIP(blank, true)) +} diff --git a/internal/auth/csrf.go b/internal/auth/csrf.go new file mode 100644 index 00000000..e0129dad --- /dev/null +++ b/internal/auth/csrf.go @@ -0,0 +1,86 @@ +package auth + +import ( + "net/http" + "net/url" + "strings" +) + +// IsCrossSite reports whether a browser request was initiated from another +// site. It is the CSRF guard for the session cookie: that cookie is +// SameSite=Lax, so a browser still attaches it to a top level cross-site GET +// navigation, and the API exposes at least one write behind a GET binding +// (UnLock). A cross-site request must therefore not act as the logged in user. +// +// Sec-Fetch-Site is authoritative when present. Only same-origin, same-site +// and none are accepted; cross-site and any other value are treated as +// cross-site, because browsers are the only clients that send the header and +// an unrecognised value is safest handled as untrusted. +// +// Without Sec-Fetch-Site, an Origin header is compared to the expected origin: +// publicURL (AUTH_PUBLIC_URL) when configured, otherwise the request scheme +// and Host. X-Forwarded-Proto is honoured for the scheme only when trustProxy +// is on, since a client can set it otherwise. +// +// A request carrying neither header comes from a non browser client (curl, a +// script, the MCP server). Such a client cannot be tricked into replaying a +// cookie it does not hold, so it is not cross-site. +func IsCrossSite(r *http.Request, publicURL string, trustProxy bool) bool { + switch strings.ToLower(strings.TrimSpace(r.Header.Get("Sec-Fetch-Site"))) { + case "": + // No Sec-Fetch-Site, fall back to the Origin check below. + case "same-origin", "same-site", "none": + return false + default: + return true + } + + origin := strings.TrimSpace(r.Header.Get("Origin")) + if origin == "" { + return false + } + return normalizeOrigin(origin) != normalizeOrigin(expectedOrigin(r, publicURL, trustProxy)) +} + +// normalizeOrigin lowercases an origin and drops an explicit default port, so +// that https://host and https://host:443 compare equal (same for http and 80). +// Browsers omit the default port in Origin, but AUTH_PUBLIC_URL and the Host +// header may carry it, and a spurious mismatch would silently turn legitimate +// requests anonymous. +func normalizeOrigin(origin string) string { + o := strings.ToLower(strings.TrimRight(strings.TrimSpace(origin), "/")) + if rest, ok := strings.CutPrefix(o, "http://"); ok { + return "http://" + strings.TrimSuffix(rest, ":80") + } + if rest, ok := strings.CutPrefix(o, "https://"); ok { + return "https://" + strings.TrimSuffix(rest, ":443") + } + return o +} + +// expectedOrigin is the scheme://host[:port] a same-origin request carries. +func expectedOrigin(r *http.Request, publicURL string, trustProxy bool) string { + if publicURL != "" { + if u, err := url.Parse(publicURL); err == nil && u.Scheme != "" && u.Host != "" { + return strings.ToLower(u.Scheme + "://" + u.Host) + } + } + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + if trustProxy { + if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" { + // Same rule as X-Forwarded-For: a proxy chain appends, so the + // last entry is the one the trusted proxy wrote. + parts := strings.Split(proto, ",") + for i := len(parts) - 1; i >= 0; i-- { + if v := strings.TrimSpace(parts[i]); v != "" { + scheme = strings.ToLower(v) + break + } + } + } + } + return strings.ToLower(scheme + "://" + r.Host) +} diff --git a/internal/auth/csrf_test.go b/internal/auth/csrf_test.go new file mode 100644 index 00000000..297f397d --- /dev/null +++ b/internal/auth/csrf_test.go @@ -0,0 +1,102 @@ +package auth + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsCrossSite(t *testing.T) { + cases := []struct { + name string + secFetch string + origin string + forwarded string + host string + publicURL string + trustProxy bool + want bool + }{ + {name: "no header at all is a non browser client", want: false}, + {name: "sec-fetch-site cross-site", secFetch: "cross-site", want: true}, + {name: "sec-fetch-site same-origin", secFetch: "same-origin", want: false}, + {name: "sec-fetch-site same-site", secFetch: "same-site", want: false}, + {name: "sec-fetch-site none", secFetch: "none", want: false}, + {name: "sec-fetch-site wins over a matching origin", secFetch: "cross-site", origin: "http://example.com", host: "example.com", want: true}, + {name: "unknown sec-fetch-site value fails closed", secFetch: "future-value", want: true}, + { + name: "origin matching the public url", origin: "https://tracker.example.com", + publicURL: "https://tracker.example.com", host: "internal:8080", want: false, + }, + { + name: "origin not matching the public url", origin: "https://evil.example.net", + publicURL: "https://tracker.example.com", host: "internal:8080", want: true, + }, + { + name: "public url with a trailing slash still matches", origin: "https://tracker.example.com", + publicURL: "https://tracker.example.com/", host: "internal:8080", want: false, + }, + {name: "origin matching the request host", origin: "http://example.com:8080", host: "example.com:8080", want: false}, + {name: "origin not matching the request host", origin: "http://evil.example.net", host: "example.com:8080", want: true}, + {name: "origin null is cross-site", origin: "null", host: "example.com", want: true}, + { + name: "forwarded proto is honoured behind a trusted proxy", origin: "https://example.com", + forwarded: "https", host: "example.com", trustProxy: true, want: false, + }, + { + name: "forwarded proto is ignored without a trusted proxy", origin: "https://example.com", + forwarded: "https", host: "example.com", want: true, + }, + + // An explicit default port means the same origin as no port at all. + { + name: "explicit 443 in the public url matches a bare origin", origin: "https://tracker.example.com", + publicURL: "https://tracker.example.com:443", host: "internal:8080", want: false, + }, + { + name: "explicit 443 in the origin matches a bare public url", origin: "https://tracker.example.com:443", + publicURL: "https://tracker.example.com", host: "internal:8080", want: false, + }, + { + name: "explicit 80 in the origin matches a bare host", origin: "http://example.com:80", + host: "example.com", want: false, + }, + { + name: "explicit 80 in the host matches a bare origin", origin: "http://example.com", + host: "example.com:80", want: false, + }, + { + name: "a non default port still has to match", origin: "https://tracker.example.com:8443", + publicURL: "https://tracker.example.com", host: "internal:8080", want: true, + }, + { + name: "443 on http is not a default port", origin: "http://example.com:443", + host: "example.com", want: true, + }, + { + name: "a port ending in 80 is not the default port", origin: "http://example.com:8080", + host: "example.com", want: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/api/v1alpha1/unlock/1", nil) + if tc.host != "" { + r.Host = tc.host + } + if tc.secFetch != "" { + r.Header.Set("Sec-Fetch-Site", tc.secFetch) + } + if tc.origin != "" { + r.Header.Set("Origin", tc.origin) + } + if tc.forwarded != "" { + r.Header.Set("X-Forwarded-Proto", tc.forwarded) + } + assert.Equal(t, tc.want, IsCrossSite(r, tc.publicURL, tc.trustProxy)) + }) + } +} diff --git a/internal/auth/identity/bootstrap.go b/internal/auth/identity/bootstrap.go new file mode 100644 index 00000000..4d303fc3 --- /dev/null +++ b/internal/auth/identity/bootstrap.go @@ -0,0 +1,133 @@ +package identity + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + "math/big" + + "github.com/bananaops/tracker/internal/auth" + store "github.com/bananaops/tracker/internal/stores" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// BootstrapUserStore is the subset of store.AuthUserStore used at startup. +type BootstrapUserStore interface { + Count(ctx context.Context) (int64, error) + Create(ctx context.Context, u *store.User) error +} + +// BootstrapTeamStore is the subset of store.AuthTeamStore used at startup. +type BootstrapTeamStore interface { + GetByName(ctx context.Context, name string) (*store.Team, error) + Create(ctx context.Context, t *store.Team) error +} + +// BootstrapResult tells the caller what was created. +type BootstrapResult struct { + AdminCreated bool + // GeneratedPassword is set only when no AUTH_ADMIN_PASSWORD was provided. + // The caller must log it once. + GeneratedPassword string + AdminsTeamID primitive.ObjectID +} + +const generatedPasswordLength = 24 + +// Bootstrap makes sure the Administrators team exists and creates the first +// admin user when the user collection is empty. +// +// It is safe to run on several replicas at once. Both writes are guarded by a +// unique index, and losing either race is treated as success: the team is read +// back, and an admin created by a peer means this replica has nothing to +// report, so AdminCreated is false and no password is returned. +func Bootstrap(ctx context.Context, users BootstrapUserStore, teams BootstrapTeamStore, adminPassword string) (BootstrapResult, error) { + admins, err := teams.GetByName(ctx, store.AdministratorsTeamName) + if errors.Is(err, store.ErrNotFound) { + admins = &store.Team{ + Name: store.AdministratorsTeamName, + Description: "Built-in team holding every permission", + Permissions: permissionStrings(auth.AllPermissions()), + Scope: store.TeamScope{All: true, Services: []string{}}, + OIDCGroups: []string{}, + Builtin: true, + } + if err := teams.Create(ctx, admins); errors.Is(err, store.ErrAlreadyExists) { + // Another replica won the race between the lookup and the insert. + // The unique index did its job, read back what the peer wrote. + admins, err = teams.GetByName(ctx, store.AdministratorsTeamName) + if err != nil { + return BootstrapResult{}, fmt.Errorf("lookup administrators team created by a peer: %w", err) + } + } else if err != nil { + return BootstrapResult{}, fmt.Errorf("create administrators team: %w", err) + } + } else if err != nil { + return BootstrapResult{}, fmt.Errorf("lookup administrators team: %w", err) + } + result := BootstrapResult{AdminsTeamID: admins.ID} + + count, err := users.Count(ctx) + if err != nil { + return BootstrapResult{}, fmt.Errorf("count users: %w", err) + } + if count > 0 { + return result, nil + } + + password := adminPassword + if password == "" { + password, err = GeneratePassword(generatedPasswordLength) + if err != nil { + return BootstrapResult{}, err + } + result.GeneratedPassword = password + } + hash, err := auth.HashPassword(password) + if err != nil { + return BootstrapResult{}, fmt.Errorf("hash admin password: %w", err) + } + admin := &store.User{ + Username: "admin", + DisplayName: "Administrator", + Source: store.UserSourceLocal, + PasswordHash: hash, + Teams: []primitive.ObjectID{admins.ID}, + MustChangePassword: true, + } + if err := users.Create(ctx, admin); errors.Is(err, store.ErrAlreadyExists) { + // A peer replica created the admin between the Count and the insert. + // Its password is the one that counts, so drop the one generated here + // and report nothing created: the caller must not log a password that + // does not open anything. + return BootstrapResult{AdminsTeamID: admins.ID}, nil + } else if err != nil { + return BootstrapResult{}, fmt.Errorf("create admin user: %w", err) + } + result.AdminCreated = true + return result, nil +} + +const passwordAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +// GeneratePassword returns a random alphanumeric password of n characters. +func GeneratePassword(n int) (string, error) { + out := make([]byte, n) + for i := range out { + idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(passwordAlphabet)))) + if err != nil { + return "", fmt.Errorf("generate password: %w", err) + } + out[i] = passwordAlphabet[idx.Int64()] + } + return string(out), nil +} + +func permissionStrings(perms []auth.Permission) []string { + out := make([]string, len(perms)) + for i, p := range perms { + out[i] = string(p) + } + return out +} diff --git a/internal/auth/identity/bootstrap_test.go b/internal/auth/identity/bootstrap_test.go new file mode 100644 index 00000000..538627d1 --- /dev/null +++ b/internal/auth/identity/bootstrap_test.go @@ -0,0 +1,130 @@ +package identity + +import ( + "context" + "testing" + + "github.com/bananaops/tracker/internal/auth" + store "github.com/bananaops/tracker/internal/stores" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type memUsers struct{ users []*store.User } + +func (m *memUsers) Count(context.Context) (int64, error) { return int64(len(m.users)), nil } +func (m *memUsers) Create(_ context.Context, u *store.User) error { + u.ID = primitive.NewObjectID() + m.users = append(m.users, u) + return nil +} + +type memTeams struct{ teams map[string]*store.Team } + +func (m *memTeams) GetByName(_ context.Context, name string) (*store.Team, error) { + if t, ok := m.teams[name]; ok { + return t, nil + } + return nil, store.ErrNotFound +} +func (m *memTeams) Create(_ context.Context, t *store.Team) error { + t.ID = primitive.NewObjectID() + m.teams[t.Name] = t + return nil +} + +func TestBootstrapCreatesAdminWithGivenPassword(t *testing.T) { + users, teams := &memUsers{}, &memTeams{teams: map[string]*store.Team{}} + res, err := Bootstrap(context.Background(), users, teams, "initial-admin-password") + require.NoError(t, err) + assert.True(t, res.AdminCreated) + assert.Empty(t, res.GeneratedPassword) + + admins := teams.teams[store.AdministratorsTeamName] + require.NotNil(t, admins) + assert.True(t, admins.Builtin) + assert.True(t, admins.Scope.All) + assert.Len(t, admins.Permissions, len(auth.AllPermissions())) + assert.Equal(t, admins.ID, res.AdminsTeamID) + + require.Len(t, users.users, 1) + admin := users.users[0] + assert.Equal(t, "admin", admin.Username) + assert.Equal(t, store.UserSourceLocal, admin.Source) + assert.True(t, admin.MustChangePassword) + assert.Equal(t, []primitive.ObjectID{admins.ID}, admin.Teams) + ok, _ := auth.VerifyPassword(admin.PasswordHash, "initial-admin-password") + assert.True(t, ok) +} + +func TestBootstrapGeneratesPasswordAndIsIdempotent(t *testing.T) { + users, teams := &memUsers{}, &memTeams{teams: map[string]*store.Team{}} + res, err := Bootstrap(context.Background(), users, teams, "") + require.NoError(t, err) + assert.Len(t, res.GeneratedPassword, 24) + ok, _ := auth.VerifyPassword(users.users[0].PasswordHash, res.GeneratedPassword) + assert.True(t, ok) + + again, err := Bootstrap(context.Background(), users, teams, "") + require.NoError(t, err) + assert.False(t, again.AdminCreated) + assert.Len(t, users.users, 1) + assert.Len(t, teams.teams, 1) +} + +// raceTeams answers ErrNotFound on the first lookup, then loses the creation +// race and finally sees the team a peer replica created. +type raceTeams struct { + peer *store.Team + lookups int + attempts int +} + +func (m *raceTeams) GetByName(_ context.Context, name string) (*store.Team, error) { + m.lookups++ + if m.lookups == 1 { + return nil, store.ErrNotFound + } + return m.peer, nil +} + +func (m *raceTeams) Create(_ context.Context, _ *store.Team) error { + m.attempts++ + return store.ErrAlreadyExists +} + +func TestBootstrapReusesTeamCreatedByAPeer(t *testing.T) { + peer := &store.Team{ID: primitive.NewObjectID(), Name: store.AdministratorsTeamName, Builtin: true} + teams := &raceTeams{peer: peer} + users := &memUsers{} + + res, err := Bootstrap(context.Background(), users, teams, "initial-admin-password") + require.NoError(t, err, "losing the team creation race is not a startup failure") + assert.Equal(t, peer.ID, res.AdminsTeamID) + assert.Equal(t, 2, teams.lookups, "the team is read back after the unique index fires") + assert.True(t, res.AdminCreated) + require.Len(t, users.users, 1) + assert.Equal(t, []primitive.ObjectID{peer.ID}, users.users[0].Teams) +} + +// racingUsers reports an empty collection but loses the creation race. +type racingUsers struct{ attempts int } + +func (m *racingUsers) Count(context.Context) (int64, error) { return 0, nil } +func (m *racingUsers) Create(_ context.Context, _ *store.User) error { + m.attempts++ + return store.ErrAlreadyExists +} + +func TestBootstrapToleratesAdminCreatedByAPeer(t *testing.T) { + teams := &memTeams{teams: map[string]*store.Team{}} + users := &racingUsers{} + + res, err := Bootstrap(context.Background(), users, teams, "") + require.NoError(t, err, "a peer replica created the admin, this replica just carries on") + assert.False(t, res.AdminCreated, "this replica did not create it") + assert.Empty(t, res.GeneratedPassword, "nothing must be logged as a password") + assert.Equal(t, teams.teams[store.AdministratorsTeamName].ID, res.AdminsTeamID) + assert.Equal(t, 1, users.attempts) +} diff --git a/internal/auth/identity/effective.go b/internal/auth/identity/effective.go new file mode 100644 index 00000000..b384bd6f --- /dev/null +++ b/internal/auth/identity/effective.go @@ -0,0 +1,33 @@ +// Package identity resolves principals from the persisted users, teams and API keys. +package identity + +import ( + "github.com/bananaops/tracker/internal/auth" + store "github.com/bananaops/tracker/internal/stores" +) + +// Effective computes the rights granted by a set of teams: the union of their +// permissions, the union of their scopes and whether one of them is the +// built-in Administrators team. +func Effective(teams []*store.Team) (auth.PermissionSet, auth.Scope, bool) { + perms := auth.NewPermissionSet() + scope := auth.ScopeOf() + admin := false + for _, t := range teams { + for _, raw := range t.Permissions { + p := auth.Permission(raw) + if auth.IsValidPermission(p) { + perms.Add(p) + } + } + if t.Scope.All { + scope = scope.Union(auth.ScopeAll()) + } else { + scope = scope.Union(auth.ScopeOf(t.Scope.Services...)) + } + if t.Builtin { + admin = true + } + } + return perms, scope, admin +} diff --git a/internal/auth/identity/effective_test.go b/internal/auth/identity/effective_test.go new file mode 100644 index 00000000..6498d880 --- /dev/null +++ b/internal/auth/identity/effective_test.go @@ -0,0 +1,33 @@ +package identity + +import ( + "testing" + + "github.com/bananaops/tracker/internal/auth" + store "github.com/bananaops/tracker/internal/stores" + "github.com/stretchr/testify/assert" +) + +func TestEffective(t *testing.T) { + perms, scope, admin := Effective(nil) + assert.Empty(t, perms) + assert.False(t, scope.All) + assert.False(t, admin) + + teams := []*store.Team{ + {Name: "readers", Permissions: []string{"event:read", "bogus"}, Scope: store.TeamScope{Services: []string{"api"}}}, + {Name: "ops", Permissions: []string{"lock:write"}, Scope: store.TeamScope{Services: []string{"web"}}}, + } + perms, scope, admin = Effective(teams) + assert.True(t, perms.Has(auth.PermEventRead)) + assert.True(t, perms.Has(auth.PermLockWrite)) + assert.False(t, perms.Has("bogus"), "unknown permissions stored in the database are ignored") + assert.Equal(t, []string{"api", "web"}, scope.ServiceList()) + assert.False(t, admin) + + teams = append(teams, &store.Team{Name: "Administrators", Builtin: true, Permissions: []string{"access:manage"}, Scope: store.TeamScope{All: true}}) + perms, scope, admin = Effective(teams) + assert.True(t, scope.All) + assert.True(t, admin) + assert.True(t, perms.Has(auth.PermAccessManage)) +} diff --git a/internal/auth/identity/resolver.go b/internal/auth/identity/resolver.go new file mode 100644 index 00000000..eb270388 --- /dev/null +++ b/internal/auth/identity/resolver.go @@ -0,0 +1,204 @@ +package identity + +import ( + "context" + "errors" + "log/slog" + "time" + + "github.com/bananaops/tracker/internal/auth" + store "github.com/bananaops/tracker/internal/stores" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// UserStore is the subset of store.AuthUserStore used to resolve sessions. +type UserStore interface { + GetByID(ctx context.Context, id primitive.ObjectID) (*store.User, error) +} + +// TeamStore is the subset of store.AuthTeamStore used to compute rights. +type TeamStore interface { + GetByIDs(ctx context.Context, ids []primitive.ObjectID) ([]*store.Team, error) +} + +// APIKeyStore is the subset of store.AuthAPIKeyStore used to resolve keys. +type APIKeyStore interface { + GetByPrefix(ctx context.Context, prefix string) (*store.APIKey, error) + TouchLastUsed(ctx context.Context, id primitive.ObjectID, at time.Time) error +} + +// Resolver implements auth.Resolver on top of the stores. +type Resolver struct { + Users UserStore + Teams TeamStore + Keys APIKeyStore + Sessions *auth.SessionManager + AnonymousPermissions []auth.Permission + Now func() time.Time + Logger *slog.Logger +} + +const lastUsedGranularity = time.Minute + +func (r *Resolver) logger() *slog.Logger { + if r.Logger != nil { + return r.Logger + } + return slog.Default() +} + +func (r *Resolver) now() time.Time { + if r.Now != nil { + return r.Now() + } + return time.Now() +} + +func (r *Resolver) anonymous() auth.Principal { + return auth.Anonymous(r.AnonymousPermissions) +} + +// Resolve never returns an error: a credential it cannot honour yields +// auth.RejectedCredential, which authorization refuses with a 401. +// +// The one exception is the session cookie. It is ambient, so a browser keeps +// sending it long after it went stale, and refusing it would serve a 401 for +// the SPA itself, including the login page that would let the user recover. +// A cookie that no longer resolves therefore falls back to anonymous, exactly +// as if it had not been sent. Explicit credentials get no such indulgence. +func (r *Resolver) Resolve(ctx context.Context, creds auth.Credentials) auth.Principal { + if creds.APIKey != "" { + if p, ok := r.resolveAPIKey(ctx, creds.APIKey); ok { + return p + } + return auth.RejectedCredential() + } + if creds.SessionToken != "" { + if p, ok := r.resolveSession(ctx, creds.SessionToken); ok { + return p + } + if creds.FromCookie { + return r.anonymous() + } + return auth.RejectedCredential() + } + return r.anonymous() +} + +func (r *Resolver) resolveAPIKey(ctx context.Context, secret string) (auth.Principal, bool) { + prefix, ok := auth.ParseAPIKeyPrefix(secret) + if !ok { + r.logger().Warn("auth: malformed api key") + return auth.Principal{}, false + } + key, err := r.Keys.GetByPrefix(ctx, prefix) + if err != nil { + if !errors.Is(err, store.ErrNotFound) { + r.logger().Error("auth: api key lookup failed", "error", err) + } else { + r.logger().Warn("auth: unknown api key", "prefix", prefix) + } + return auth.Principal{}, false + } + if !auth.APIKeyMatches(key.Hash, secret) { + r.logger().Warn("auth: api key secret mismatch", "prefix", prefix) + return auth.Principal{}, false + } + now := r.now() + if key.RevokedAt != nil { + r.logger().Warn("auth: revoked api key used", "prefix", prefix) + return auth.Principal{}, false + } + if key.ExpiresAt != nil && now.After(*key.ExpiresAt) { + r.logger().Warn("auth: expired api key used", "prefix", prefix) + return auth.Principal{}, false + } + if key.LastUsedAt == nil || now.Sub(*key.LastUsedAt) > lastUsedGranularity { + if err := r.Keys.TouchLastUsed(ctx, key.ID, now); err != nil { + r.logger().Error("auth: touch api key failed", "error", err) + } + } + + if key.TeamID == nil { + return auth.Principal{ + Kind: auth.KindAPIKey, + Username: "apikey:" + prefix, + Permissions: auth.NewPermissionSet(auth.AllPermissions()...), + Scope: auth.ScopeAll(), + IsAdmin: true, + KeyPrefix: prefix, + }, true + } + teams, err := r.Teams.GetByIDs(ctx, []primitive.ObjectID{*key.TeamID}) + if err != nil { + r.logger().Error("auth: team lookup failed", "error", err) + return auth.Principal{}, false + } + if len(teams) == 0 { + r.logger().Warn("auth: api key belongs to a deleted team", "prefix", prefix) + return auth.Principal{}, false + } + // A key attached to the built-in Administrators team inherits its admin + // flag, exactly like a user of that team. Spec 4.1 and the comment on + // auth.Principal.IsAdmin both say so, and a key holding access:manage is + // an administrator credential in practice anyway. + perms, scope, admin := Effective(teams) + return auth.Principal{ + Kind: auth.KindAPIKey, + Username: "apikey:" + prefix, + TeamIDs: []string{key.TeamID.Hex()}, + Permissions: perms, + Scope: scope, + IsAdmin: admin, + KeyPrefix: prefix, + }, true +} + +func (r *Resolver) resolveSession(ctx context.Context, token string) (auth.Principal, bool) { + sess, err := r.Sessions.Verify(token) + if err != nil { + return auth.Principal{}, false + } + id, err := primitive.ObjectIDFromHex(sess.UserID) + if err != nil { + return auth.Principal{}, false + } + user, err := r.Users.GetByID(ctx, id) + if err != nil { + if !errors.Is(err, store.ErrNotFound) { + r.logger().Error("auth: user lookup failed", "error", err) + } + return auth.Principal{}, false + } + if user.Disabled || user.SessionVersion != sess.SessionVersion { + return auth.Principal{}, false + } + p, err := r.PrincipalForUser(ctx, user) + if err != nil { + r.logger().Error("auth: team lookup failed", "error", err) + return auth.Principal{}, false + } + return p, true +} + +// PrincipalForUser builds the principal of a user from its teams. +func (r *Resolver) PrincipalForUser(ctx context.Context, user *store.User) (auth.Principal, error) { + teams, err := r.Teams.GetByIDs(ctx, user.Teams) + if err != nil { + return auth.Principal{}, err + } + perms, scope, admin := Effective(teams) + teamIDs := make([]string, 0, len(teams)) + for _, t := range teams { + teamIDs = append(teamIDs, t.ID.Hex()) + } + return auth.Principal{ + Kind: auth.KindUser, + UserID: user.ID.Hex(), + Username: user.Username, + TeamIDs: teamIDs, + Permissions: perms, + Scope: scope, + IsAdmin: admin, + }, nil +} diff --git a/internal/auth/identity/resolver_test.go b/internal/auth/identity/resolver_test.go new file mode 100644 index 00000000..9567d199 --- /dev/null +++ b/internal/auth/identity/resolver_test.go @@ -0,0 +1,207 @@ +package identity + +import ( + "bytes" + "context" + "testing" + "time" + + "github.com/bananaops/tracker/internal/auth" + store "github.com/bananaops/tracker/internal/stores" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type fakeUsers struct { + byID map[primitive.ObjectID]*store.User +} + +func (f *fakeUsers) GetByID(_ context.Context, id primitive.ObjectID) (*store.User, error) { + if u, ok := f.byID[id]; ok { + return u, nil + } + return nil, store.ErrNotFound +} + +type fakeTeams struct { + byID map[primitive.ObjectID]*store.Team +} + +func (f *fakeTeams) GetByIDs(_ context.Context, ids []primitive.ObjectID) ([]*store.Team, error) { + var out []*store.Team + for _, id := range ids { + if t, ok := f.byID[id]; ok { + out = append(out, t) + } + } + return out, nil +} + +type fakeKeys struct { + byPrefix map[string]*store.APIKey + touched int +} + +func (f *fakeKeys) GetByPrefix(_ context.Context, prefix string) (*store.APIKey, error) { + if k, ok := f.byPrefix[prefix]; ok { + return k, nil + } + return nil, store.ErrNotFound +} + +func (f *fakeKeys) TouchLastUsed(_ context.Context, _ primitive.ObjectID, _ time.Time) error { + f.touched++ + return nil +} + +func newFixture(t *testing.T) (*Resolver, *store.User, *store.Team, *fakeKeys) { + t.Helper() + team := &store.Team{ID: primitive.NewObjectID(), Name: "ops", Permissions: []string{"event:read"}, Scope: store.TeamScope{Services: []string{"api"}}} + admins := &store.Team{ID: primitive.NewObjectID(), Name: "Administrators", Builtin: true, Permissions: []string{"access:manage"}, Scope: store.TeamScope{All: true}} + user := &store.User{ID: primitive.NewObjectID(), Username: "alice", Teams: []primitive.ObjectID{team.ID}, SessionVersion: 2} + sessions, err := auth.NewSessionManager(bytes.Repeat([]byte{3}, 32), time.Hour) + require.NoError(t, err) + keys := &fakeKeys{byPrefix: map[string]*store.APIKey{}} + r := &Resolver{ + Users: &fakeUsers{byID: map[primitive.ObjectID]*store.User{user.ID: user}}, + Teams: &fakeTeams{byID: map[primitive.ObjectID]*store.Team{team.ID: team, admins.ID: admins}}, + Keys: keys, + Sessions: sessions, + AnonymousPermissions: []auth.Permission{auth.PermLinksRead}, + Now: time.Now, + } + return r, user, team, keys +} + +func TestResolveAnonymous(t *testing.T) { + r, _, _, _ := newFixture(t) + p := r.Resolve(context.Background(), auth.Credentials{}) + assert.Equal(t, auth.KindAnonymous, p.Kind) + assert.True(t, p.Has(auth.PermLinksRead)) + assert.False(t, p.Has(auth.PermEventRead)) +} + +func TestResolveSession(t *testing.T) { + r, user, team, _ := newFixture(t) + token, _, _ := r.Sessions.Issue(user.ID.Hex(), user.SessionVersion) + + p := r.Resolve(context.Background(), auth.Credentials{SessionToken: token}) + assert.Equal(t, auth.KindUser, p.Kind) + assert.Equal(t, "alice", p.Username) + assert.Equal(t, []string{team.ID.Hex()}, p.TeamIDs) + assert.True(t, p.Has(auth.PermEventRead)) + assert.True(t, p.Scope.Allows("api")) + assert.False(t, p.Scope.Allows("web")) + assert.False(t, p.IsAdmin) + + // A bearer token is explicit: once it stops resolving it is refused. + stale, _, _ := r.Sessions.Issue(user.ID.Hex(), user.SessionVersion-1) + assert.True(t, r.Resolve(context.Background(), auth.Credentials{SessionToken: stale}).CredentialRejected) + + user.Disabled = true + assert.True(t, r.Resolve(context.Background(), auth.Credentials{SessionToken: token}).CredentialRejected) + user.Disabled = false + + assert.True(t, r.Resolve(context.Background(), auth.Credentials{SessionToken: "garbage"}).CredentialRejected) +} + +// The session cookie is ambient: a browser keeps sending a stale one, and +// refusing it would serve a 401 for the SPA itself, login page included. It +// must degrade to anonymous, unlike the same token sent as a bearer. +func TestResolveStaleCookieFallsBackToAnonymous(t *testing.T) { + r, user, _, _ := newFixture(t) + stale, _, _ := r.Sessions.Issue(user.ID.Hex(), user.SessionVersion-1) + + p := r.Resolve(context.Background(), auth.Credentials{SessionToken: stale, FromCookie: true}) + assert.Equal(t, auth.KindAnonymous, p.Kind) + assert.False(t, p.CredentialRejected) + assert.True(t, p.Has(auth.PermLinksRead), "anonymous permissions still apply") + + p = r.Resolve(context.Background(), auth.Credentials{SessionToken: "garbage", FromCookie: true}) + assert.Equal(t, auth.KindAnonymous, p.Kind) + assert.False(t, p.CredentialRejected) +} + +func TestResolveAPIKey(t *testing.T) { + r, _, team, keys := newFixture(t) + gen, _ := auth.GenerateAPIKey() + keys.byPrefix[gen.Prefix] = &store.APIKey{ID: primitive.NewObjectID(), Prefix: gen.Prefix, Hash: gen.Hash, TeamID: &team.ID} + + p := r.Resolve(context.Background(), auth.Credentials{APIKey: gen.Secret}) + assert.Equal(t, auth.KindAPIKey, p.Kind) + assert.Equal(t, "apikey:"+gen.Prefix, p.Username) + assert.Equal(t, gen.Prefix, p.KeyPrefix) + assert.True(t, p.Has(auth.PermEventRead)) + assert.False(t, p.IsAdmin) + assert.Equal(t, 1, keys.touched) + + // Second use within a minute does not touch again. + now := time.Now() + keys.byPrefix[gen.Prefix].LastUsedAt = &now + r.Resolve(context.Background(), auth.Credentials{APIKey: gen.Secret}) + assert.Equal(t, 1, keys.touched) + + wrong := gen.Secret[:len(gen.Secret)-1] + "x" + assert.True(t, r.Resolve(context.Background(), auth.Credentials{APIKey: wrong}).CredentialRejected) + + // A revoked key must not inherit the anonymous permissions, which under + // the transitional default are wider than what the key itself carried. + revoked := now + keys.byPrefix[gen.Prefix].RevokedAt = &revoked + rejected := r.Resolve(context.Background(), auth.Credentials{APIKey: gen.Secret}) + assert.True(t, rejected.CredentialRejected) + assert.False(t, rejected.Has(auth.PermLinksRead), "a dead key gains nothing from AUTH_ANONYMOUS_PERMISSIONS") + keys.byPrefix[gen.Prefix].RevokedAt = nil + + past := now.Add(-time.Minute) + keys.byPrefix[gen.Prefix].ExpiresAt = &past + assert.True(t, r.Resolve(context.Background(), auth.Credentials{APIKey: gen.Secret}).CredentialRejected) + keys.byPrefix[gen.Prefix].ExpiresAt = nil + + // Global key. + global, _ := auth.GenerateAPIKey() + keys.byPrefix[global.Prefix] = &store.APIKey{ID: primitive.NewObjectID(), Prefix: global.Prefix, Hash: global.Hash} + p = r.Resolve(context.Background(), auth.Credentials{APIKey: global.Secret}) + assert.True(t, p.IsAdmin) + assert.True(t, p.Scope.All) + assert.True(t, p.Has(auth.PermAccessManage)) + + // Unknown prefix and malformed key. + assert.True(t, r.Resolve(context.Background(), auth.Credentials{APIKey: "trk_zzzzzzzz_nothing"}).CredentialRejected) + assert.True(t, r.Resolve(context.Background(), auth.Credentials{APIKey: "trk_bad"}).CredentialRejected) +} + +// A key attached to the built-in Administrators team is an administrator +// credential, as spec 4.1 and the comment on auth.Principal.IsAdmin say. Only +// the built-in flag grants it, not the permissions the team happens to hold. +func TestResolveAPIKeyOnAdministratorsTeam(t *testing.T) { + r, _, team, keys := newFixture(t) + admins := &store.Team{ + ID: primitive.NewObjectID(), + Name: store.AdministratorsTeamName, + Builtin: true, + Permissions: []string{string(auth.PermAccessManage)}, + Scope: store.TeamScope{All: true}, + } + r.Teams.(*fakeTeams).byID[admins.ID] = admins + + adminKey, _ := auth.GenerateAPIKey() + keys.byPrefix[adminKey.Prefix] = &store.APIKey{ + ID: primitive.NewObjectID(), Prefix: adminKey.Prefix, Hash: adminKey.Hash, TeamID: &admins.ID, + } + + p := r.Resolve(context.Background(), auth.Credentials{APIKey: adminKey.Secret}) + require.Equal(t, auth.KindAPIKey, p.Kind) + assert.True(t, p.IsAdmin, "a key on the built-in team inherits the admin flag") + assert.Equal(t, []string{admins.ID.Hex()}, p.TeamIDs) + assert.True(t, p.Has(auth.PermAccessManage)) + assert.True(t, p.Scope.All) + + // A key on an ordinary team stays a plain credential. + teamKey, _ := auth.GenerateAPIKey() + keys.byPrefix[teamKey.Prefix] = &store.APIKey{ + ID: primitive.NewObjectID(), Prefix: teamKey.Prefix, Hash: teamKey.Hash, TeamID: &team.ID, + } + assert.False(t, r.Resolve(context.Background(), auth.Credentials{APIKey: teamKey.Secret}).IsAdmin) +} diff --git a/internal/auth/password.go b/internal/auth/password.go new file mode 100644 index 00000000..be7a39ac --- /dev/null +++ b/internal/auth/password.go @@ -0,0 +1,95 @@ +package auth + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "errors" + "fmt" + "strings" + "sync" + "unicode/utf8" + + "golang.org/x/crypto/argon2" +) + +const ( + PasswordMinLength = 12 + PasswordMaxLength = 128 + + argonTime uint32 = 3 + argonMemory uint32 = 64 * 1024 + argonThreads uint8 = 2 + argonKeyLen uint32 = 32 + argonSaltLen = 16 +) + +// ErrPasswordPolicy is returned when a password does not meet the length policy. +var ErrPasswordPolicy = fmt.Errorf("password must be between %d and %d characters", PasswordMinLength, PasswordMaxLength) + +// ValidatePasswordPolicy checks the password length policy. +func ValidatePasswordPolicy(password string) error { + n := utf8.RuneCountInString(password) + if n < PasswordMinLength || n > PasswordMaxLength { + return ErrPasswordPolicy + } + return nil +} + +// HashPassword returns an Argon2id hash in PHC string format. +func HashPassword(password string) (string, error) { + if err := ValidatePasswordPolicy(password); err != nil { + return "", err + } + salt := make([]byte, argonSaltLen) + if _, err := rand.Read(salt); err != nil { + return "", fmt.Errorf("generate salt: %w", err) + } + key := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen) + return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + argon2.Version, argonMemory, argonTime, argonThreads, + base64.RawStdEncoding.EncodeToString(salt), + base64.RawStdEncoding.EncodeToString(key), + ), nil +} + +// VerifyPassword compares a password with a PHC encoded Argon2id hash. +func VerifyPassword(encoded, password string) (bool, error) { + parts := strings.Split(encoded, "$") + if len(parts) != 6 || parts[1] != "argon2id" { + return false, errors.New("unsupported password hash format") + } + var version int + if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil || version != argon2.Version { + return false, errors.New("unsupported argon2 version") + } + var memory, iterations uint32 + var threads uint8 + if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &iterations, &threads); err != nil { + return false, fmt.Errorf("invalid argon2 parameters: %w", err) + } + salt, err := base64.RawStdEncoding.DecodeString(parts[4]) + if err != nil { + return false, fmt.Errorf("invalid salt: %w", err) + } + expected, err := base64.RawStdEncoding.DecodeString(parts[5]) + if err != nil { + return false, fmt.Errorf("invalid hash: %w", err) + } + key := argon2.IDKey([]byte(password), salt, iterations, memory, threads, uint32(len(expected))) + return subtle.ConstantTimeCompare(key, expected) == 1, nil +} + +var ( + dummyHashOnce sync.Once + dummyHash string +) + +// DummyVerify burns the same CPU time as a real verification. Call it when the +// user does not exist so that login timing does not reveal valid usernames. +func DummyVerify(password string) { + dummyHashOnce.Do(func() { + dummyHash, _ = HashPassword("tracker-dummy-password-for-timing") + }) + _, _ = VerifyPassword(dummyHash, password) +} diff --git a/internal/auth/password_test.go b/internal/auth/password_test.go new file mode 100644 index 00000000..f098ec99 --- /dev/null +++ b/internal/auth/password_test.go @@ -0,0 +1,46 @@ +package auth + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHashAndVerifyPassword(t *testing.T) { + hash, err := HashPassword("correct horse battery") + require.NoError(t, err) + assert.True(t, strings.HasPrefix(hash, "$argon2id$v=19$m=65536,t=3,p=2$")) + + ok, err := VerifyPassword(hash, "correct horse battery") + require.NoError(t, err) + assert.True(t, ok) + + ok, err = VerifyPassword(hash, "wrong horse battery!") + require.NoError(t, err) + assert.False(t, ok) + + other, err := HashPassword("correct horse battery") + require.NoError(t, err) + assert.NotEqual(t, hash, other, "salt must be random") +} + +func TestPasswordPolicy(t *testing.T) { + assert.ErrorIs(t, ValidatePasswordPolicy("short"), ErrPasswordPolicy) + assert.ErrorIs(t, ValidatePasswordPolicy(strings.Repeat("x", 129)), ErrPasswordPolicy) + assert.NoError(t, ValidatePasswordPolicy("exactly12chr")) + _, err := HashPassword("short") + assert.ErrorIs(t, err, ErrPasswordPolicy) +} + +func TestVerifyPasswordRejectsGarbage(t *testing.T) { + _, err := VerifyPassword("not-a-phc-string", "whatever-password") + assert.Error(t, err) + _, err = VerifyPassword("$bcrypt$foo", "whatever-password") + assert.Error(t, err) +} + +func TestDummyVerifyDoesNotPanic(t *testing.T) { + DummyVerify("anything") +} diff --git a/internal/auth/permission.go b/internal/auth/permission.go new file mode 100644 index 00000000..805c18ac --- /dev/null +++ b/internal/auth/permission.go @@ -0,0 +1,104 @@ +// Package auth contains the transport-agnostic building blocks of Tracker +// authentication: permissions, principals, passwords, API keys and sessions. +package auth + +import ( + "fmt" + "sort" + "strings" +) + +// Permission is a right granted to a team, formatted as "domain:action". +type Permission string + +const ( + PermEventRead Permission = "event:read" + PermEventWrite Permission = "event:write" + PermCatalogRead Permission = "catalog:read" + PermCatalogWrite Permission = "catalog:write" + PermLockRead Permission = "lock:read" + PermLockWrite Permission = "lock:write" + PermLinksRead Permission = "links:read" + PermLinksWrite Permission = "links:write" + PermAccessManage Permission = "access:manage" + + // PermPublic and PermAuthenticated are pseudo permissions used only in the + // authorization table. They can never be granted to a team. + PermPublic Permission = "public" + PermAuthenticated Permission = "authenticated" +) + +var allPermissions = []Permission{ + PermEventRead, PermEventWrite, + PermCatalogRead, PermCatalogWrite, + PermLockRead, PermLockWrite, + PermLinksRead, PermLinksWrite, + PermAccessManage, +} + +// AllPermissions returns a copy of every grantable permission. +func AllPermissions() []Permission { + out := make([]Permission, len(allPermissions)) + copy(out, allPermissions) + return out +} + +// IsValidPermission reports whether p is a grantable permission. +func IsValidPermission(p Permission) bool { + for _, known := range allPermissions { + if known == p { + return true + } + } + return false +} + +// ParsePermissions parses a comma separated list such as "event:read,lock:write". +// Blank items are ignored. An unknown permission is an error. +func ParsePermissions(raw string) ([]Permission, error) { + out := []Permission{} + for _, part := range strings.Split(raw, ",") { + p := Permission(strings.TrimSpace(part)) + if p == "" { + continue + } + if !IsValidPermission(p) { + return nil, fmt.Errorf("unknown permission %q", p) + } + out = append(out, p) + } + return out, nil +} + +// PermissionSet is an unordered set of permissions. +type PermissionSet map[Permission]struct{} + +// NewPermissionSet builds a set from the given permissions. +func NewPermissionSet(perms ...Permission) PermissionSet { + s := PermissionSet{} + s.Add(perms...) + return s +} + +// Has reports whether p is in the set. +func (s PermissionSet) Has(p Permission) bool { + _, ok := s[p] + return ok +} + +// Add inserts permissions into the set. +func (s PermissionSet) Add(perms ...Permission) { + for _, p := range perms { + s[p] = struct{}{} + } +} + +// Slice returns the permissions sorted alphabetically. +func (s PermissionSet) Slice() []Permission { + out := make([]Permission, 0, len(s)) + for p := range s { + out = append(out, p) + } + sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) + return out +} diff --git a/internal/auth/permission_test.go b/internal/auth/permission_test.go new file mode 100644 index 00000000..92cee474 --- /dev/null +++ b/internal/auth/permission_test.go @@ -0,0 +1,37 @@ +package auth + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParsePermissions(t *testing.T) { + perms, err := ParsePermissions(" event:read, catalog:write ,, ") + require.NoError(t, err) + assert.Equal(t, []Permission{PermEventRead, PermCatalogWrite}, perms) + + perms, err = ParsePermissions("") + require.NoError(t, err) + assert.Empty(t, perms) + + _, err = ParsePermissions("event:read,bogus") + assert.EqualError(t, err, `unknown permission "bogus"`) +} + +func TestPermissionSet(t *testing.T) { + s := NewPermissionSet(PermLockWrite, PermEventRead) + assert.True(t, s.Has(PermEventRead)) + assert.False(t, s.Has(PermEventWrite)) + s.Add(PermEventWrite) + assert.True(t, s.Has(PermEventWrite)) + assert.Equal(t, []Permission{PermEventRead, PermEventWrite, PermLockWrite}, s.Slice()) +} + +func TestAllPermissionsIsACopy(t *testing.T) { + all := AllPermissions() + all[0] = "tampered" + assert.True(t, IsValidPermission(PermEventRead)) + assert.False(t, IsValidPermission(PermPublic), "pseudo permissions are not grantable") +} diff --git a/internal/auth/principal.go b/internal/auth/principal.go new file mode 100644 index 00000000..17cb491c --- /dev/null +++ b/internal/auth/principal.go @@ -0,0 +1,87 @@ +package auth + +import "context" + +// Kind tells how a principal was authenticated. +type Kind string + +const ( + KindAnonymous Kind = "anonymous" + KindUser Kind = "user" + KindAPIKey Kind = "apikey" +) + +// Principal is the resolved identity of a request, whatever the transport. +type Principal struct { + Kind Kind + UserID string + Username string + TeamIDs []string + Permissions PermissionSet + Scope Scope + // IsAdmin is true for members of the built-in Administrators team and for global API keys. + IsAdmin bool + // KeyPrefix is set when Kind is KindAPIKey, for logging. + KeyPrefix string + // CredentialRejected is true when the request presented an explicit + // credential that could not be honoured. Authorization turns it into a + // 401 whatever the permission asked for, including a public one. + CredentialRejected bool +} + +// Anonymous returns the principal used for unauthenticated requests. +func Anonymous(perms []Permission) Principal { + return Principal{ + Kind: KindAnonymous, + Username: "anonymous", + Permissions: NewPermissionSet(perms...), + Scope: ScopeAll(), + } +} + +// RejectedCredential is the principal of a request that presented an explicit +// credential, an API key or a bearer token, which could not be honoured: +// malformed, unknown, revoked or expired. +// +// It is deliberately not the anonymous principal. Falling back to anonymous +// would hand the caller whatever AUTH_ANONYMOUS_PERMISSIONS grants, which +// under the transitional default is more than most credentials carry: a key +// restricted to event:read would silently gain event:write the moment it is +// revoked. A dead credential must be refused, not upgraded. +// +// An absent credential is not rejected, and neither is a session cookie that +// no longer resolves: the cookie is ambient, and a browser holding a stale one +// must still be able to load the SPA and its login page. See +// Credentials.FromCookie. +func RejectedCredential() Principal { + return Principal{ + Kind: KindAnonymous, + Username: "anonymous", + Permissions: NewPermissionSet(), + Scope: ScopeAll(), + CredentialRejected: true, + } +} + +// IsAuthenticated reports whether the principal is a user or an API key. +func (p Principal) IsAuthenticated() bool { + return p.Kind == KindUser || p.Kind == KindAPIKey +} + +// Has reports whether the principal holds the permission. +func (p Principal) Has(perm Permission) bool { + return p.Permissions.Has(perm) +} + +type principalKey struct{} + +// WithPrincipal stores the principal in the context. +func WithPrincipal(ctx context.Context, p Principal) context.Context { + return context.WithValue(ctx, principalKey{}, p) +} + +// FromContext returns the principal stored by WithPrincipal. +func FromContext(ctx context.Context) (Principal, bool) { + p, ok := ctx.Value(principalKey{}).(Principal) + return p, ok +} diff --git a/internal/auth/principal_test.go b/internal/auth/principal_test.go new file mode 100644 index 00000000..2233ad50 --- /dev/null +++ b/internal/auth/principal_test.go @@ -0,0 +1,29 @@ +package auth + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAnonymousPrincipal(t *testing.T) { + p := Anonymous([]Permission{PermEventRead}) + assert.False(t, p.IsAuthenticated()) + assert.True(t, p.Has(PermEventRead)) + assert.False(t, p.Has(PermEventWrite)) + assert.True(t, p.Scope.All) + assert.Equal(t, "anonymous", p.Username) +} + +func TestPrincipalContext(t *testing.T) { + _, ok := FromContext(context.Background()) + assert.False(t, ok) + + p := Principal{Kind: KindUser, UserID: "42", Username: "alice", Permissions: NewPermissionSet(PermLockRead)} + ctx := WithPrincipal(context.Background(), p) + got, ok := FromContext(ctx) + assert.True(t, ok) + assert.Equal(t, "alice", got.Username) + assert.True(t, got.IsAuthenticated()) +} diff --git a/internal/auth/ratelimit.go b/internal/auth/ratelimit.go new file mode 100644 index 00000000..4aeff455 --- /dev/null +++ b/internal/auth/ratelimit.go @@ -0,0 +1,95 @@ +package auth + +import ( + "sync" + "time" +) + +const ( + // sweepEveryInsertions and sweepInterval bound the memory of the limiter: + // whichever comes first triggers a full pass over the map. + sweepEveryInsertions = 1000 + sweepInterval = time.Minute +) + +// LoginLimiter blocks a key after too many failures inside a sliding window. +// It is in-memory and per process, which is enough to slow down online guessing. +type LoginLimiter struct { + mu sync.Mutex + maxFailures int + window time.Duration + failures map[string][]time.Time + // insertions counts the failures recorded since the last sweep, and + // lastSweep dates that sweep. See maybeSweep. + insertions int + lastSweep time.Time + // Now is overridable in tests. + Now func() time.Time +} + +// NewLoginLimiter creates a limiter allowing maxFailures per window. +func NewLoginLimiter(maxFailures int, window time.Duration) *LoginLimiter { + return &LoginLimiter{maxFailures: maxFailures, window: window, failures: map[string][]time.Time{}, Now: time.Now} +} + +// Blocked reports whether the key has reached the failure budget. +func (l *LoginLimiter) Blocked(key string) bool { + l.mu.Lock() + defer l.mu.Unlock() + now := l.Now() + l.prune(key, now) + l.maybeSweep(now) + return len(l.failures[key]) >= l.maxFailures +} + +// RecordFailure adds a failed attempt for the key. +func (l *LoginLimiter) RecordFailure(key string) { + l.mu.Lock() + defer l.mu.Unlock() + now := l.Now() + l.prune(key, now) + l.failures[key] = append(l.failures[key], now) + l.insertions++ + l.maybeSweep(now) +} + +// Reset forgets the failures of the key, after a successful login. +func (l *LoginLimiter) Reset(key string) { + l.mu.Lock() + defer l.mu.Unlock() + delete(l.failures, key) +} + +// prune drops the failures of one key that left the window, and removes the +// key entirely when nothing is left. The caller holds the lock. +func (l *LoginLimiter) prune(key string, now time.Time) { + kept := l.failures[key][:0] + for _, at := range l.failures[key] { + if now.Sub(at) < l.window { + kept = append(kept, at) + } + } + if len(kept) == 0 { + delete(l.failures, key) + return + } + l.failures[key] = kept +} + +// maybeSweep prunes every key, not just the one being looked at. Without it +// the map only ever shrinks for keys somebody retries, so a caller varying +// the username or the IP grows it without bound. The caller holds the lock. +func (l *LoginLimiter) maybeSweep(now time.Time) { + if l.lastSweep.IsZero() { + l.lastSweep = now + return + } + if l.insertions < sweepEveryInsertions && now.Sub(l.lastSweep) < sweepInterval { + return + } + l.insertions = 0 + l.lastSweep = now + for key := range l.failures { + l.prune(key, now) + } +} diff --git a/internal/auth/ratelimit_test.go b/internal/auth/ratelimit_test.go new file mode 100644 index 00000000..7c7af48f --- /dev/null +++ b/internal/auth/ratelimit_test.go @@ -0,0 +1,68 @@ +package auth + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestLoginLimiter(t *testing.T) { + l := NewLoginLimiter(3, time.Minute) + now := time.Now() + l.Now = func() time.Time { return now } + + assert.False(t, l.Blocked("alice|1.1.1.1")) + l.RecordFailure("alice|1.1.1.1") + l.RecordFailure("alice|1.1.1.1") + assert.False(t, l.Blocked("alice|1.1.1.1")) + l.RecordFailure("alice|1.1.1.1") + assert.True(t, l.Blocked("alice|1.1.1.1")) + assert.False(t, l.Blocked("bob|1.1.1.1"), "keys are independent") + + now = now.Add(61 * time.Second) + assert.False(t, l.Blocked("alice|1.1.1.1"), "failures expire with the window") + + l.RecordFailure("alice|1.1.1.1") + l.RecordFailure("alice|1.1.1.1") + l.RecordFailure("alice|1.1.1.1") + assert.True(t, l.Blocked("alice|1.1.1.1")) + l.Reset("alice|1.1.1.1") + assert.False(t, l.Blocked("alice|1.1.1.1")) +} + +func TestLoginLimiterSweepsExpiredKeys(t *testing.T) { + l := NewLoginLimiter(3, time.Minute) + now := time.Now() + l.Now = func() time.Time { return now } + + // An attacker varying the username creates one key per attempt. + for i := 0; i < 500; i++ { + l.RecordFailure(fmt.Sprintf("user%d|203.0.113.9", i)) + } + assert.Equal(t, 500, l.size()) + + // Neither the insertion budget nor the sweep delay is reached yet. + now = now.Add(30 * time.Second) + l.RecordFailure("late|203.0.113.9") + assert.Equal(t, 501, l.size(), "no sweep before the sweep interval") + + // Past the sweep interval, every key whose failures left the window goes. + now = now.Add(2 * time.Minute) + l.RecordFailure("fresh|203.0.113.9") + assert.Equal(t, 1, l.size(), "only the key recorded just now survives") + + // Blocked sweeps too, so a read only workload cannot leak either. + now = now.Add(2 * time.Minute) + assert.False(t, l.Blocked("someone|203.0.113.9")) + assert.Equal(t, 0, l.size()) +} + +// size is the number of tracked keys, exposed to the tests only so the +// production type carries no unused method. +func (l *LoginLimiter) size() int { + l.mu.Lock() + defer l.mu.Unlock() + return len(l.failures) +} diff --git a/internal/auth/scope.go b/internal/auth/scope.go new file mode 100644 index 00000000..c09a50f5 --- /dev/null +++ b/internal/auth/scope.go @@ -0,0 +1,58 @@ +package auth + +import "sort" + +// Scope restricts a principal to a set of catalog services. +// All=true means every service, including ones that do not exist yet. +type Scope struct { + All bool + Services map[string]struct{} +} + +// ScopeAll returns an unrestricted scope. +func ScopeAll() Scope { + return Scope{All: true, Services: map[string]struct{}{}} +} + +// ScopeOf returns a scope restricted to the given services. +func ScopeOf(services ...string) Scope { + s := Scope{Services: make(map[string]struct{}, len(services))} + for _, svc := range services { + s.Services[svc] = struct{}{} + } + return s +} + +// Allows reports whether the service is inside the scope. +func (s Scope) Allows(service string) bool { + if s.All { + return true + } + _, ok := s.Services[service] + return ok +} + +// Union returns a scope allowing everything s or o allows. +func (s Scope) Union(o Scope) Scope { + if s.All || o.All { + return ScopeAll() + } + out := ScopeOf() + for svc := range s.Services { + out.Services[svc] = struct{}{} + } + for svc := range o.Services { + out.Services[svc] = struct{}{} + } + return out +} + +// ServiceList returns the explicitly allowed services, sorted. +func (s Scope) ServiceList() []string { + out := make([]string, 0, len(s.Services)) + for svc := range s.Services { + out = append(out, svc) + } + sort.Strings(out) + return out +} diff --git a/internal/auth/scope_test.go b/internal/auth/scope_test.go new file mode 100644 index 00000000..a419ccb9 --- /dev/null +++ b/internal/auth/scope_test.go @@ -0,0 +1,24 @@ +package auth + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestScope(t *testing.T) { + all := ScopeAll() + assert.True(t, all.Allows("anything")) + + partial := ScopeOf("api", "web") + assert.True(t, partial.Allows("api")) + assert.False(t, partial.Allows("batch")) + assert.Equal(t, []string{"api", "web"}, partial.ServiceList()) + + union := partial.Union(ScopeOf("batch")) + assert.True(t, union.Allows("batch")) + assert.False(t, union.All) + + assert.True(t, partial.Union(all).All) + assert.False(t, ScopeOf().Allows("api"), "empty scope allows nothing") +} diff --git a/internal/auth/session.go b/internal/auth/session.go new file mode 100644 index 00000000..ab734514 --- /dev/null +++ b/internal/auth/session.go @@ -0,0 +1,87 @@ +package auth + +import ( + "errors" + "fmt" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +// SessionSecretLength is the minimum HMAC secret size in bytes. +const SessionSecretLength = 32 + +// ErrInvalidSession is returned for any token that cannot be trusted. +var ErrInvalidSession = errors.New("invalid session") + +// SessionManager issues and verifies HS256 session tokens. +type SessionManager struct { + secret []byte + ttl time.Duration + // Now is overridable in tests. + Now func() time.Time +} + +// Session is the verified content of a token. +type Session struct { + UserID string + SessionVersion int + ExpiresAt time.Time +} + +type sessionClaims struct { + jwt.RegisteredClaims + SessionVersion int `json:"sv"` +} + +// NewSessionManager validates the secret and TTL. +func NewSessionManager(secret []byte, ttl time.Duration) (*SessionManager, error) { + if len(secret) < SessionSecretLength { + return nil, fmt.Errorf("session secret must be at least %d bytes", SessionSecretLength) + } + if ttl <= 0 { + return nil, errors.New("session ttl must be positive") + } + return &SessionManager{secret: secret, ttl: ttl, Now: time.Now}, nil +} + +// TTL returns the configured session lifetime. +func (m *SessionManager) TTL() time.Duration { return m.ttl } + +// Issue signs a token for the user. +func (m *SessionManager) Issue(userID string, sessionVersion int) (string, time.Time, error) { + now := m.Now() + expires := now.Add(m.ttl) + claims := sessionClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + Subject: userID, + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(expires), + }, + SessionVersion: sessionVersion, + } + token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(m.secret) + if err != nil { + return "", time.Time{}, fmt.Errorf("sign session: %w", err) + } + return token, expires, nil +} + +// Verify parses and validates a token. +func (m *SessionManager) Verify(token string) (Session, error) { + var claims sessionClaims + parsed, err := jwt.ParseWithClaims(token, &claims, func(t *jwt.Token) (any, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, ErrInvalidSession + } + return m.secret, nil + }, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}), jwt.WithTimeFunc(m.Now), jwt.WithExpirationRequired()) + if err != nil || !parsed.Valid || claims.Subject == "" { + return Session{}, ErrInvalidSession + } + return Session{ + UserID: claims.Subject, + SessionVersion: claims.SessionVersion, + ExpiresAt: claims.ExpiresAt.Time, + }, nil +} diff --git a/internal/auth/session_test.go b/internal/auth/session_test.go new file mode 100644 index 00000000..9f4dc71b --- /dev/null +++ b/internal/auth/session_test.go @@ -0,0 +1,56 @@ +package auth + +import ( + "bytes" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func testSecret() []byte { return bytes.Repeat([]byte{7}, 32) } + +func TestSessionRoundTrip(t *testing.T) { + m, err := NewSessionManager(testSecret(), time.Hour) + require.NoError(t, err) + now := time.Date(2026, 9, 5, 10, 0, 0, 0, time.UTC) + m.Now = func() time.Time { return now } + + token, expires, err := m.Issue("user-1", 3) + require.NoError(t, err) + assert.Equal(t, now.Add(time.Hour), expires) + + s, err := m.Verify(token) + require.NoError(t, err) + assert.Equal(t, "user-1", s.UserID) + assert.Equal(t, 3, s.SessionVersion) + assert.Equal(t, expires.Unix(), s.ExpiresAt.Unix()) +} + +func TestSessionExpired(t *testing.T) { + m, _ := NewSessionManager(testSecret(), time.Hour) + now := time.Now() + m.Now = func() time.Time { return now } + token, _, _ := m.Issue("user-1", 1) + m.Now = func() time.Time { return now.Add(2 * time.Hour) } + _, err := m.Verify(token) + assert.ErrorIs(t, err, ErrInvalidSession) +} + +func TestSessionWrongSecret(t *testing.T) { + a, _ := NewSessionManager(testSecret(), time.Hour) + b, _ := NewSessionManager(bytes.Repeat([]byte{9}, 32), time.Hour) + token, _, _ := a.Issue("user-1", 1) + _, err := b.Verify(token) + assert.ErrorIs(t, err, ErrInvalidSession) + _, err = a.Verify("garbage") + assert.ErrorIs(t, err, ErrInvalidSession) +} + +func TestSessionManagerValidation(t *testing.T) { + _, err := NewSessionManager([]byte("short"), time.Hour) + assert.Error(t, err) + _, err = NewSessionManager(testSecret(), 0) + assert.Error(t, err) +} diff --git a/internal/auth/transport.go b/internal/auth/transport.go new file mode 100644 index 00000000..eaea9e7a --- /dev/null +++ b/internal/auth/transport.go @@ -0,0 +1,72 @@ +package auth + +import ( + "context" + "log/slog" + "net/http" + + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +// Resolver turns raw credentials into a principal. It never fails: an invalid +// credential yields the anonymous principal. +type Resolver interface { + Resolve(ctx context.Context, creds Credentials) Principal +} + +// ResolverFunc adapts a function to the Resolver interface. +type ResolverFunc func(ctx context.Context, creds Credentials) Principal + +// Resolve implements Resolver. +func (f ResolverFunc) Resolve(ctx context.Context, creds Credentials) Principal { return f(ctx, creds) } + +// HTTPMiddleware resolves the principal of every HTTP request and stores it in +// the request context. It never rejects a request: authorization happens later. +// +// The configuration is only read for the cross-site guard: a session cookie +// presented on a cross-site browser request is dropped, so the request +// resolves to the anonymous principal instead of the logged in user. It fails +// closed to anonymous rather than to 403 so that a cross-site GET of a public +// route keeps working. +func HTTPMiddleware(r Resolver, cfg Config) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + creds := CredentialsFromHTTP(req) + if creds.FromCookie && IsCrossSite(req, cfg.PublicURL, cfg.TrustProxy) { + slog.Debug("auth: ignoring the session cookie of a cross-site request", + "method", req.Method, "path", req.URL.Path, + "origin", req.Header.Get("Origin"), "sec_fetch_site", req.Header.Get("Sec-Fetch-Site")) + creds = Credentials{} + } + p := r.Resolve(req.Context(), creds) + next.ServeHTTP(w, req.WithContext(WithPrincipal(req.Context(), p))) + }) + } +} + +// UnaryInterceptor is the gRPC counterpart of HTTPMiddleware. +func UnaryInterceptor(r Resolver) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + return handler(withResolvedPrincipal(ctx, r), req) + } +} + +// StreamInterceptor is the streaming counterpart of UnaryInterceptor. +func StreamInterceptor(r Resolver) grpc.StreamServerInterceptor { + return func(srv any, ss grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + return handler(srv, &principalStream{ServerStream: ss, ctx: withResolvedPrincipal(ss.Context(), r)}) + } +} + +func withResolvedPrincipal(ctx context.Context, r Resolver) context.Context { + md, _ := metadata.FromIncomingContext(ctx) + return WithPrincipal(ctx, r.Resolve(ctx, CredentialsFromMetadata(md))) +} + +type principalStream struct { + grpc.ServerStream + ctx context.Context +} + +func (s *principalStream) Context() context.Context { return s.ctx } diff --git a/internal/auth/transport_test.go b/internal/auth/transport_test.go new file mode 100644 index 00000000..2a2ab5c1 --- /dev/null +++ b/internal/auth/transport_test.go @@ -0,0 +1,110 @@ +package auth + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +func fakeResolver() Resolver { + return ResolverFunc(func(_ context.Context, c Credentials) Principal { + if c.APIKey == "trk_abcdefgh_ok" { + return Principal{Kind: KindAPIKey, Username: "apikey:abcdefgh", Permissions: NewPermissionSet(PermEventRead)} + } + if c.SessionToken == "session.ok" { + return Principal{Kind: KindUser, Username: "alice", Permissions: NewPermissionSet(PermEventRead)} + } + return Anonymous(nil) + }) +} + +func TestHTTPMiddlewareStoresPrincipal(t *testing.T) { + var seen Principal + h := HTTPMiddleware(fakeResolver(), Config{})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + seen, _ = FromContext(r.Context()) + })) + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.Header.Set("X-Api-Key", "trk_abcdefgh_ok") + h.ServeHTTP(httptest.NewRecorder(), r) + assert.Equal(t, KindAPIKey, seen.Kind) + + h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil)) + assert.Equal(t, KindAnonymous, seen.Kind) +} + +func TestUnaryInterceptorStoresPrincipal(t *testing.T) { + var seen Principal + handler := func(ctx context.Context, req any) (any, error) { + seen, _ = FromContext(ctx) + return nil, nil + } + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("x-api-key", "trk_abcdefgh_ok")) + _, err := UnaryInterceptor(fakeResolver())(ctx, nil, &grpc.UnaryServerInfo{FullMethod: "/x/Y"}, handler) + require.NoError(t, err) + assert.Equal(t, KindAPIKey, seen.Kind) +} + +type fakeStream struct { + grpc.ServerStream + ctx context.Context +} + +func (s fakeStream) Context() context.Context { return s.ctx } + +func TestStreamInterceptorStoresPrincipal(t *testing.T) { + var seen Principal + handler := func(srv any, ss grpc.ServerStream) error { + seen, _ = FromContext(ss.Context()) + return nil + } + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("x-api-key", "trk_abcdefgh_ok")) + err := StreamInterceptor(fakeResolver())(nil, fakeStream{ctx: ctx}, &grpc.StreamServerInfo{FullMethod: "/x/Y"}, handler) + require.NoError(t, err) + assert.Equal(t, KindAPIKey, seen.Kind) +} + +// A session cookie is SameSite=Lax, so a browser still sends it on a top level +// cross-site GET. The middleware must not turn that into an authenticated call. +func TestHTTPMiddlewareIgnoresCookieOnCrossSiteRequest(t *testing.T) { + var seen Principal + cfg := Config{PublicURL: "https://tracker.example.com"} + h := HTTPMiddleware(fakeResolver(), cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + seen, _ = FromContext(r.Context()) + })) + + withCookie := func(secFetch string) *http.Request { + r := httptest.NewRequest(http.MethodGet, "/api/v1alpha1/unlock/abc", nil) + r.AddCookie(&http.Cookie{Name: SessionCookieName, Value: "session.ok"}) + if secFetch != "" { + r.Header.Set("Sec-Fetch-Site", secFetch) + } + return r + } + + h.ServeHTTP(httptest.NewRecorder(), withCookie("same-origin")) + assert.Equal(t, KindUser, seen.Kind, "same-origin cookie request stays authenticated") + + h.ServeHTTP(httptest.NewRecorder(), withCookie("cross-site")) + assert.Equal(t, KindAnonymous, seen.Kind, "cross-site cookie request falls back to anonymous") + assert.False(t, seen.Has(PermEventRead)) + + // An API key is not a browser credential and is never dropped. + r := httptest.NewRequest(http.MethodGet, "/api/v1alpha1/unlock/abc", nil) + r.Header.Set("X-Api-Key", "trk_abcdefgh_ok") + r.Header.Set("Sec-Fetch-Site", "cross-site") + h.ServeHTTP(httptest.NewRecorder(), r) + assert.Equal(t, KindAPIKey, seen.Kind, "an API key is not subject to the cookie CSRF guard") + + // A bearer session token is explicit, so it is not dropped either. + r = httptest.NewRequest(http.MethodGet, "/api/v1alpha1/unlock/abc", nil) + r.Header.Set("Authorization", "Bearer session.ok") + r.Header.Set("Sec-Fetch-Site", "cross-site") + h.ServeHTTP(httptest.NewRecorder(), r) + assert.Equal(t, KindUser, seen.Kind, "an explicit bearer token is not subject to the cookie CSRF guard") +} diff --git a/internal/stores/auth_apikeys.go b/internal/stores/auth_apikeys.go new file mode 100644 index 00000000..74fa0b96 --- /dev/null +++ b/internal/stores/auth_apikeys.go @@ -0,0 +1,92 @@ +package store + +import ( + "context" + "errors" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// AuthAPIKeyStore persists API keys. Only the hash of a secret is stored. +type AuthAPIKeyStore struct { + coll *mongo.Collection +} + +func NewAuthAPIKeyStore() *AuthAPIKeyStore { + return NewAuthAPIKeyStoreFromCollection(NewClient(authAPIKeysCollection)) +} + +func NewAuthAPIKeyStoreFromCollection(coll *mongo.Collection) *AuthAPIKeyStore { + return &AuthAPIKeyStore{coll: coll} +} + +func (s *AuthAPIKeyStore) Create(ctx context.Context, k *APIKey) error { + k.CreatedAt = time.Now().UTC() + res, err := s.coll.InsertOne(ctx, k) + if mongo.IsDuplicateKeyError(err) { + return ErrAlreadyExists + } + if err != nil { + return err + } + k.ID = res.InsertedID.(primitive.ObjectID) + return nil +} + +func (s *AuthAPIKeyStore) GetByID(ctx context.Context, id primitive.ObjectID) (*APIKey, error) { + return s.findOne(ctx, bson.M{"_id": id}) +} + +func (s *AuthAPIKeyStore) GetByPrefix(ctx context.Context, prefix string) (*APIKey, error) { + return s.findOne(ctx, bson.M{"prefix": prefix}) +} + +func (s *AuthAPIKeyStore) findOne(ctx context.Context, filter bson.M) (*APIKey, error) { + var k APIKey + err := s.coll.FindOne(ctx, filter).Decode(&k) + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, ErrNotFound + } + if err != nil { + return nil, err + } + return &k, nil +} + +func (s *AuthAPIKeyStore) List(ctx context.Context) ([]*APIKey, error) { + cur, err := s.coll.Find(ctx, bson.M{}, options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}})) + if err != nil { + return nil, err + } + out := []*APIKey{} + if err := cur.All(ctx, &out); err != nil { + return nil, err + } + return out, nil +} + +func (s *AuthAPIKeyStore) Revoke(ctx context.Context, id primitive.ObjectID, at time.Time) error { + res, err := s.coll.UpdateOne(ctx, bson.M{"_id": id, "revokedAt": nil}, bson.M{"$set": bson.M{"revokedAt": at}}) + if err != nil { + return err + } + if res.MatchedCount == 0 { + return ErrNotFound + } + return nil +} + +// RevokeByTeam revokes every active key of a team, when the team is deleted. +func (s *AuthAPIKeyStore) RevokeByTeam(ctx context.Context, teamID primitive.ObjectID, at time.Time) error { + _, err := s.coll.UpdateMany(ctx, bson.M{"teamId": teamID, "revokedAt": nil}, bson.M{"$set": bson.M{"revokedAt": at}}) + return err +} + +func (s *AuthAPIKeyStore) TouchLastUsed(ctx context.Context, id primitive.ObjectID, at time.Time) error { + _, err := s.coll.UpdateByID(ctx, id, bson.M{"$set": bson.M{"lastUsedAt": at}}) + return err +} diff --git a/internal/stores/auth_apikeys_test.go b/internal/stores/auth_apikeys_test.go new file mode 100644 index 00000000..d6b5c13e --- /dev/null +++ b/internal/stores/auth_apikeys_test.go @@ -0,0 +1,52 @@ +package store + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestAuthAPIKeyStore(t *testing.T) { + db := testDatabase(t) + s := NewAuthAPIKeyStoreFromCollection(db.Collection(authAPIKeysCollection)) + ctx := context.Background() + team := primitive.NewObjectID() + + k := &APIKey{Prefix: "abcdefgh", Hash: "h1", Name: "ci", TeamID: &team, CreatedBy: primitive.NewObjectID()} + require.NoError(t, s.Create(ctx, k)) + assert.False(t, k.CreatedAt.IsZero()) + assert.ErrorIs(t, s.Create(ctx, &APIKey{Prefix: "abcdefgh", Hash: "h2", Name: "dup"}), ErrAlreadyExists) + + global := &APIKey{Prefix: "zzzzzzzz", Hash: "h3", Name: "global", CreatedBy: primitive.NewObjectID()} + require.NoError(t, s.Create(ctx, global)) + + got, err := s.GetByPrefix(ctx, "abcdefgh") + require.NoError(t, err) + assert.Equal(t, "ci", got.Name) + _, err = s.GetByPrefix(ctx, "nope") + assert.ErrorIs(t, err, ErrNotFound) + + at := time.Now().UTC().Truncate(time.Millisecond) + require.NoError(t, s.TouchLastUsed(ctx, k.ID, at)) + got, _ = s.GetByID(ctx, k.ID) + require.NotNil(t, got.LastUsedAt) + + require.NoError(t, s.RevokeByTeam(ctx, team, at)) + got, _ = s.GetByID(ctx, k.ID) + require.NotNil(t, got.RevokedAt) + g, _ := s.GetByID(ctx, global.ID) + assert.Nil(t, g.RevokedAt, "global keys are untouched") + + require.NoError(t, s.Revoke(ctx, global.ID, at)) + g, _ = s.GetByID(ctx, global.ID) + assert.NotNil(t, g.RevokedAt) + assert.ErrorIs(t, s.Revoke(ctx, primitive.NewObjectID(), at), ErrNotFound) + + list, err := s.List(ctx) + require.NoError(t, err) + assert.Len(t, list, 2) +} diff --git a/internal/stores/auth_models.go b/internal/stores/auth_models.go new file mode 100644 index 00000000..bf14d7e2 --- /dev/null +++ b/internal/stores/auth_models.go @@ -0,0 +1,80 @@ +package store + +import ( + "errors" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + UserSourceLocal = "local" + UserSourceOIDC = "oidc" + + // AdministratorsTeamName is the built-in team that holds every permission. + AdministratorsTeamName = "Administrators" + + authUsersCollection = "auth_users" + authTeamsCollection = "auth_teams" + authAPIKeysCollection = "auth_api_keys" // #nosec G101 -- collection name, not a credential + authSettingsCollection = "auth_settings" +) + +var ( + ErrNotFound = errors.New("not found") + ErrAlreadyExists = errors.New("already exists") +) + +// User is a local or OIDC account. +type User struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + Username string `bson:"username"` + UsernameLower string `bson:"usernameLower"` + Email string `bson:"email"` + DisplayName string `bson:"displayName"` + Source string `bson:"source"` + PasswordHash string `bson:"passwordHash,omitempty"` + OIDCIssuer string `bson:"oidcIssuer,omitempty"` + OIDCSubject string `bson:"oidcSubject,omitempty"` + Teams []primitive.ObjectID `bson:"teams"` + Disabled bool `bson:"disabled"` + MustChangePassword bool `bson:"mustChangePassword"` + SessionVersion int `bson:"sessionVersion"` + CreatedAt time.Time `bson:"createdAt"` + UpdatedAt time.Time `bson:"updatedAt"` + LastLoginAt *time.Time `bson:"lastLoginAt,omitempty"` +} + +// TeamScope restricts a team to catalog services. All wins over Services. +type TeamScope struct { + All bool `bson:"all"` + Services []string `bson:"services"` +} + +// Team groups users and API keys and carries permissions. +type Team struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + Name string `bson:"name"` + NameLower string `bson:"nameLower"` + Description string `bson:"description"` + Permissions []string `bson:"permissions"` + Scope TeamScope `bson:"scope"` + OIDCGroups []string `bson:"oidcGroups"` + Builtin bool `bson:"builtin"` + CreatedAt time.Time `bson:"createdAt"` + UpdatedAt time.Time `bson:"updatedAt"` +} + +// APIKey is a machine credential. TeamID nil means a global key. +type APIKey struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + Prefix string `bson:"prefix"` + Hash string `bson:"hash"` + Name string `bson:"name"` + TeamID *primitive.ObjectID `bson:"teamId"` + CreatedBy primitive.ObjectID `bson:"createdBy"` + CreatedAt time.Time `bson:"createdAt"` + ExpiresAt *time.Time `bson:"expiresAt,omitempty"` + LastUsedAt *time.Time `bson:"lastUsedAt,omitempty"` + RevokedAt *time.Time `bson:"revokedAt,omitempty"` +} diff --git a/internal/stores/auth_settings.go b/internal/stores/auth_settings.go new file mode 100644 index 00000000..5967fdfa --- /dev/null +++ b/internal/stores/auth_settings.go @@ -0,0 +1,48 @@ +package store + +import ( + "context" + "crypto/rand" + "fmt" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// AuthSettingsStore holds server generated secrets that must survive restarts. +type AuthSettingsStore struct { + coll *mongo.Collection +} + +func NewAuthSettingsStore() *AuthSettingsStore { + return NewAuthSettingsStoreFromCollection(NewClient(authSettingsCollection)) +} + +func NewAuthSettingsStoreFromCollection(coll *mongo.Collection) *AuthSettingsStore { + return &AuthSettingsStore{coll: coll} +} + +type sessionSecretDoc struct { + Secret []byte `bson:"secret"` +} + +// SessionSecret returns the persisted session secret, generating it on first call. +// Concurrent first calls are safe: the upsert only inserts once. +func (s *AuthSettingsStore) SessionSecret(ctx context.Context) ([]byte, error) { + fresh := make([]byte, 32) + if _, err := rand.Read(fresh); err != nil { + return nil, fmt.Errorf("generate session secret: %w", err) + } + var doc sessionSecretDoc + err := s.coll.FindOneAndUpdate(ctx, + bson.M{"_id": "session"}, + bson.M{"$setOnInsert": bson.M{"secret": fresh, "createdAt": time.Now().UTC()}}, + options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After), + ).Decode(&doc) + if err != nil { + return nil, fmt.Errorf("load session secret: %w", err) + } + return doc.Secret, nil +} diff --git a/internal/stores/auth_settings_test.go b/internal/stores/auth_settings_test.go new file mode 100644 index 00000000..c68c6aab --- /dev/null +++ b/internal/stores/auth_settings_test.go @@ -0,0 +1,22 @@ +package store + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAuthSettingsSessionSecretIsStable(t *testing.T) { + db := testDatabase(t) + s := NewAuthSettingsStoreFromCollection(db.Collection(authSettingsCollection)) + ctx := context.Background() + + first, err := s.SessionSecret(ctx) + require.NoError(t, err) + assert.Len(t, first, 32) + second, err := s.SessionSecret(ctx) + require.NoError(t, err) + assert.Equal(t, first, second) +} diff --git a/internal/stores/auth_teams.go b/internal/stores/auth_teams.go new file mode 100644 index 00000000..02f5d5d9 --- /dev/null +++ b/internal/stores/auth_teams.go @@ -0,0 +1,125 @@ +package store + +import ( + "context" + "errors" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// AuthTeamStore persists teams. +type AuthTeamStore struct { + coll *mongo.Collection +} + +func NewAuthTeamStore() *AuthTeamStore { + return NewAuthTeamStoreFromCollection(NewClient(authTeamsCollection)) +} + +func NewAuthTeamStoreFromCollection(coll *mongo.Collection) *AuthTeamStore { + return &AuthTeamStore{coll: coll} +} + +func normalizeTeam(t *Team) { + t.NameLower = strings.ToLower(strings.TrimSpace(t.Name)) + if t.Permissions == nil { + t.Permissions = []string{} + } + if t.Scope.Services == nil { + t.Scope.Services = []string{} + } + if t.OIDCGroups == nil { + t.OIDCGroups = []string{} + } +} + +func (s *AuthTeamStore) Create(ctx context.Context, t *Team) error { + now := time.Now().UTC() + normalizeTeam(t) + t.CreatedAt, t.UpdatedAt = now, now + res, err := s.coll.InsertOne(ctx, t) + if mongo.IsDuplicateKeyError(err) { + return ErrAlreadyExists + } + if err != nil { + return err + } + t.ID = res.InsertedID.(primitive.ObjectID) + return nil +} + +func (s *AuthTeamStore) GetByID(ctx context.Context, id primitive.ObjectID) (*Team, error) { + return s.findOne(ctx, bson.M{"_id": id}) +} + +func (s *AuthTeamStore) GetByName(ctx context.Context, name string) (*Team, error) { + return s.findOne(ctx, bson.M{"nameLower": strings.ToLower(strings.TrimSpace(name))}) +} + +func (s *AuthTeamStore) findOne(ctx context.Context, filter bson.M) (*Team, error) { + var t Team + err := s.coll.FindOne(ctx, filter).Decode(&t) + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, ErrNotFound + } + if err != nil { + return nil, err + } + return &t, nil +} + +// GetByIDs returns the teams that exist among ids, in name order. +func (s *AuthTeamStore) GetByIDs(ctx context.Context, ids []primitive.ObjectID) ([]*Team, error) { + if len(ids) == 0 { + return []*Team{}, nil + } + return s.find(ctx, bson.M{"_id": bson.M{"$in": ids}}) +} + +func (s *AuthTeamStore) List(ctx context.Context) ([]*Team, error) { + return s.find(ctx, bson.M{}) +} + +func (s *AuthTeamStore) find(ctx context.Context, filter bson.M) ([]*Team, error) { + cur, err := s.coll.Find(ctx, filter, options.Find().SetSort(bson.D{{Key: "nameLower", Value: 1}})) + if err != nil { + return nil, err + } + out := []*Team{} + if err := cur.All(ctx, &out); err != nil { + return nil, err + } + return out, nil +} + +func (s *AuthTeamStore) Update(ctx context.Context, t *Team) error { + normalizeTeam(t) + t.UpdatedAt = time.Now().UTC() + res, err := s.coll.ReplaceOne(ctx, bson.M{"_id": t.ID}, t) + if mongo.IsDuplicateKeyError(err) { + return ErrAlreadyExists + } + if err != nil { + return err + } + if res.MatchedCount == 0 { + return ErrNotFound + } + return nil +} + +func (s *AuthTeamStore) Delete(ctx context.Context, id primitive.ObjectID) error { + res, err := s.coll.DeleteOne(ctx, bson.M{"_id": id}) + if err != nil { + return err + } + if res.DeletedCount == 0 { + return ErrNotFound + } + return nil +} diff --git a/internal/stores/auth_teams_test.go b/internal/stores/auth_teams_test.go new file mode 100644 index 00000000..25f3bdea --- /dev/null +++ b/internal/stores/auth_teams_test.go @@ -0,0 +1,49 @@ +package store + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestAuthTeamStoreCRUD(t *testing.T) { + db := testDatabase(t) + s := NewAuthTeamStoreFromCollection(db.Collection(authTeamsCollection)) + ctx := context.Background() + + team := &Team{Name: "Platform", Permissions: []string{"event:read"}, Scope: TeamScope{All: true}} + require.NoError(t, s.Create(ctx, team)) + assert.Equal(t, "platform", team.NameLower) + assert.ErrorIs(t, s.Create(ctx, &Team{Name: "PLATFORM"}), ErrAlreadyExists) + + got, err := s.GetByName(ctx, "platform") + require.NoError(t, err) + assert.Equal(t, team.ID, got.ID) + + other := &Team{Name: "Ops", Scope: TeamScope{Services: []string{"api"}}} + require.NoError(t, s.Create(ctx, other)) + byIDs, err := s.GetByIDs(ctx, []primitive.ObjectID{team.ID, other.ID, primitive.NewObjectID()}) + require.NoError(t, err) + assert.Len(t, byIDs, 2) + + empty, err := s.GetByIDs(ctx, nil) + require.NoError(t, err) + assert.Empty(t, empty) + + got.Description = "platform team" + require.NoError(t, s.Update(ctx, got)) + again, _ := s.GetByID(ctx, team.ID) + assert.Equal(t, "platform team", again.Description) + + require.NoError(t, s.Delete(ctx, other.ID)) + _, err = s.GetByID(ctx, other.ID) + assert.ErrorIs(t, err, ErrNotFound) + assert.ErrorIs(t, s.Delete(ctx, other.ID), ErrNotFound) + + list, err := s.List(ctx) + require.NoError(t, err) + assert.Len(t, list, 1) +} diff --git a/internal/stores/auth_testing_test.go b/internal/stores/auth_testing_test.go new file mode 100644 index 00000000..313c3869 --- /dev/null +++ b/internal/stores/auth_testing_test.go @@ -0,0 +1,36 @@ +package store + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// testDatabase connects to MONGO_TEST_URI, creates a throwaway database with +// all indexes and drops it at the end of the test. +func testDatabase(t *testing.T) *mongo.Database { + t.Helper() + uri := os.Getenv("MONGO_TEST_URI") + if uri == "" { + t.Skip("MONGO_TEST_URI not set") + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri)) + require.NoError(t, err) + db := client.Database(fmt.Sprintf("tracker_test_%d", time.Now().UnixNano())) + t.Cleanup(func() { + c, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _ = db.Drop(c) + _ = client.Disconnect(c) + }) + require.NoError(t, EnsureIndexes(ctx, db)) + return db +} diff --git a/internal/stores/auth_users.go b/internal/stores/auth_users.go new file mode 100644 index 00000000..e601d67e --- /dev/null +++ b/internal/stores/auth_users.go @@ -0,0 +1,124 @@ +package store + +import ( + "context" + "errors" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// AuthUserStore persists users. +type AuthUserStore struct { + coll *mongo.Collection +} + +// NewAuthUserStore connects to the configured database. +func NewAuthUserStore() *AuthUserStore { + return NewAuthUserStoreFromCollection(NewClient(authUsersCollection)) +} + +// NewAuthUserStoreFromCollection wraps an existing collection (tests). +func NewAuthUserStoreFromCollection(coll *mongo.Collection) *AuthUserStore { + return &AuthUserStore{coll: coll} +} + +func (s *AuthUserStore) Create(ctx context.Context, u *User) error { + now := time.Now().UTC() + u.UsernameLower = strings.ToLower(u.Username) + u.CreatedAt, u.UpdatedAt = now, now + if u.Teams == nil { + u.Teams = []primitive.ObjectID{} + } + res, err := s.coll.InsertOne(ctx, u) + if mongo.IsDuplicateKeyError(err) { + return ErrAlreadyExists + } + if err != nil { + return err + } + u.ID = res.InsertedID.(primitive.ObjectID) + return nil +} + +func (s *AuthUserStore) GetByID(ctx context.Context, id primitive.ObjectID) (*User, error) { + return s.findOne(ctx, bson.M{"_id": id}) +} + +func (s *AuthUserStore) GetByUsername(ctx context.Context, username string) (*User, error) { + return s.findOne(ctx, bson.M{"usernameLower": strings.ToLower(strings.TrimSpace(username))}) +} + +func (s *AuthUserStore) findOne(ctx context.Context, filter bson.M) (*User, error) { + var u User + err := s.coll.FindOne(ctx, filter).Decode(&u) + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, ErrNotFound + } + if err != nil { + return nil, err + } + return &u, nil +} + +func (s *AuthUserStore) List(ctx context.Context) ([]*User, error) { + cur, err := s.coll.Find(ctx, bson.M{}, options.Find().SetSort(bson.D{{Key: "usernameLower", Value: 1}})) + if err != nil { + return nil, err + } + var out []*User + if err := cur.All(ctx, &out); err != nil { + return nil, err + } + if out == nil { + out = []*User{} + } + return out, nil +} + +func (s *AuthUserStore) Update(ctx context.Context, u *User) error { + u.UsernameLower = strings.ToLower(u.Username) + u.UpdatedAt = time.Now().UTC() + if u.Teams == nil { + u.Teams = []primitive.ObjectID{} + } + res, err := s.coll.ReplaceOne(ctx, bson.M{"_id": u.ID}, u) + if mongo.IsDuplicateKeyError(err) { + return ErrAlreadyExists + } + if err != nil { + return err + } + if res.MatchedCount == 0 { + return ErrNotFound + } + return nil +} + +func (s *AuthUserStore) Count(ctx context.Context) (int64, error) { + return s.coll.CountDocuments(ctx, bson.M{}) +} + +func (s *AuthUserStore) TouchLogin(ctx context.Context, id primitive.ObjectID, at time.Time) error { + _, err := s.coll.UpdateByID(ctx, id, bson.M{"$set": bson.M{"lastLoginAt": at}}) + return err +} + +// RemoveTeam pulls a deleted team out of every user. +func (s *AuthUserStore) RemoveTeam(ctx context.Context, teamID primitive.ObjectID) error { + _, err := s.coll.UpdateMany(ctx, bson.M{"teams": teamID}, bson.M{"$pull": bson.M{"teams": teamID}}) + return err +} + +// CountEnabledInTeam counts enabled members of a team, ignoring excludeUser. +func (s *AuthUserStore) CountEnabledInTeam(ctx context.Context, teamID, excludeUser primitive.ObjectID) (int64, error) { + filter := bson.M{"teams": teamID, "disabled": false} + if !excludeUser.IsZero() { + filter["_id"] = bson.M{"$ne": excludeUser} + } + return s.coll.CountDocuments(ctx, filter) +} diff --git a/internal/stores/auth_users_test.go b/internal/stores/auth_users_test.go new file mode 100644 index 00000000..1e8bfe31 --- /dev/null +++ b/internal/stores/auth_users_test.go @@ -0,0 +1,68 @@ +package store + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestAuthUserStoreCRUD(t *testing.T) { + db := testDatabase(t) + s := NewAuthUserStoreFromCollection(db.Collection(authUsersCollection)) + ctx := context.Background() + + n, err := s.Count(ctx) + require.NoError(t, err) + assert.Zero(t, n) + + team := primitive.NewObjectID() + u := &User{Username: "Alice", Email: "alice@example.com", Source: UserSourceLocal, PasswordHash: "x", Teams: []primitive.ObjectID{team}} + require.NoError(t, s.Create(ctx, u)) + assert.False(t, u.ID.IsZero()) + assert.Equal(t, "alice", u.UsernameLower) + + dup := &User{Username: "ALICE", Source: UserSourceLocal} + assert.ErrorIs(t, s.Create(ctx, dup), ErrAlreadyExists) + + got, err := s.GetByUsername(ctx, "aLiCe") + require.NoError(t, err) + assert.Equal(t, u.ID, got.ID) + + _, err = s.GetByUsername(ctx, "nobody") + assert.ErrorIs(t, err, ErrNotFound) + + got.DisplayName = "Alice A." + got.SessionVersion++ + require.NoError(t, s.Update(ctx, got)) + again, err := s.GetByID(ctx, u.ID) + require.NoError(t, err) + assert.Equal(t, "Alice A.", again.DisplayName) + assert.Equal(t, 1, again.SessionVersion) + + at := time.Now().UTC().Truncate(time.Millisecond) + require.NoError(t, s.TouchLogin(ctx, u.ID, at)) + again, _ = s.GetByID(ctx, u.ID) + require.NotNil(t, again.LastLoginAt) + assert.Equal(t, at, again.LastLoginAt.UTC()) + + count, err := s.CountEnabledInTeam(ctx, team, primitive.NilObjectID) + require.NoError(t, err) + assert.Equal(t, int64(1), count) + count, err = s.CountEnabledInTeam(ctx, team, u.ID) + require.NoError(t, err) + assert.Zero(t, count) + + require.NoError(t, s.RemoveTeam(ctx, team)) + again, _ = s.GetByID(ctx, u.ID) + assert.Empty(t, again.Teams) + + list, err := s.List(ctx) + require.NoError(t, err) + assert.Len(t, list, 1) + + assert.ErrorIs(t, s.Update(ctx, &User{ID: primitive.NewObjectID(), Username: "ghost"}), ErrNotFound) +} diff --git a/internal/stores/db.go b/internal/stores/db.go index dd922dcd..cb0b7521 100644 --- a/internal/stores/db.go +++ b/internal/stores/db.go @@ -24,13 +24,17 @@ var cert tls.Certificate var mongoDatabase *mongo.Database func NewClient(collection string) (c *mongo.Collection) { + ctx := context.Background() + if mongoDatabase != nil { + ensureCollection(ctx, mongoDatabase, collection) + return mongoDatabase.Collection(collection) + } config := config.ConfigDatabase var m *mongo.Client var err error uri := createMongoUri(config) - ctx := context.Background() if config.CAFile != "" { tlsConfig := loadTlsCerts(config) @@ -45,17 +49,17 @@ func NewClient(collection string) (c *mongo.Collection) { } } - // Stocker la database pour l'initialisation des index + // Stocker la database pour l'initialisation des index et les stores suivants mongoDatabase = m.Database(config.Name) + ensureCollection(ctx, mongoDatabase, collection) + return mongoDatabase.Collection(collection) +} - // init client collection - err = mongoDatabase.CreateCollection(ctx, collection) - if err != nil { +func ensureCollection(ctx context.Context, db *mongo.Database, collection string) { + if err := db.CreateCollection(ctx, collection); err != nil { // Ignorer l'erreur si la collection existe déjà log.Printf("collection %s may already exist: %v", collection, err) } - - return mongoDatabase.Collection(collection) } // GetDatabase retourne l'instance de la base de données MongoDB diff --git a/internal/stores/indexes.go b/internal/stores/indexes.go index 52ffe0a4..970f6485 100644 --- a/internal/stores/indexes.go +++ b/internal/stores/indexes.go @@ -2,6 +2,7 @@ package store import ( "context" + "fmt" "log/slog" "time" @@ -35,6 +36,11 @@ func EnsureIndexes(ctx context.Context, db *mongo.Database) error { return err } + // Index pour les collections auth_* + if err := ensureAuthIndexes(ctx, db, logger); err != nil { + return err + } + logger.Info("All database indexes ensured successfully") return nil } @@ -192,6 +198,38 @@ func ensureLinksIndexes(ctx context.Context, db *mongo.Database, logger *slog.Lo return createIndexes(ctx, collection, indexes, logger, "links") } +func ensureAuthIndexes(ctx context.Context, db *mongo.Database, logger *slog.Logger) error { + users := []mongo.IndexModel{ + {Keys: bson.D{{Key: "usernameLower", Value: 1}}, Options: options.Index().SetUnique(true).SetName("idx_username_lower")}, + { + Keys: bson.D{{Key: "oidcIssuer", Value: 1}, {Key: "oidcSubject", Value: 1}}, + Options: options.Index().SetUnique(true).SetName("idx_oidc_identity"). + SetPartialFilterExpression(bson.D{{Key: "oidcSubject", Value: bson.D{{Key: "$exists", Value: true}}}}), + }, + {Keys: bson.D{{Key: "teams", Value: 1}}, Options: options.Index().SetName("idx_user_teams")}, + } + if _, err := db.Collection(authUsersCollection).Indexes().CreateMany(ctx, users); err != nil { + return fmt.Errorf("create %s indexes: %w", authUsersCollection, err) + } + + teams := []mongo.IndexModel{ + {Keys: bson.D{{Key: "nameLower", Value: 1}}, Options: options.Index().SetUnique(true).SetName("idx_team_name_lower")}, + } + if _, err := db.Collection(authTeamsCollection).Indexes().CreateMany(ctx, teams); err != nil { + return fmt.Errorf("create %s indexes: %w", authTeamsCollection, err) + } + + keys := []mongo.IndexModel{ + {Keys: bson.D{{Key: "prefix", Value: 1}}, Options: options.Index().SetUnique(true).SetName("idx_apikey_prefix")}, + {Keys: bson.D{{Key: "teamId", Value: 1}}, Options: options.Index().SetName("idx_apikey_team")}, + } + if _, err := db.Collection(authAPIKeysCollection).Indexes().CreateMany(ctx, keys); err != nil { + return fmt.Errorf("create %s indexes: %w", authAPIKeysCollection, err) + } + logger.Info("Auth indexes ensured") + return nil +} + func createIndexes(ctx context.Context, collection *mongo.Collection, indexes []mongo.IndexModel, logger *slog.Logger, collectionName string) error { // Créer un contexte avec timeout pour éviter les blocages ctxTimeout, cancel := context.WithTimeout(ctx, 30*time.Second) diff --git a/proto/auth/v1alpha1/auth.proto b/proto/auth/v1alpha1/auth.proto new file mode 100644 index 00000000..9781ef9f --- /dev/null +++ b/proto/auth/v1alpha1/auth.proto @@ -0,0 +1,227 @@ +syntax = "proto3"; + +package tracker.auth.v1alpha1; + +import "google/api/annotations.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "proto/auth/v1alpha1"; + +// AuthService exposes the current identity and the administration of users, +// teams and API keys. Login, logout and password change are plain HTTP +// handlers because they set cookies. +service AuthService { + rpc GetAuthConfig(GetAuthConfigRequest) returns (GetAuthConfigResponse) { + option (google.api.http) = {get: "/api/v1alpha1/auth/config"}; + } + rpc Me(MeRequest) returns (MeResponse) { + option (google.api.http) = {get: "/api/v1alpha1/auth/me"}; + } + + rpc ListUsers(ListUsersRequest) returns (ListUsersResponse) { + option (google.api.http) = {get: "/api/v1alpha1/auth/users"}; + } + rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) { + option (google.api.http) = { + post: "/api/v1alpha1/auth/users" + body: "*" + }; + } + rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse) { + option (google.api.http) = { + put: "/api/v1alpha1/auth/users/{id}" + body: "*" + }; + } + + rpc ListTeams(ListTeamsRequest) returns (ListTeamsResponse) { + option (google.api.http) = {get: "/api/v1alpha1/auth/teams"}; + } + rpc CreateTeam(CreateTeamRequest) returns (CreateTeamResponse) { + option (google.api.http) = { + post: "/api/v1alpha1/auth/teams" + body: "*" + }; + } + rpc UpdateTeam(UpdateTeamRequest) returns (UpdateTeamResponse) { + option (google.api.http) = { + put: "/api/v1alpha1/auth/teams/{id}" + body: "*" + }; + } + rpc DeleteTeam(DeleteTeamRequest) returns (DeleteTeamResponse) { + option (google.api.http) = {delete: "/api/v1alpha1/auth/teams/{id}"}; + } + + rpc ListApiKeys(ListApiKeysRequest) returns (ListApiKeysResponse) { + option (google.api.http) = {get: "/api/v1alpha1/auth/api-keys"}; + } + rpc CreateApiKey(CreateApiKeyRequest) returns (CreateApiKeyResponse) { + option (google.api.http) = { + post: "/api/v1alpha1/auth/api-keys" + body: "*" + }; + } + rpc RevokeApiKey(RevokeApiKeyRequest) returns (RevokeApiKeyResponse) { + option (google.api.http) = {delete: "/api/v1alpha1/auth/api-keys/{id}"}; + } +} + +message GetAuthConfigRequest {} + +message GetAuthConfigResponse { + bool local_login_enabled = 1; + bool oidc_enabled = 2; + string oidc_button_label = 3; + repeated string anonymous_permissions = 4; + bool demo_mode = 5; +} + +message MeRequest {} + +message TeamRef { + string id = 1; + string name = 2; +} + +message MeResponse { + bool authenticated = 1; + // "anonymous", "user" or "apikey" + string kind = 2; + string user_id = 3; + string username = 4; + string display_name = 5; + // "local" or "oidc", empty for API keys and anonymous + string source = 6; + repeated TeamRef teams = 7; + repeated string permissions = 8; + bool scope_all = 9; + repeated string scope_services = 10; + bool must_change_password = 11; + bool is_admin = 12; +} + +message User { + string id = 1; + string username = 2; + string email = 3; + string display_name = 4; + string source = 5; + repeated string team_ids = 6; + bool disabled = 7; + bool must_change_password = 8; + google.protobuf.Timestamp created_at = 9; + google.protobuf.Timestamp last_login_at = 10; +} + +message ListUsersRequest {} +message ListUsersResponse { + repeated User users = 1; +} + +message CreateUserRequest { + string username = 1; + string email = 2; + string display_name = 3; + // Temporary password, the user must change it at first login. + string password = 4; + repeated string team_ids = 5; +} +message CreateUserResponse { + User user = 1; +} + +// UpdateUserRequest replaces email, display_name, team_ids and disabled. +// new_password, when set, resets the password and invalidates sessions. +message UpdateUserRequest { + string id = 1; + string email = 2; + string display_name = 3; + repeated string team_ids = 4; + bool disabled = 5; + string new_password = 6; +} +message UpdateUserResponse { + User user = 1; +} + +message Team { + string id = 1; + string name = 2; + string description = 3; + repeated string permissions = 4; + bool scope_all = 5; + repeated string scope_services = 6; + repeated string oidc_groups = 7; + bool builtin = 8; +} + +message ListTeamsRequest {} +message ListTeamsResponse { + repeated Team teams = 1; +} + +message CreateTeamRequest { + string name = 1; + string description = 2; + repeated string permissions = 3; + bool scope_all = 4; + repeated string scope_services = 5; + repeated string oidc_groups = 6; +} +message CreateTeamResponse { + Team team = 1; +} + +message UpdateTeamRequest { + string id = 1; + string name = 2; + string description = 3; + repeated string permissions = 4; + bool scope_all = 5; + repeated string scope_services = 6; + repeated string oidc_groups = 7; +} +message UpdateTeamResponse { + Team team = 1; +} + +message DeleteTeamRequest { + string id = 1; +} +message DeleteTeamResponse {} + +message ApiKey { + string id = 1; + string prefix = 2; + string name = 3; + // Empty for a global key. + string team_id = 4; + string created_by = 5; + google.protobuf.Timestamp created_at = 6; + google.protobuf.Timestamp expires_at = 7; + google.protobuf.Timestamp last_used_at = 8; + google.protobuf.Timestamp revoked_at = 9; +} + +message ListApiKeysRequest {} +message ListApiKeysResponse { + repeated ApiKey api_keys = 1; +} + +message CreateApiKeyRequest { + string name = 1; + // Empty creates a global key, allowed only for Administrators members. + string team_id = 2; + google.protobuf.Timestamp expires_at = 3; +} +message CreateApiKeyResponse { + ApiKey api_key = 1; + // Shown once, never stored. + string secret = 2; +} + +message RevokeApiKeyRequest { + string id = 1; +} +message RevokeApiKeyResponse {} diff --git a/server/auth.go b/server/auth.go new file mode 100644 index 00000000..ae92516a --- /dev/null +++ b/server/auth.go @@ -0,0 +1,217 @@ +package server + +import ( + "context" + "errors" + "log/slog" + "time" + + authv1 "github.com/bananaops/tracker/generated/proto/auth/v1alpha1" + "github.com/bananaops/tracker/internal/auth" + "github.com/bananaops/tracker/internal/auth/authz" + store "github.com/bananaops/tracker/internal/stores" + "go.mongodb.org/mongo-driver/bson/primitive" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// Auth implements tracker.auth.v1alpha1.AuthService. +type Auth struct { + authv1.UnimplementedAuthServiceServer + users *store.AuthUserStore + teams *store.AuthTeamStore + keys *store.AuthAPIKeyStore + cfg auth.Config + logger *slog.Logger + now func() time.Time +} + +// NewAuth builds the AuthService implementation. +func NewAuth(users *store.AuthUserStore, teams *store.AuthTeamStore, keys *store.AuthAPIKeyStore, cfg auth.Config) *Auth { + return &Auth{ + users: users, + teams: teams, + keys: keys, + cfg: cfg, + logger: slog.Default(), + now: time.Now, + } +} + +func (a *Auth) GetAuthConfig(ctx context.Context, _ *authv1.GetAuthConfigRequest) (*authv1.GetAuthConfigResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } + return &authv1.GetAuthConfigResponse{ + LocalLoginEnabled: true, + OidcEnabled: false, + AnonymousPermissions: permissionStrings(a.cfg.AnonymousPermissions), + DemoMode: a.cfg.DemoMode, + }, nil +} + +func (a *Auth) Me(ctx context.Context, _ *authv1.MeRequest) (*authv1.MeResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } + p := currentPrincipal(ctx) + resp := &authv1.MeResponse{ + Authenticated: p.IsAuthenticated(), + Kind: string(p.Kind), + UserId: p.UserID, + Username: p.Username, + Permissions: permissionStrings(p.Permissions.Slice()), + ScopeAll: p.Scope.All, + ScopeServices: p.Scope.ServiceList(), + IsAdmin: p.IsAdmin, + } + if p.Kind == auth.KindUser { + if id, err := primitive.ObjectIDFromHex(p.UserID); err == nil { + if user, err := a.users.GetByID(ctx, id); err == nil { + resp.DisplayName = user.DisplayName + resp.Source = user.Source + resp.MustChangePassword = user.MustChangePassword + } + } + } + ids := make([]primitive.ObjectID, 0, len(p.TeamIDs)) + for _, raw := range p.TeamIDs { + if id, err := primitive.ObjectIDFromHex(raw); err == nil { + ids = append(ids, id) + } + } + teams, err := a.teams.GetByIDs(ctx, ids) + if err != nil { + return nil, storeError(err, "teams") + } + for _, t := range teams { + resp.Teams = append(resp.Teams, &authv1.TeamRef{Id: t.ID.Hex(), Name: t.Name}) + } + return resp, nil +} + +// currentPrincipal never fails: a missing principal is anonymous without rights. +func currentPrincipal(ctx context.Context) auth.Principal { + if p, ok := auth.FromContext(ctx); ok { + return p + } + return auth.Anonymous(nil) +} + +func permissionStrings(perms []auth.Permission) []string { + out := make([]string, 0, len(perms)) + for _, p := range perms { + out = append(out, string(p)) + } + return out +} + +func parseObjectID(id, what string) (primitive.ObjectID, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return primitive.NilObjectID, status.Errorf(codes.InvalidArgument, "invalid %s id", what) + } + return oid, nil +} + +// parseTeamIDs validates ids and checks that every team exists. +func (a *Auth) parseTeamIDs(ctx context.Context, raw []string) ([]primitive.ObjectID, error) { + ids := make([]primitive.ObjectID, 0, len(raw)) + seen := map[primitive.ObjectID]struct{}{} + for _, r := range raw { + id, err := parseObjectID(r, "team") + if err != nil { + return nil, err + } + if _, dup := seen[id]; dup { + continue + } + seen[id] = struct{}{} + ids = append(ids, id) + } + teams, err := a.teams.GetByIDs(ctx, ids) + if err != nil { + return nil, storeError(err, "teams") + } + if len(teams) != len(ids) { + return nil, status.Error(codes.NotFound, "one of the teams does not exist") + } + return ids, nil +} + +// storeError maps store errors to gRPC statuses. +func storeError(err error, what string) error { + switch { + case errors.Is(err, store.ErrNotFound): + return status.Errorf(codes.NotFound, "%s not found", what) + case errors.Is(err, store.ErrAlreadyExists): + return status.Errorf(codes.AlreadyExists, "%s already exists", what) + default: + slog.Error("auth store error", "what", what, "error", err) + return status.Error(codes.Internal, "internal error") + } +} + +func tsOrNil(t *time.Time) *timestamppb.Timestamp { + if t == nil { + return nil + } + return timestamppb.New(*t) +} + +func objectIDStrings(ids []primitive.ObjectID) []string { + out := make([]string, 0, len(ids)) + for _, id := range ids { + out = append(out, id.Hex()) + } + return out +} + +func toProtoUser(u *store.User) *authv1.User { + return &authv1.User{ + Id: u.ID.Hex(), + Username: u.Username, + Email: u.Email, + DisplayName: u.DisplayName, + Source: u.Source, + TeamIds: objectIDStrings(u.Teams), + Disabled: u.Disabled, + MustChangePassword: u.MustChangePassword, + CreatedAt: timestamppb.New(u.CreatedAt), + LastLoginAt: tsOrNil(u.LastLoginAt), + } +} + +func toProtoTeam(t *store.Team) *authv1.Team { + return &authv1.Team{ + Id: t.ID.Hex(), + Name: t.Name, + Description: t.Description, + Permissions: t.Permissions, + ScopeAll: t.Scope.All, + ScopeServices: t.Scope.Services, + OidcGroups: t.OIDCGroups, + Builtin: t.Builtin, + } +} + +func toProtoAPIKey(k *store.APIKey) *authv1.ApiKey { + out := &authv1.ApiKey{ + Id: k.ID.Hex(), + Prefix: k.Prefix, + Name: k.Name, + CreatedBy: k.CreatedBy.Hex(), + CreatedAt: timestamppb.New(k.CreatedAt), + ExpiresAt: tsOrNil(k.ExpiresAt), + LastUsedAt: tsOrNil(k.LastUsedAt), + RevokedAt: tsOrNil(k.RevokedAt), + } + if k.TeamID != nil { + out.TeamId = k.TeamID.Hex() + } + if k.CreatedBy.IsZero() { + out.CreatedBy = "" + } + return out +} diff --git a/server/auth_apikeys.go b/server/auth_apikeys.go new file mode 100644 index 00000000..d46b8e21 --- /dev/null +++ b/server/auth_apikeys.go @@ -0,0 +1,94 @@ +package server + +import ( + "context" + "strings" + + authv1 "github.com/bananaops/tracker/generated/proto/auth/v1alpha1" + "github.com/bananaops/tracker/internal/auth" + "github.com/bananaops/tracker/internal/auth/authz" + store "github.com/bananaops/tracker/internal/stores" + "go.mongodb.org/mongo-driver/bson/primitive" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (a *Auth) ListApiKeys(ctx context.Context, _ *authv1.ListApiKeysRequest) (*authv1.ListApiKeysResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } + keys, err := a.keys.List(ctx) + if err != nil { + return nil, storeError(err, "api keys") + } + resp := &authv1.ListApiKeysResponse{ApiKeys: make([]*authv1.ApiKey, 0, len(keys))} + for _, k := range keys { + resp.ApiKeys = append(resp.ApiKeys, toProtoAPIKey(k)) + } + return resp, nil +} + +func (a *Auth) CreateApiKey(ctx context.Context, req *authv1.CreateApiKeyRequest) (*authv1.CreateApiKeyResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } + name := strings.TrimSpace(req.Name) + if name == "" || len(name) > teamNameMaxLength { + return nil, status.Error(codes.InvalidArgument, "api key name is required") + } + caller := currentPrincipal(ctx) + + var teamID *primitive.ObjectID + if req.TeamId == "" { + if !caller.IsAdmin { + return nil, status.Error(codes.PermissionDenied, "only administrators can create global api keys") + } + } else { + id, err := parseObjectID(req.TeamId, "team") + if err != nil { + return nil, err + } + if _, err := a.teams.GetByID(ctx, id); err != nil { + return nil, storeError(err, "team") + } + teamID = &id + } + + key := &store.APIKey{Name: name, TeamID: teamID} + if req.ExpiresAt != nil { + exp := req.ExpiresAt.AsTime().UTC() + if !exp.After(a.now()) { + return nil, status.Error(codes.InvalidArgument, "expiresAt must be in the future") + } + key.ExpiresAt = &exp + } + if id, err := primitive.ObjectIDFromHex(caller.UserID); err == nil { + key.CreatedBy = id + } + + gen, err := auth.GenerateAPIKey() + if err != nil { + return nil, status.Error(codes.Internal, "internal error") + } + key.Prefix, key.Hash = gen.Prefix, gen.Hash + if err := a.keys.Create(ctx, key); err != nil { + return nil, storeError(err, "api key") + } + a.logger.Info("auth.apikey.create", "prefix", key.Prefix, "name", key.Name, "global", teamID == nil, "by", caller.Username) + return &authv1.CreateApiKeyResponse{ApiKey: toProtoAPIKey(key), Secret: gen.Secret}, nil +} + +func (a *Auth) RevokeApiKey(ctx context.Context, req *authv1.RevokeApiKeyRequest) (*authv1.RevokeApiKeyResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } + id, err := parseObjectID(req.Id, "api key") + if err != nil { + return nil, err + } + if err := a.keys.Revoke(ctx, id, a.now().UTC()); err != nil { + return nil, storeError(err, "active api key") + } + a.logger.Info("auth.apikey.revoke", "id", id.Hex(), "by", currentPrincipal(ctx).Username) + return &authv1.RevokeApiKeyResponse{}, nil +} diff --git a/server/auth_apikeys_test.go b/server/auth_apikeys_test.go new file mode 100644 index 00000000..af5e60f4 --- /dev/null +++ b/server/auth_apikeys_test.go @@ -0,0 +1,74 @@ +package server + +import ( + "context" + "strings" + "testing" + "time" + + authv1 "github.com/bananaops/tracker/generated/proto/auth/v1alpha1" + "github.com/bananaops/tracker/internal/auth" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestAPIKeyLifecycle(t *testing.T) { + f := newAuthFixture(t) + svc := newAuthService(f) + admin := f.principalOf(t, f.admin) + + team, err := svc.CreateTeam(rpcCtx(admin, "CreateTeam"), &authv1.CreateTeamRequest{Name: "Ops", Permissions: []string{"lock:write"}}) + require.NoError(t, err) + + created, err := svc.CreateApiKey(rpcCtx(admin, "CreateApiKey"), &authv1.CreateApiKeyRequest{Name: "deploy", TeamId: team.Team.Id}) + require.NoError(t, err) + assert.True(t, strings.HasPrefix(created.Secret, "trk_")) + assert.Equal(t, created.ApiKey.Prefix, strings.Split(created.Secret, "_")[1]) + assert.Equal(t, team.Team.Id, created.ApiKey.TeamId) + assert.Equal(t, f.admin.ID.Hex(), created.ApiKey.CreatedBy) + + // The secret really authenticates with the team rights. + p := f.resolver.Resolve(context.Background(), auth.Credentials{APIKey: created.Secret}) + assert.Equal(t, auth.KindAPIKey, p.Kind) + assert.True(t, p.Has(auth.PermLockWrite)) + assert.False(t, p.IsAdmin) + + // Global key: admins only. + manager := auth.Principal{Kind: auth.KindUser, UserID: f.admin.ID.Hex(), Username: "mgr", Permissions: auth.NewPermissionSet(auth.PermAccessManage)} + _, err = svc.CreateApiKey(rpcCtx(manager, "CreateApiKey"), &authv1.CreateApiKeyRequest{Name: "global"}) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) + global, err := svc.CreateApiKey(rpcCtx(admin, "CreateApiKey"), &authv1.CreateApiKeyRequest{Name: "global"}) + require.NoError(t, err) + assert.Empty(t, global.ApiKey.TeamId) + p = f.resolver.Resolve(context.Background(), auth.Credentials{APIKey: global.Secret}) + assert.True(t, p.IsAdmin) + + // Validation. + _, err = svc.CreateApiKey(rpcCtx(admin, "CreateApiKey"), &authv1.CreateApiKeyRequest{Name: ""}) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + _, err = svc.CreateApiKey(rpcCtx(admin, "CreateApiKey"), &authv1.CreateApiKeyRequest{Name: "x", TeamId: "000000000000000000000000"}) + assert.Equal(t, codes.NotFound, status.Code(err)) + _, err = svc.CreateApiKey(rpcCtx(admin, "CreateApiKey"), &authv1.CreateApiKeyRequest{Name: "x", ExpiresAt: timestamppb.New(time.Now().Add(-time.Hour))}) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + future := time.Now().Add(time.Hour) + exp, err := svc.CreateApiKey(rpcCtx(admin, "CreateApiKey"), &authv1.CreateApiKeyRequest{Name: "temp", ExpiresAt: timestamppb.New(future)}) + require.NoError(t, err) + assert.WithinDuration(t, future, exp.ApiKey.ExpiresAt.AsTime(), time.Second) + + // Revocation. + _, err = svc.RevokeApiKey(rpcCtx(admin, "RevokeApiKey"), &authv1.RevokeApiKeyRequest{Id: created.ApiKey.Id}) + require.NoError(t, err) + p = f.resolver.Resolve(context.Background(), auth.Credentials{APIKey: created.Secret}) + assert.Equal(t, auth.KindAnonymous, p.Kind) + _, err = svc.RevokeApiKey(rpcCtx(admin, "RevokeApiKey"), &authv1.RevokeApiKeyRequest{Id: created.ApiKey.Id}) + assert.Equal(t, codes.NotFound, status.Code(err), "already revoked") + _, err = svc.RevokeApiKey(rpcCtx(admin, "RevokeApiKey"), &authv1.RevokeApiKeyRequest{Id: "bad"}) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + + list, err := svc.ListApiKeys(rpcCtx(admin, "ListApiKeys"), &authv1.ListApiKeysRequest{}) + require.NoError(t, err) + assert.Len(t, list.ApiKeys, 3) +} diff --git a/server/auth_http.go b/server/auth_http.go new file mode 100644 index 00000000..56e2bc13 --- /dev/null +++ b/server/auth_http.go @@ -0,0 +1,216 @@ +package server + +import ( + "encoding/json" + "errors" + "io" + "log/slog" + "net/http" + "strings" + "time" + + "github.com/bananaops/tracker/internal/auth" + "github.com/bananaops/tracker/internal/auth/authz" + store "github.com/bananaops/tracker/internal/stores" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +const ( + loginMaxFailures = 5 + loginWindow = time.Minute + maxAuthBodyBytes = 4096 +) + +// AuthHTTP serves the cookie based endpoints that cannot be gRPC methods. +type AuthHTTP struct { + users *store.AuthUserStore + sessions *auth.SessionManager + limiter *auth.LoginLimiter + cfg auth.Config + logger *slog.Logger +} + +func NewAuthHTTP(users *store.AuthUserStore, sessions *auth.SessionManager, cfg auth.Config) *AuthHTTP { + return &AuthHTTP{ + users: users, + sessions: sessions, + limiter: auth.NewLoginLimiter(loginMaxFailures, loginWindow), + cfg: cfg, + logger: slog.Default(), + } +} + +// Register mounts the endpoints on the gateway mux. +func (h *AuthHTTP) Register(mux *runtime.ServeMux) { + routes := []struct { + path string + handler runtime.HandlerFunc + }{ + {"/api/v1alpha1/auth/login", h.handleLogin}, + {"/api/v1alpha1/auth/logout", h.handleLogout}, + {"/api/v1alpha1/auth/password", h.handleChangePassword}, + } + for _, r := range routes { + if err := mux.HandlePath(http.MethodPost, r.path, r.handler); err != nil { + h.logger.Error("Failed to register auth route", "path", r.path, "error", err) + } + } +} + +func writeJSONError(w http.ResponseWriter, code int, msg string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + _ = json.NewEncoder(w).Encode(map[string]string{"error": msg}) +} + +func decodeJSON(r *http.Request, v any) error { + return json.NewDecoder(io.LimitReader(r.Body, maxAuthBodyBytes)).Decode(v) +} + +type loginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +func (h *AuthHTTP) handleLogin(w http.ResponseWriter, r *http.Request, _ map[string]string) { + // A cross-site form can POST here (the JSON decoder does not require a + // preflighted Content-Type), which would let a third party site probe + // passwords and plant a session cookie in the victim's browser. Reject + // before any password work so the check costs nothing. + if auth.IsCrossSite(r, h.cfg.PublicURL, h.cfg.TrustProxy) { + h.logger.Warn("auth.login", "result", "cross_site", "origin", r.Header.Get("Origin")) + writeJSONError(w, http.StatusForbidden, "cross-site login requests are refused") + return + } + var req loginRequest + if err := decodeJSON(r, &req); err != nil || req.Username == "" || req.Password == "" { + writeJSONError(w, http.StatusBadRequest, "username and password are required") + return + } + ip := auth.ClientIP(r, h.cfg.TrustProxy) + limitKey := strings.ToLower(req.Username) + "|" + ip + if h.limiter.Blocked(limitKey) { + h.logger.Warn("auth.login", "result", "rate_limited", "username", req.Username, "ip", ip) + authz.AuthLogins.WithLabelValues(authz.LoginMethodLocal, authz.LoginRateLimited).Inc() + writeJSONError(w, http.StatusTooManyRequests, "too many failed attempts, retry later") + return + } + + fail := func(reason string) { + h.limiter.RecordFailure(limitKey) + h.logger.Warn("auth.login", "result", "failure", "reason", reason, "username", req.Username, "ip", ip) + authz.AuthLogins.WithLabelValues(authz.LoginMethodLocal, authz.LoginFailure).Inc() + writeJSONError(w, http.StatusUnauthorized, "invalid credentials") + } + + user, err := h.users.GetByUsername(r.Context(), req.Username) + if err != nil { + if !errors.Is(err, store.ErrNotFound) { + h.logger.Error("auth.login lookup failed", "error", err) + writeJSONError(w, http.StatusInternalServerError, "internal error") + return + } + auth.DummyVerify(req.Password) + fail("unknown_user") + return + } + if user.Source != store.UserSourceLocal || user.Disabled || user.PasswordHash == "" { + auth.DummyVerify(req.Password) + fail("not_eligible") + return + } + ok, err := auth.VerifyPassword(user.PasswordHash, req.Password) + if err != nil || !ok { + fail("bad_password") + return + } + + h.limiter.Reset(limitKey) + if err := h.issueSession(w, user); err != nil { + h.logger.Error("auth.login issue session failed", "error", err) + writeJSONError(w, http.StatusInternalServerError, "internal error") + return + } + if err := h.users.TouchLogin(r.Context(), user.ID, time.Now().UTC()); err != nil { + h.logger.Error("auth.login touch failed", "error", err) + } + h.logger.Info("auth.login", "result", "success", "username", user.Username, "ip", ip) + authz.AuthLogins.WithLabelValues(authz.LoginMethodLocal, authz.LoginSuccess).Inc() + w.WriteHeader(http.StatusNoContent) +} + +func (h *AuthHTTP) issueSession(w http.ResponseWriter, user *store.User) error { + token, expires, err := h.sessions.Issue(user.ID.Hex(), user.SessionVersion) + if err != nil { + return err + } + http.SetCookie(w, auth.SessionCookie(token, expires, h.cfg.CookieSecure)) + return nil +} + +func (h *AuthHTTP) handleLogout(w http.ResponseWriter, _ *http.Request, _ map[string]string) { + http.SetCookie(w, auth.ClearSessionCookie(h.cfg.CookieSecure)) + w.WriteHeader(http.StatusNoContent) +} + +type changePasswordRequest struct { + CurrentPassword string `json:"currentPassword"` + NewPassword string `json:"newPassword"` +} + +func (h *AuthHTTP) handleChangePassword(w http.ResponseWriter, r *http.Request, _ map[string]string) { + p, ok := auth.FromContext(r.Context()) + if !ok || p.Kind != auth.KindUser { + writeJSONError(w, http.StatusUnauthorized, "authentication required") + return + } + var req changePasswordRequest + if err := decodeJSON(r, &req); err != nil || req.CurrentPassword == "" || req.NewPassword == "" { + writeJSONError(w, http.StatusBadRequest, "currentPassword and newPassword are required") + return + } + id, err := primitive.ObjectIDFromHex(p.UserID) + if err != nil { + writeJSONError(w, http.StatusUnauthorized, "authentication required") + return + } + user, err := h.users.GetByID(r.Context(), id) + if err != nil { + writeJSONError(w, http.StatusUnauthorized, "authentication required") + return + } + if user.Source != store.UserSourceLocal { + writeJSONError(w, http.StatusBadRequest, "password is managed by the identity provider") + return + } + ok, err = auth.VerifyPassword(user.PasswordHash, req.CurrentPassword) + if err != nil || !ok { + h.logger.Warn("auth.password", "result", "failure", "username", user.Username) + writeJSONError(w, http.StatusUnauthorized, "current password is incorrect") + return + } + if err := auth.ValidatePasswordPolicy(req.NewPassword); err != nil { + writeJSONError(w, http.StatusBadRequest, err.Error()) + return + } + hash, err := auth.HashPassword(req.NewPassword) + if err != nil { + writeJSONError(w, http.StatusInternalServerError, "internal error") + return + } + user.PasswordHash = hash + user.MustChangePassword = false + user.SessionVersion++ + if err := h.users.Update(r.Context(), user); err != nil { + h.logger.Error("auth.password update failed", "error", err) + writeJSONError(w, http.StatusInternalServerError, "internal error") + return + } + if err := h.issueSession(w, user); err != nil { + writeJSONError(w, http.StatusInternalServerError, "internal error") + return + } + h.logger.Info("auth.password", "result", "success", "username", user.Username) + w.WriteHeader(http.StatusNoContent) +} diff --git a/server/auth_http_test.go b/server/auth_http_test.go new file mode 100644 index 00000000..f82afec7 --- /dev/null +++ b/server/auth_http_test.go @@ -0,0 +1,168 @@ +package server + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/bananaops/tracker/internal/auth" + "github.com/bananaops/tracker/internal/auth/authz" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// loginCount reads tracker_auth_logins_total for one result label. +func loginCount(result string) float64 { + return testutil.ToFloat64(authz.AuthLogins.WithLabelValues("local", result)) +} + +func newAuthHTTPServer(t *testing.T, f *authFixture) http.Handler { + t.Helper() + mux := runtime.NewServeMux() + NewAuthHTTP(f.users, f.sessions, f.cfg).Register(mux) + return auth.HTTPMiddleware(f.resolver, f.cfg)(mux) +} + +func post(h http.Handler, path, body string, cookie *http.Cookie) *httptest.ResponseRecorder { + req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + if cookie != nil { + req.AddCookie(cookie) + } + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + return rec +} + +func sessionCookie(t *testing.T, rec *httptest.ResponseRecorder) *http.Cookie { + t.Helper() + for _, c := range rec.Result().Cookies() { + if c.Name == auth.SessionCookieName { + return c + } + } + t.Fatal("no session cookie in response") + return nil +} + +func TestLoginSuccessSetsCookie(t *testing.T) { + f := newAuthFixture(t) + h := newAuthHTTPServer(t, f) + before := loginCount("success") + + rec := post(h, "/api/v1alpha1/auth/login", `{"username":"Admin","password":"admin-password-123"}`, nil) + require.Equal(t, http.StatusNoContent, rec.Code, rec.Body.String()) + c := sessionCookie(t, rec) + assert.True(t, c.HttpOnly) + assert.Equal(t, http.SameSiteLaxMode, c.SameSite) + assert.False(t, c.Secure, "no https public url in this fixture") + + sess, err := f.sessions.Verify(c.Value) + require.NoError(t, err) + assert.Equal(t, f.admin.ID.Hex(), sess.UserID) + + again, _ := f.users.GetByID(context.Background(), f.admin.ID) + assert.NotNil(t, again.LastLoginAt) + assert.Equal(t, before+1, loginCount("success")) +} + +func TestLoginFailures(t *testing.T) { + f := newAuthFixture(t) + h := newAuthHTTPServer(t, f) + before := loginCount("failure") + + rec := post(h, "/api/v1alpha1/auth/login", `{"username":"admin","password":"wrong-password-1"}`, nil) + assert.Equal(t, http.StatusUnauthorized, rec.Code) + assert.Empty(t, rec.Result().Cookies()) + + rec = post(h, "/api/v1alpha1/auth/login", `{"username":"ghost","password":"wrong-password-1"}`, nil) + assert.Equal(t, http.StatusUnauthorized, rec.Code, "unknown user gets the same answer") + + rec = post(h, "/api/v1alpha1/auth/login", `{"username":"admin"}`, nil) + assert.Equal(t, http.StatusBadRequest, rec.Code) + + rec = post(h, "/api/v1alpha1/auth/login", `not json`, nil) + assert.Equal(t, http.StatusBadRequest, rec.Code) + + f.admin.Disabled = true + require.NoError(t, f.users.Update(context.Background(), f.admin)) + rec = post(h, "/api/v1alpha1/auth/login", `{"username":"admin","password":"admin-password-123"}`, nil) + assert.Equal(t, http.StatusUnauthorized, rec.Code, "disabled user cannot log in") + + // Wrong password, unknown user and disabled user, the two 400 answers are + // not login attempts and are not counted. + assert.Equal(t, before+3, loginCount("failure")) +} + +func TestLoginRateLimited(t *testing.T) { + f := newAuthFixture(t) + h := newAuthHTTPServer(t, f) + before := loginCount("rate_limited") + for i := 0; i < 5; i++ { + rec := post(h, "/api/v1alpha1/auth/login", `{"username":"admin","password":"wrong-password-1"}`, nil) + assert.Equal(t, http.StatusUnauthorized, rec.Code) + } + rec := post(h, "/api/v1alpha1/auth/login", `{"username":"admin","password":"admin-password-123"}`, nil) + assert.Equal(t, http.StatusTooManyRequests, rec.Code, "even the right password is blocked") + assert.Equal(t, before+1, loginCount("rate_limited")) +} + +func TestLogoutClearsCookie(t *testing.T) { + f := newAuthFixture(t) + h := newAuthHTTPServer(t, f) + rec := post(h, "/api/v1alpha1/auth/logout", "", nil) + assert.Equal(t, http.StatusNoContent, rec.Code) + c := sessionCookie(t, rec) + assert.Equal(t, -1, c.MaxAge) +} + +func TestChangePassword(t *testing.T) { + f := newAuthFixture(t) + h := newAuthHTTPServer(t, f) + + rec := post(h, "/api/v1alpha1/auth/password", `{"currentPassword":"admin-password-123","newPassword":"brand-new-password-1"}`, nil) + assert.Equal(t, http.StatusUnauthorized, rec.Code, "anonymous") + + login := post(h, "/api/v1alpha1/auth/login", `{"username":"admin","password":"admin-password-123"}`, nil) + cookie := sessionCookie(t, login) + + rec = post(h, "/api/v1alpha1/auth/password", `{"currentPassword":"nope-nope-nope","newPassword":"brand-new-password-1"}`, cookie) + assert.Equal(t, http.StatusUnauthorized, rec.Code, "wrong current password") + + rec = post(h, "/api/v1alpha1/auth/password", `{"currentPassword":"admin-password-123","newPassword":"short"}`, cookie) + assert.Equal(t, http.StatusBadRequest, rec.Code, "policy") + + rec = post(h, "/api/v1alpha1/auth/password", `{"currentPassword":"admin-password-123","newPassword":"brand-new-password-1"}`, cookie) + require.Equal(t, http.StatusNoContent, rec.Code, rec.Body.String()) + fresh := sessionCookie(t, rec) + + // The old session is invalidated, the new one works. + rec = post(h, "/api/v1alpha1/auth/password", `{"currentPassword":"brand-new-password-1","newPassword":"another-new-password-2"}`, cookie) + assert.Equal(t, http.StatusUnauthorized, rec.Code, "old session version rejected") + rec = post(h, "/api/v1alpha1/auth/password", `{"currentPassword":"brand-new-password-1","newPassword":"another-new-password-2"}`, fresh) + assert.Equal(t, http.StatusNoContent, rec.Code) + + u, _ := f.users.GetByID(context.Background(), f.admin.ID) + assert.False(t, u.MustChangePassword) + assert.Equal(t, 2, u.SessionVersion) +} + +func TestLoginRejectsCrossSiteRequest(t *testing.T) { + f := newAuthFixture(t) + h := newAuthHTTPServer(t, f) + + req := httptest.NewRequest(http.MethodPost, "/api/v1alpha1/auth/login", + strings.NewReader(`{"username":"admin","password":"admin-password-123"}`)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Sec-Fetch-Site", "cross-site") + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusForbidden, rec.Code) + assert.Empty(t, rec.Result().Cookies(), "no session is issued to a cross-site caller") + assert.Contains(t, rec.Body.String(), "cross-site") +} diff --git a/server/auth_teams.go b/server/auth_teams.go new file mode 100644 index 00000000..2ce137ea --- /dev/null +++ b/server/auth_teams.go @@ -0,0 +1,161 @@ +package server + +import ( + "context" + "sort" + "strings" + + authv1 "github.com/bananaops/tracker/generated/proto/auth/v1alpha1" + "github.com/bananaops/tracker/internal/auth" + "github.com/bananaops/tracker/internal/auth/authz" + store "github.com/bananaops/tracker/internal/stores" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const teamNameMaxLength = 64 + +func (a *Auth) ListTeams(ctx context.Context, _ *authv1.ListTeamsRequest) (*authv1.ListTeamsResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } + teams, err := a.teams.List(ctx) + if err != nil { + return nil, storeError(err, "teams") + } + resp := &authv1.ListTeamsResponse{Teams: make([]*authv1.Team, 0, len(teams))} + for _, t := range teams { + resp.Teams = append(resp.Teams, toProtoTeam(t)) + } + return resp, nil +} + +// teamFromRequest validates the editable fields of a team. +func teamFromRequest(name, description string, perms []string, scopeAll bool, services, groups []string) (*store.Team, error) { + name = strings.TrimSpace(name) + if name == "" || len(name) > teamNameMaxLength { + return nil, status.Errorf(codes.InvalidArgument, "team name must be 1 to %d characters", teamNameMaxLength) + } + cleanPerms := make([]string, 0, len(perms)) + seenPerm := map[string]struct{}{} + for _, raw := range perms { + p := auth.Permission(strings.TrimSpace(raw)) + if !auth.IsValidPermission(p) { + return nil, status.Errorf(codes.InvalidArgument, "unknown permission %q", raw) + } + if _, dup := seenPerm[string(p)]; dup { + continue + } + seenPerm[string(p)] = struct{}{} + cleanPerms = append(cleanPerms, string(p)) + } + sort.Strings(cleanPerms) + + cleanServices := dedupeTrimmed(services) + scope := store.TeamScope{All: scopeAll || len(cleanServices) == 0, Services: cleanServices} + if scope.All { + scope.Services = []string{} + } + return &store.Team{ + Name: name, + Description: strings.TrimSpace(description), + Permissions: cleanPerms, + Scope: scope, + OIDCGroups: dedupeTrimmed(groups), + }, nil +} + +func dedupeTrimmed(in []string) []string { + out := make([]string, 0, len(in)) + seen := map[string]struct{}{} + for _, raw := range in { + s := strings.TrimSpace(raw) + if s == "" { + continue + } + if _, dup := seen[s]; dup { + continue + } + seen[s] = struct{}{} + out = append(out, s) + } + sort.Strings(out) + return out +} + +func (a *Auth) CreateTeam(ctx context.Context, req *authv1.CreateTeamRequest) (*authv1.CreateTeamResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } + team, err := teamFromRequest(req.Name, req.Description, req.Permissions, req.ScopeAll, req.ScopeServices, req.OidcGroups) + if err != nil { + return nil, err + } + if err := a.teams.Create(ctx, team); err != nil { + return nil, storeError(err, "team") + } + a.logger.Info("auth.team.create", "team", team.Name, "by", currentPrincipal(ctx).Username) + return &authv1.CreateTeamResponse{Team: toProtoTeam(team)}, nil +} + +func (a *Auth) UpdateTeam(ctx context.Context, req *authv1.UpdateTeamRequest) (*authv1.UpdateTeamResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } + id, err := parseObjectID(req.Id, "team") + if err != nil { + return nil, err + } + existing, err := a.teams.GetByID(ctx, id) + if err != nil { + return nil, storeError(err, "team") + } + if existing.Builtin { + // Name, permissions and scope of the built-in team are immutable. + existing.Description = strings.TrimSpace(req.Description) + existing.OIDCGroups = dedupeTrimmed(req.OidcGroups) + } else { + edited, err := teamFromRequest(req.Name, req.Description, req.Permissions, req.ScopeAll, req.ScopeServices, req.OidcGroups) + if err != nil { + return nil, err + } + existing.Name = edited.Name + existing.Description = edited.Description + existing.Permissions = edited.Permissions + existing.Scope = edited.Scope + existing.OIDCGroups = edited.OIDCGroups + } + if err := a.teams.Update(ctx, existing); err != nil { + return nil, storeError(err, "team") + } + a.logger.Info("auth.team.update", "team", existing.Name, "by", currentPrincipal(ctx).Username) + return &authv1.UpdateTeamResponse{Team: toProtoTeam(existing)}, nil +} + +func (a *Auth) DeleteTeam(ctx context.Context, req *authv1.DeleteTeamRequest) (*authv1.DeleteTeamResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } + id, err := parseObjectID(req.Id, "team") + if err != nil { + return nil, err + } + team, err := a.teams.GetByID(ctx, id) + if err != nil { + return nil, storeError(err, "team") + } + if team.Builtin { + return nil, status.Error(codes.FailedPrecondition, "the built-in team cannot be deleted") + } + if err := a.users.RemoveTeam(ctx, id); err != nil { + return nil, storeError(err, "users") + } + if err := a.keys.RevokeByTeam(ctx, id, a.now().UTC()); err != nil { + return nil, storeError(err, "api keys") + } + if err := a.teams.Delete(ctx, id); err != nil { + return nil, storeError(err, "team") + } + a.logger.Info("auth.team.delete", "team", team.Name, "by", currentPrincipal(ctx).Username) + return &authv1.DeleteTeamResponse{}, nil +} diff --git a/server/auth_teams_test.go b/server/auth_teams_test.go new file mode 100644 index 00000000..f0d9e33b --- /dev/null +++ b/server/auth_teams_test.go @@ -0,0 +1,80 @@ +package server + +import ( + "context" + "testing" + + authv1 "github.com/bananaops/tracker/generated/proto/auth/v1alpha1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestTeamLifecycle(t *testing.T) { + f := newAuthFixture(t) + svc := newAuthService(f) + admin := f.principalOf(t, f.admin) + + created, err := svc.CreateTeam(rpcCtx(admin, "CreateTeam"), &authv1.CreateTeamRequest{ + Name: "Platform", Description: "platform team", Permissions: []string{"event:read", "event:write"}, + ScopeServices: []string{"api", "api", " web "}, + }) + require.NoError(t, err) + assert.Equal(t, "Platform", created.Team.Name) + assert.False(t, created.Team.ScopeAll) + assert.Equal(t, []string{"api", "web"}, created.Team.ScopeServices) + assert.False(t, created.Team.Builtin) + + all, err := svc.CreateTeam(rpcCtx(admin, "CreateTeam"), &authv1.CreateTeamRequest{Name: "Everyone"}) + require.NoError(t, err) + assert.True(t, all.Team.ScopeAll, "no services means every service") + + _, err = svc.CreateTeam(rpcCtx(admin, "CreateTeam"), &authv1.CreateTeamRequest{Name: "platform"}) + assert.Equal(t, codes.AlreadyExists, status.Code(err)) + _, err = svc.CreateTeam(rpcCtx(admin, "CreateTeam"), &authv1.CreateTeamRequest{Name: " "}) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + _, err = svc.CreateTeam(rpcCtx(admin, "CreateTeam"), &authv1.CreateTeamRequest{Name: "X", Permissions: []string{"public"}}) + assert.Equal(t, codes.InvalidArgument, status.Code(err), "pseudo permissions are not grantable") + _, err = svc.CreateTeam(rpcCtx(admin, "CreateTeam"), &authv1.CreateTeamRequest{Name: "X", Permissions: []string{"nope:read"}}) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + + updated, err := svc.UpdateTeam(rpcCtx(admin, "UpdateTeam"), &authv1.UpdateTeamRequest{ + Id: created.Team.Id, Name: "Platform2", Description: "d", Permissions: []string{"lock:read"}, ScopeAll: true, + }) + require.NoError(t, err) + assert.Equal(t, "Platform2", updated.Team.Name) + assert.True(t, updated.Team.ScopeAll) + assert.Equal(t, []string{"lock:read"}, updated.Team.Permissions) + + // Builtin team: only description and oidc groups change. + builtin, err := svc.UpdateTeam(rpcCtx(admin, "UpdateTeam"), &authv1.UpdateTeamRequest{ + Id: f.adminsID, Name: "Hackers", Description: "root", Permissions: []string{"event:read"}, OidcGroups: []string{"admins"}, + }) + require.NoError(t, err) + assert.Equal(t, "Administrators", builtin.Team.Name) + assert.Equal(t, "root", builtin.Team.Description) + assert.Equal(t, []string{"admins"}, builtin.Team.OidcGroups) + assert.Contains(t, builtin.Team.Permissions, "access:manage") + + _, err = svc.DeleteTeam(rpcCtx(admin, "DeleteTeam"), &authv1.DeleteTeamRequest{Id: f.adminsID}) + assert.Equal(t, codes.FailedPrecondition, status.Code(err)) + + // Deleting a team detaches users and revokes its keys. + _, err = svc.CreateUser(rpcCtx(admin, "CreateUser"), &authv1.CreateUserRequest{Username: "bob", Password: "bob-initial-pass-1", TeamIds: []string{created.Team.Id}}) + require.NoError(t, err) + key, err := svc.CreateApiKey(rpcCtx(admin, "CreateApiKey"), &authv1.CreateApiKeyRequest{Name: "ci", TeamId: created.Team.Id}) + require.NoError(t, err) + + _, err = svc.DeleteTeam(rpcCtx(admin, "DeleteTeam"), &authv1.DeleteTeamRequest{Id: created.Team.Id}) + require.NoError(t, err) + bob, _ := f.users.GetByUsername(context.Background(), "bob") + assert.Empty(t, bob.Teams) + keys, _ := svc.ListApiKeys(rpcCtx(admin, "ListApiKeys"), &authv1.ListApiKeysRequest{}) + require.Len(t, keys.ApiKeys, 1) + assert.Equal(t, key.ApiKey.Id, keys.ApiKeys[0].Id) + assert.NotNil(t, keys.ApiKeys[0].RevokedAt) + + _, err = svc.DeleteTeam(rpcCtx(admin, "DeleteTeam"), &authv1.DeleteTeamRequest{Id: created.Team.Id}) + assert.Equal(t, codes.NotFound, status.Code(err)) +} diff --git a/server/auth_test.go b/server/auth_test.go new file mode 100644 index 00000000..c99a18f9 --- /dev/null +++ b/server/auth_test.go @@ -0,0 +1,73 @@ +package server + +import ( + "context" + "testing" + + authv1 "github.com/bananaops/tracker/generated/proto/auth/v1alpha1" + "github.com/bananaops/tracker/internal/auth" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func newAuthService(f *authFixture) *Auth { + return NewAuth(f.users, f.teams, f.keys, f.cfg) +} + +func TestGetAuthConfigIsPublic(t *testing.T) { + f := newAuthFixture(t) + f.cfg.AnonymousPermissions = []auth.Permission{auth.PermEventRead} + f.cfg.DemoMode = true + svc := newAuthService(f) + + resp, err := svc.GetAuthConfig(rpcCtx(auth.Anonymous(nil), "GetAuthConfig"), &authv1.GetAuthConfigRequest{}) + require.NoError(t, err) + assert.True(t, resp.LocalLoginEnabled) + assert.False(t, resp.OidcEnabled) + assert.Equal(t, []string{"event:read"}, resp.AnonymousPermissions) + assert.True(t, resp.DemoMode) +} + +func TestMe(t *testing.T) { + f := newAuthFixture(t) + svc := newAuthService(f) + + resp, err := svc.Me(rpcCtx(auth.Anonymous([]auth.Permission{auth.PermEventRead}), "Me"), &authv1.MeRequest{}) + require.NoError(t, err) + assert.False(t, resp.Authenticated) + assert.Equal(t, "anonymous", resp.Kind) + assert.Equal(t, []string{"event:read"}, resp.Permissions) + + resp, err = svc.Me(rpcCtx(f.principalOf(t, f.admin), "Me"), &authv1.MeRequest{}) + require.NoError(t, err) + assert.True(t, resp.Authenticated) + assert.Equal(t, "user", resp.Kind) + assert.Equal(t, "admin", resp.Username) + assert.Equal(t, "Administrator", resp.DisplayName) + assert.Equal(t, "local", resp.Source) + assert.True(t, resp.MustChangePassword) + assert.True(t, resp.IsAdmin) + assert.True(t, resp.ScopeAll) + require.Len(t, resp.Teams, 1) + assert.Equal(t, "Administrators", resp.Teams[0].Name) + assert.Contains(t, resp.Permissions, "access:manage") +} + +func TestAuthServiceRequiresAccessManage(t *testing.T) { + f := newAuthFixture(t) + svc := newAuthService(f) + reader := auth.Principal{Kind: auth.KindUser, UserID: f.admin.ID.Hex(), Permissions: auth.NewPermissionSet(auth.PermEventRead)} + + _, err := svc.ListUsers(rpcCtx(auth.Anonymous(nil), "ListUsers"), &authv1.ListUsersRequest{}) + assert.Equal(t, codes.Unauthenticated, status.Code(err)) + _, err = svc.ListUsers(rpcCtx(reader, "ListUsers"), &authv1.ListUsersRequest{}) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) + _, err = svc.ListTeams(rpcCtx(reader, "ListTeams"), &authv1.ListTeamsRequest{}) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) + _, err = svc.ListApiKeys(rpcCtx(reader, "ListApiKeys"), &authv1.ListApiKeysRequest{}) + assert.Equal(t, codes.PermissionDenied, status.Code(err)) + _, err = svc.ListUsers(context.Background(), &authv1.ListUsersRequest{}) + assert.Equal(t, codes.PermissionDenied, status.Code(err), "no method in context is denied") +} diff --git a/server/auth_testing_test.go b/server/auth_testing_test.go new file mode 100644 index 00000000..7854c78d --- /dev/null +++ b/server/auth_testing_test.go @@ -0,0 +1,90 @@ +package server + +import ( + "bytes" + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/bananaops/tracker/internal/auth" + "github.com/bananaops/tracker/internal/auth/identity" + store "github.com/bananaops/tracker/internal/stores" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +// authFixture wires real stores on a throwaway database. +type authFixture struct { + users *store.AuthUserStore + teams *store.AuthTeamStore + keys *store.AuthAPIKeyStore + sessions *auth.SessionManager + resolver *identity.Resolver + cfg auth.Config + adminsID string + admin *store.User +} + +func newAuthFixture(t *testing.T) *authFixture { + t.Helper() + uri := os.Getenv("MONGO_TEST_URI") + if uri == "" { + t.Skip("MONGO_TEST_URI not set") + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri)) + require.NoError(t, err) + db := client.Database(fmt.Sprintf("tracker_test_%d", time.Now().UnixNano())) + t.Cleanup(func() { + c, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _ = db.Drop(c) + _ = client.Disconnect(c) + }) + require.NoError(t, store.EnsureIndexes(ctx, db)) + + f := &authFixture{ + users: store.NewAuthUserStoreFromCollection(db.Collection("auth_users")), + teams: store.NewAuthTeamStoreFromCollection(db.Collection("auth_teams")), + keys: store.NewAuthAPIKeyStoreFromCollection(db.Collection("auth_api_keys")), + } + f.sessions, err = auth.NewSessionManager(bytes.Repeat([]byte{9}, 32), time.Hour) + require.NoError(t, err) + f.cfg = auth.Config{SessionTTL: time.Hour, AnonymousPermissions: []auth.Permission{}} + f.resolver = &identity.Resolver{Users: f.users, Teams: f.teams, Keys: f.keys, Sessions: f.sessions} + + res, err := identity.Bootstrap(ctx, f.users, f.teams, "admin-password-123") + require.NoError(t, err) + f.adminsID = res.AdminsTeamID.Hex() + f.admin, err = f.users.GetByUsername(ctx, "admin") + require.NoError(t, err) + return f +} + +// principalOf resolves the principal of a stored user through the real resolver. +func (f *authFixture) principalOf(t *testing.T, u *store.User) auth.Principal { + t.Helper() + p, err := f.resolver.PrincipalForUser(context.Background(), u) + require.NoError(t, err) + return p +} + +type fakeTransportStream struct{ method string } + +func (s fakeTransportStream) Method() string { return s.method } +func (fakeTransportStream) SetHeader(metadata.MD) error { return nil } +func (fakeTransportStream) SendHeader(metadata.MD) error { return nil } +func (fakeTransportStream) SetTrailer(metadata.MD) error { return nil } + +// rpcCtx builds a context as the gRPC server would: principal plus full method name. +func rpcCtx(p auth.Principal, method string) context.Context { + ctx := grpc.NewContextWithServerTransportStream(context.Background(), + fakeTransportStream{method: "/tracker.auth.v1alpha1.AuthService/" + method}) + return auth.WithPrincipal(ctx, p) +} diff --git a/server/auth_users.go b/server/auth_users.go new file mode 100644 index 00000000..c5fd9796 --- /dev/null +++ b/server/auth_users.go @@ -0,0 +1,144 @@ +package server + +import ( + "context" + "regexp" + "strings" + + authv1 "github.com/bananaops/tracker/generated/proto/auth/v1alpha1" + "github.com/bananaops/tracker/internal/auth" + "github.com/bananaops/tracker/internal/auth/authz" + store "github.com/bananaops/tracker/internal/stores" + "go.mongodb.org/mongo-driver/bson/primitive" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var usernamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{1,63}$`) + +func (a *Auth) ListUsers(ctx context.Context, _ *authv1.ListUsersRequest) (*authv1.ListUsersResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } + users, err := a.users.List(ctx) + if err != nil { + return nil, storeError(err, "users") + } + resp := &authv1.ListUsersResponse{Users: make([]*authv1.User, 0, len(users))} + for _, u := range users { + resp.Users = append(resp.Users, toProtoUser(u)) + } + return resp, nil +} + +func (a *Auth) CreateUser(ctx context.Context, req *authv1.CreateUserRequest) (*authv1.CreateUserResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } + username := strings.TrimSpace(req.Username) + if !usernamePattern.MatchString(username) { + return nil, status.Error(codes.InvalidArgument, "username must be 2 to 64 characters of letters, digits, dot, underscore or dash") + } + if err := auth.ValidatePasswordPolicy(req.Password); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + teamIDs, err := a.parseTeamIDs(ctx, req.TeamIds) + if err != nil { + return nil, err + } + hash, err := auth.HashPassword(req.Password) + if err != nil { + return nil, status.Error(codes.Internal, "internal error") + } + user := &store.User{ + Username: username, + Email: strings.TrimSpace(req.Email), + DisplayName: strings.TrimSpace(req.DisplayName), + Source: store.UserSourceLocal, + PasswordHash: hash, + Teams: teamIDs, + MustChangePassword: true, + } + if err := a.users.Create(ctx, user); err != nil { + return nil, storeError(err, "user") + } + a.logger.Info("auth.user.create", "username", user.Username, "by", currentPrincipal(ctx).Username) + return &authv1.CreateUserResponse{User: toProtoUser(user)}, nil +} + +func (a *Auth) UpdateUser(ctx context.Context, req *authv1.UpdateUserRequest) (*authv1.UpdateUserResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } + id, err := parseObjectID(req.Id, "user") + if err != nil { + return nil, err + } + user, err := a.users.GetByID(ctx, id) + if err != nil { + return nil, storeError(err, "user") + } + caller := currentPrincipal(ctx) + if req.Disabled && caller.UserID == user.ID.Hex() { + return nil, status.Error(codes.FailedPrecondition, "you cannot disable your own account") + } + teamIDs, err := a.parseTeamIDs(ctx, req.TeamIds) + if err != nil { + return nil, err + } + if req.NewPassword != "" { + if user.Source != store.UserSourceLocal { + return nil, status.Error(codes.InvalidArgument, "password is managed by the identity provider") + } + if err := auth.ValidatePasswordPolicy(req.NewPassword); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + } + + admins, err := a.teams.GetByName(ctx, store.AdministratorsTeamName) + if err != nil { + return nil, storeError(err, "administrators team") + } + wasAdmin := !user.Disabled && containsID(user.Teams, admins.ID) + staysAdmin := !req.Disabled && containsID(teamIDs, admins.ID) + if wasAdmin && !staysAdmin { + others, err := a.users.CountEnabledInTeam(ctx, admins.ID, user.ID) + if err != nil { + return nil, storeError(err, "users") + } + if others == 0 { + return nil, status.Error(codes.FailedPrecondition, "cannot remove the last administrator") + } + } + + user.Email = strings.TrimSpace(req.Email) + user.DisplayName = strings.TrimSpace(req.DisplayName) + user.Teams = teamIDs + if req.Disabled && !user.Disabled { + user.SessionVersion++ + } + user.Disabled = req.Disabled + if req.NewPassword != "" { + hash, err := auth.HashPassword(req.NewPassword) + if err != nil { + return nil, status.Error(codes.Internal, "internal error") + } + user.PasswordHash = hash + user.MustChangePassword = true + user.SessionVersion++ + } + if err := a.users.Update(ctx, user); err != nil { + return nil, storeError(err, "user") + } + a.logger.Info("auth.user.update", "username", user.Username, "disabled", user.Disabled, "by", caller.Username) + return &authv1.UpdateUserResponse{User: toProtoUser(user)}, nil +} + +func containsID(ids []primitive.ObjectID, id primitive.ObjectID) bool { + for _, x := range ids { + if x == id { + return true + } + } + return false +} diff --git a/server/auth_users_test.go b/server/auth_users_test.go new file mode 100644 index 00000000..aef66cd7 --- /dev/null +++ b/server/auth_users_test.go @@ -0,0 +1,93 @@ +package server + +import ( + "context" + "testing" + + authv1 "github.com/bananaops/tracker/generated/proto/auth/v1alpha1" + "github.com/bananaops/tracker/internal/auth" + store "github.com/bananaops/tracker/internal/stores" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestCreateAndListUsers(t *testing.T) { + f := newAuthFixture(t) + svc := newAuthService(f) + ctx := rpcCtx(f.principalOf(t, f.admin), "CreateUser") + + resp, err := svc.CreateUser(ctx, &authv1.CreateUserRequest{ + Username: "bob", Email: "bob@example.com", DisplayName: "Bob", Password: "bob-initial-pass-1", TeamIds: []string{f.adminsID}, + }) + require.NoError(t, err) + assert.Equal(t, "bob", resp.User.Username) + assert.Equal(t, "local", resp.User.Source) + assert.True(t, resp.User.MustChangePassword) + assert.Equal(t, []string{f.adminsID}, resp.User.TeamIds) + + _, err = svc.CreateUser(ctx, &authv1.CreateUserRequest{Username: "BOB", Password: "bob-initial-pass-1"}) + assert.Equal(t, codes.AlreadyExists, status.Code(err)) + _, err = svc.CreateUser(ctx, &authv1.CreateUserRequest{Username: "bad name!", Password: "bob-initial-pass-1"}) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + _, err = svc.CreateUser(ctx, &authv1.CreateUserRequest{Username: "carol", Password: "short"}) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + _, err = svc.CreateUser(ctx, &authv1.CreateUserRequest{Username: "carol", Password: "carol-initial-pass-1", TeamIds: []string{"not-an-id"}}) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + _, err = svc.CreateUser(ctx, &authv1.CreateUserRequest{Username: "carol", Password: "carol-initial-pass-1", TeamIds: []string{"000000000000000000000000"}}) + assert.Equal(t, codes.NotFound, status.Code(err), "unknown team") + + list, err := svc.ListUsers(rpcCtx(f.principalOf(t, f.admin), "ListUsers"), &authv1.ListUsersRequest{}) + require.NoError(t, err) + assert.Len(t, list.Users, 2) +} + +func TestUpdateUserGuards(t *testing.T) { + f := newAuthFixture(t) + svc := newAuthService(f) + admin := f.principalOf(t, f.admin) + + created, err := svc.CreateUser(rpcCtx(admin, "CreateUser"), &authv1.CreateUserRequest{Username: "bob", Password: "bob-initial-pass-1"}) + require.NoError(t, err) + + // Admin cannot disable itself. + _, err = svc.UpdateUser(rpcCtx(admin, "UpdateUser"), &authv1.UpdateUserRequest{Id: f.admin.ID.Hex(), TeamIds: []string{f.adminsID}, Disabled: true}) + assert.Equal(t, codes.FailedPrecondition, status.Code(err)) + + // Admin cannot leave the Administrators team while being the last one. + _, err = svc.UpdateUser(rpcCtx(admin, "UpdateUser"), &authv1.UpdateUserRequest{Id: f.admin.ID.Hex(), TeamIds: []string{}}) + assert.Equal(t, codes.FailedPrecondition, status.Code(err)) + + // Promote bob, then admin may leave. + _, err = svc.UpdateUser(rpcCtx(admin, "UpdateUser"), &authv1.UpdateUserRequest{Id: created.User.Id, TeamIds: []string{f.adminsID}, Email: "bob@example.com"}) + require.NoError(t, err) + _, err = svc.UpdateUser(rpcCtx(admin, "UpdateUser"), &authv1.UpdateUserRequest{Id: f.admin.ID.Hex(), TeamIds: []string{}}) + require.NoError(t, err) + + // Admin joins back, so that bob is no longer the last administrator. + _, err = svc.UpdateUser(rpcCtx(admin, "UpdateUser"), &authv1.UpdateUserRequest{Id: f.admin.ID.Hex(), TeamIds: []string{f.adminsID}, DisplayName: "Administrator"}) + require.NoError(t, err) + + // Disabling bob bumps its session version; a password reset too. + before, _ := f.users.GetByUsername(context.Background(), "bob") + resp, err := svc.UpdateUser(rpcCtx(admin, "UpdateUser"), &authv1.UpdateUserRequest{Id: created.User.Id, TeamIds: []string{f.adminsID}, Disabled: true, NewPassword: "bob-reset-pass-22"}) + require.NoError(t, err) + assert.True(t, resp.User.Disabled) + after, _ := f.users.GetByUsername(context.Background(), "bob") + assert.Equal(t, before.SessionVersion+2, after.SessionVersion) + assert.True(t, after.MustChangePassword) + ok, _ := auth.VerifyPassword(after.PasswordHash, "bob-reset-pass-22") + assert.True(t, ok) + + _, err = svc.UpdateUser(rpcCtx(admin, "UpdateUser"), &authv1.UpdateUserRequest{Id: "000000000000000000000000"}) + assert.Equal(t, codes.NotFound, status.Code(err)) + _, err = svc.UpdateUser(rpcCtx(admin, "UpdateUser"), &authv1.UpdateUserRequest{Id: "zzz"}) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + + // Password reset is refused for OIDC accounts. + oidc := &store.User{Username: "sso-user", Source: store.UserSourceOIDC, OIDCIssuer: "https://idp", OIDCSubject: "abc"} + require.NoError(t, f.users.Create(context.Background(), oidc)) + _, err = svc.UpdateUser(rpcCtx(admin, "UpdateUser"), &authv1.UpdateUserRequest{Id: oidc.ID.Hex(), NewPassword: "whatever-pass-123"}) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) +} diff --git a/server/authz_wiring_test.go b/server/authz_wiring_test.go new file mode 100644 index 00000000..ad7f1a0e --- /dev/null +++ b/server/authz_wiring_test.go @@ -0,0 +1,48 @@ +package server + +import ( + "context" + "reflect" + "testing" + + catalogv1 "github.com/bananaops/tracker/generated/proto/catalog/v1alpha1" + eventv1 "github.com/bananaops/tracker/generated/proto/event/v1alpha1" + lockv1 "github.com/bananaops/tracker/generated/proto/lock/v1alpha1" + "github.com/bananaops/tracker/internal/auth" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Every RPC of the three existing services must refuse an anonymous caller +// without permissions before touching the database. The services are built +// with nil stores on purpose: reaching the store would panic. +func TestExistingServicesAreGuarded(t *testing.T) { + services := []struct { + name string + impl any + desc grpc.ServiceDesc + }{ + {"EventService", &Event{}, eventv1.EventService_ServiceDesc}, + {"CatalogService", &Catalog{}, catalogv1.CatalogService_ServiceDesc}, + {"LockService", &Lock{}, lockv1.LockService_ServiceDesc}, + } + for _, svc := range services { + v := reflect.ValueOf(svc.impl) + for _, m := range svc.desc.Methods { + method := v.MethodByName(m.MethodName) + if !method.IsValid() { + t.Errorf("%s.%s not implemented", svc.name, m.MethodName) + continue + } + full := "/" + svc.desc.ServiceName + "/" + m.MethodName + ctx := grpc.NewContextWithServerTransportStream(context.Background(), fakeTransportStream{method: full}) + ctx = auth.WithPrincipal(ctx, auth.Anonymous(nil)) + req := reflect.New(method.Type().In(1).Elem()) + out := method.Call([]reflect.Value{reflect.ValueOf(ctx), req}) + err, _ := out[1].Interface().(error) + assert.Equal(t, codes.Unauthenticated, status.Code(err), "%s.%s must call authz.Authorize first", svc.name, m.MethodName) + } + } +} diff --git a/server/catalog.go b/server/catalog.go index 6edf0f43..0ba1e356 100644 --- a/server/catalog.go +++ b/server/catalog.go @@ -7,6 +7,7 @@ import ( "os" v1alpha1 "github.com/bananaops/tracker/generated/proto/catalog/v1alpha1" + "github.com/bananaops/tracker/internal/auth/authz" "github.com/bananaops/tracker/internal/config" store "github.com/bananaops/tracker/internal/stores" "google.golang.org/protobuf/types/known/timestamppb" @@ -30,6 +31,9 @@ func (e *Catalog) CreateUpdateCatalog( ctx context.Context, i *v1alpha1.CreateUpdateCatalogRequest, ) (*v1alpha1.CreateUpdateCatalogResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } // Validation des champs requis if i.Name == "" { @@ -117,6 +121,9 @@ func (e *Catalog) GetCatalog( ctx context.Context, i *v1alpha1.GetCatalogRequest, ) (*v1alpha1.GetCatalogResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } var catalogResult = &v1alpha1.GetCatalogResponse{} var err error @@ -132,6 +139,9 @@ func (e *Catalog) ListCatalogs( ctx context.Context, i *v1alpha1.ListCatalogsRequest, ) (*v1alpha1.ListCatalogsResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } var catalogsResult = &v1alpha1.ListCatalogsResponse{} var err error @@ -149,6 +159,9 @@ func (e *Catalog) DeleteCatalog( ctx context.Context, i *v1alpha1.DeleteCatalogRequest, ) (*v1alpha1.DeleteCatalogResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } var catalogResult = &v1alpha1.DeleteCatalogResponse{} @@ -164,6 +177,9 @@ func (e *Catalog) GetVersionCompliance( ctx context.Context, i *v1alpha1.GetVersionComplianceRequest, ) (*v1alpha1.GetVersionComplianceResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } var response = &v1alpha1.GetVersionComplianceResponse{} var projectCompliances []*v1alpha1.ProjectCompliance @@ -304,6 +320,9 @@ func (e *Catalog) UpdateVersions( ctx context.Context, i *v1alpha1.UpdateVersionsRequest, ) (*v1alpha1.UpdateVersionsResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } e.logger.Info("🔧 Updating versions for service", "name", i.Name, @@ -348,6 +367,9 @@ func (e *Catalog) UpdateDependencies( ctx context.Context, i *v1alpha1.UpdateDependenciesRequest, ) (*v1alpha1.UpdateDependenciesResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } // Validation if i.Name == "" { diff --git a/server/event.go b/server/event.go index e17ed180..b6b57094 100644 --- a/server/event.go +++ b/server/event.go @@ -10,6 +10,7 @@ import ( v1alpha1 "github.com/bananaops/tracker/generated/proto/event/v1alpha1" lock "github.com/bananaops/tracker/generated/proto/lock/v1alpha1" + "github.com/bananaops/tracker/internal/auth/authz" "github.com/bananaops/tracker/internal/config" store "github.com/bananaops/tracker/internal/stores" "github.com/bananaops/tracker/internal/utils" @@ -102,6 +103,9 @@ func (e *Event) CreateEvent( ctx context.Context, i *v1alpha1.CreateEventRequest, ) (*v1alpha1.CreateEventResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } var event = &v1alpha1.Event{ Title: i.Title, @@ -164,6 +168,15 @@ func (e *Event) CreateEvent( EventId: "", // Sera mis à jour après la création de l'événement } + // This is an internal call: it reuses the request context, so the + // method name authz sees stays "/tracker.event.v1alpha1.EventService/ + // CreateEvent", not CreateLock. The lock is therefore authorized by + // event:write, not lock:write, even though spec 3.1 files CreateLock + // under lock:write. That is deliberate: creating an event that locks a + // service is one operation from the caller's point of view. + // Side effect: tracker_auth_requests_total counts such a request twice + // under the same method label, once for CreateEvent and once for the + // nested Authorize below. Same for the UpdateLock call further down. _, err := e.lockService.CreateLock(ctx, lockReq) if err != nil { e.logger.Error("failed to create lock", @@ -201,6 +214,9 @@ func (e *Event) CreateEvent( existingLock, err2 := e.lockService.store.Get(ctx, filter) if err2 == nil && existingLock != nil && existingLock.Id != "" { + // Internal call on the request context, see the comment above the + // CreateLock call: authorized by event:write and counted a second + // time in tracker_auth_requests_total under CreateEvent. _, errUpd := e.lockService.UpdateLock(ctx, &lock.UpdateLockRequest{ Id: existingLock.Id, EventId: eventResult.Event.Metadata.Id, @@ -254,6 +270,9 @@ func (e *Event) GetEvent( ctx context.Context, i *v1alpha1.GetEventRequest, ) (*v1alpha1.GetEventResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } var eventResult = &v1alpha1.GetEventResponse{} var err error @@ -277,6 +296,9 @@ func (e *Event) SearchEvents( ctx context.Context, i *v1alpha1.SearchEventsRequest, ) (*v1alpha1.SearchEventsResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } filter, err := utils.CreateFilter(i) if err != nil { @@ -297,6 +319,9 @@ func (e *Event) ListEvents( ctx context.Context, i *v1alpha1.ListEventsRequest, ) (*v1alpha1.ListEventsResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } var eventsResult = &v1alpha1.ListEventsResponse{} var err error @@ -314,6 +339,9 @@ func (e *Event) TodayEvents( ctx context.Context, i *v1alpha1.TodayEventsRequest, ) (*v1alpha1.TodayEventsResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } today := time.Now().Format("2006-01-02") @@ -341,6 +369,9 @@ func (e *Event) UpdateEvent( ctx context.Context, i *v1alpha1.UpdateEventRequest, ) (*v1alpha1.UpdateEventResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } var eventResult = &v1alpha1.UpdateEventResponse{} var eventDatabase = &v1alpha1.GetEventResponse{} @@ -529,10 +560,17 @@ func (e *Event) UpdateEvent( return eventResult, nil } -func (e *Event) DeleteEvent( +// DeleteEvents implements the EventService.DeleteEvents RPC. The method is +// named DeleteEvents (plural) to match the generated EventServiceServer +// interface; the previous DeleteEvent (singular) name never satisfied that +// interface, so the RPC always fell back to the embedded unimplemented stub. +func (e *Event) DeleteEvents( ctx context.Context, i *v1alpha1.DeleteEventRequest, ) (*v1alpha1.DeleteEventResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } var eventResult = &v1alpha1.DeleteEventResponse{} @@ -548,6 +586,9 @@ func (e *Event) AddChangelogEntry( ctx context.Context, i *v1alpha1.AddChangelogEntryRequest, ) (*v1alpha1.AddChangelogEntryResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } // Retrieve the existing event eventDatabase, err := e.store.Get(ctx, map[string]interface{}{"metadata.id": i.Id}) @@ -595,6 +636,9 @@ func (e *Event) GetEventChangelog( ctx context.Context, i *v1alpha1.GetEventChangelogRequest, ) (*v1alpha1.GetEventChangelogResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } // Retrieve the existing event eventDatabase, err := e.store.Get(ctx, map[string]interface{}{"metadata.id": i.Id}) @@ -655,6 +699,9 @@ func (e *Event) AddSlackId( ctx context.Context, i *v1alpha1.AddSlackIdRequest, ) (*v1alpha1.AddSlackIdResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } // Retrieve the existing event eventDatabase, err := e.store.Get(ctx, map[string]interface{}{"metadata.id": i.Id}) @@ -721,6 +768,9 @@ func (e *Event) GetEventStats( ctx context.Context, i *v1alpha1.GetEventStatsRequest, ) (*v1alpha1.GetEventStatsResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } // Build filter from request statsFilter := &utils.StatsFilter{ @@ -801,6 +851,9 @@ func (e *Event) GetEventStatsByMonth( ctx context.Context, i *v1alpha1.GetEventStatsByMonthRequest, ) (*v1alpha1.GetEventStatsByMonthResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } // Build filter from request statsFilter := &utils.StatsFilter{ diff --git a/server/homer.go b/server/homer.go index 7ed0f2f0..123ab73e 100644 --- a/server/homer.go +++ b/server/homer.go @@ -8,6 +8,8 @@ import ( "net/http" "time" + "github.com/bananaops/tracker/internal/auth" + "github.com/bananaops/tracker/internal/auth/authz" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "gopkg.in/yaml.v3" ) @@ -90,7 +92,7 @@ func RegisterHomerHandler(mux *runtime.ServeMux, homerURL string) { return } - err := mux.HandlePath("GET", "/api/homer-links", func(w http.ResponseWriter, r *http.Request, _ map[string]string) { + err := mux.HandlePath("GET", "/api/homer-links", authz.RequireHTTP(auth.PermLinksRead, func(w http.ResponseWriter, r *http.Request, _ map[string]string) { result, fetchErr := FetchHomerLinks(homerURL) if fetchErr != nil { slog.Error("Failed to fetch Homer links", "error", fetchErr) @@ -103,7 +105,7 @@ func RegisterHomerHandler(mux *runtime.ServeMux, homerURL string) { if encErr := json.NewEncoder(w).Encode(result); encErr != nil { slog.Error("Failed to encode homer links response", "error", encErr) } - }) + })) if err != nil { slog.Error("Failed to register /api/homer-links handler", "error", err) } diff --git a/server/links.go b/server/links.go index 025c1a81..f7a71d80 100644 --- a/server/links.go +++ b/server/links.go @@ -6,6 +6,8 @@ import ( "net/http" "strings" + "github.com/bananaops/tracker/internal/auth" + "github.com/bananaops/tracker/internal/auth/authz" store "github.com/bananaops/tracker/internal/stores" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" ) @@ -23,19 +25,19 @@ func RegisterLinksHandler(mux *runtime.ServeMux) { initLinksStore() // GET /api/links - if err := mux.HandlePath("GET", "/api/links", handleListLinks); err != nil { + if err := mux.HandlePath("GET", "/api/links", authz.RequireHTTP(auth.PermLinksRead, handleListLinks)); err != nil { slog.Error("Failed to register GET /api/links", "error", err) } // POST /api/links - if err := mux.HandlePath("POST", "/api/links", handleCreateLink); err != nil { + if err := mux.HandlePath("POST", "/api/links", authz.RequireHTTP(auth.PermLinksWrite, handleCreateLink)); err != nil { slog.Error("Failed to register POST /api/links", "error", err) } // PUT /api/links/{id} - if err := mux.HandlePath("PUT", "/api/links/{id}", handleUpdateLink); err != nil { + if err := mux.HandlePath("PUT", "/api/links/{id}", authz.RequireHTTP(auth.PermLinksWrite, handleUpdateLink)); err != nil { slog.Error("Failed to register PUT /api/links/{id}", "error", err) } // DELETE /api/links/{id} - if err := mux.HandlePath("DELETE", "/api/links/{id}", handleDeleteLink); err != nil { + if err := mux.HandlePath("DELETE", "/api/links/{id}", authz.RequireHTTP(auth.PermLinksWrite, handleDeleteLink)); err != nil { slog.Error("Failed to register DELETE /api/links/{id}", "error", err) } } diff --git a/server/lock.go b/server/lock.go index 4e4f5284..18b0492d 100644 --- a/server/lock.go +++ b/server/lock.go @@ -8,6 +8,7 @@ import ( eventv1alpha1 "github.com/bananaops/tracker/generated/proto/event/v1alpha1" v1alpha1 "github.com/bananaops/tracker/generated/proto/lock/v1alpha1" + "github.com/bananaops/tracker/internal/auth/authz" "github.com/bananaops/tracker/internal/config" store "github.com/bananaops/tracker/internal/stores" "google.golang.org/protobuf/types/known/timestamppb" @@ -33,6 +34,9 @@ func (e *Lock) CreateLock( ctx context.Context, i *v1alpha1.CreateLockRequest, ) (*v1alpha1.CreateLockResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } var lock = &v1alpha1.Lock{ Service: i.Service, @@ -118,6 +122,9 @@ func (e *Lock) GetLock( ctx context.Context, i *v1alpha1.GetLockRequest, ) (*v1alpha1.GetLockResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } var lockResult = &v1alpha1.GetLockResponse{} var err error @@ -133,6 +140,9 @@ func (e *Lock) UpdateLock( ctx context.Context, i *v1alpha1.UpdateLockRequest, ) (*v1alpha1.UpdateLockResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } // Retrieve existing lock by id existing, err := e.store.Get(ctx, map[string]interface{}{"id": i.Id}) @@ -179,6 +189,9 @@ func (e *Lock) UnLock( ctx context.Context, i *v1alpha1.UnLockRequest, ) (*v1alpha1.UnLockResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } var lockResult = &v1alpha1.GetLockResponse{} var err error @@ -240,6 +253,9 @@ func (e *Lock) ListLocks( ctx context.Context, i *v1alpha1.ListLocksRequest, ) (*v1alpha1.ListLocksResponse, error) { + if err := authz.Authorize(ctx); err != nil { + return nil, err + } var LocksResult = &v1alpha1.ListLocksResponse{} var err error