Compare commits

..

131 Commits

Author SHA1 Message Date
Felix Gateru f8410b8940 SMQ-3338 - Add created at period filter to entities (#3339)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2026-03-04 12:37:35 +01:00
dependabot[bot] 2260293dfc NOISSUE - Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace from 1.40.0 to 1.41.0 (#3374)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-03 15:32:32 +01:00
dependabot[bot] c9f34e3759 NOISSUE - Bump github.com/authzed/spicedb from 1.49.1 to 1.49.2 (#3372)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-03 15:31:41 +01:00
dependabot[bot] aeb1d9d0a1 NOISSUE - Bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp from 0.65.0 to 0.66.0 (#3371)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-03 15:29:25 +01:00
dependabot[bot] 03a8feb679 NOISSUE - Bump go.opentelemetry.io/otel/sdk from 1.40.0 to 1.41.0 (#3375)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-03 15:26:08 +01:00
Felix Gateru 9c2608659f SMQ-1672 - Revoke refresh token (#3241)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
Co-authored-by: nyagamunene <stevenyaga2014@gmail.com>
2026-03-03 15:22:28 +01:00
Steve Munene 2c476c17ee NOISSUE - Update tags filtering (#3376)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2026-03-03 15:14:02 +01:00
Dušan Borovčanin 3b2dd09ab4 NOISSUE - Update script dependencies (#3370)
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-03-02 18:32:06 +01:00
dependabot[bot] 5255d887ef NOISSUE - Bump github.com/nats-io/nats.go from 1.48.0 to 1.49.0 (#3369)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 15:39:42 +01:00
dependabot[bot] bb8917cc34 NOISSUE - Bump golang from 1.25.7-alpine3.22 to 1.26.0-alpine3.22 in /docker in the docker-dependency group (#3367)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 11:23:24 +01:00
dependabot[bot] 4792a99c9a NOISSUE - Bump the gh-dependency group in /.github/workflows with 2 updates (#3368)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 11:19:44 +01:00
dusan fe49305238 Update dependencies version
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-03-02 11:17:34 +01:00
Steve Munene 03143d4142 NOISSUE - Add migrations for UI metadata (#3366)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2026-02-27 17:00:04 +01:00
Steve Munene 15a6c026e9 NOISSUE - Seperate PAT from policy (#3330)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Co-authored-by: Arvindh <arvindh91@gmail.com>
2026-02-26 16:35:47 +01:00
dependabot[bot] 175f0e08ab NOISSUE - Bump google.golang.org/grpc from 1.78.0 to 1.79.1 (#3355)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 10:10:57 +01:00
dependabot[bot] 41526634f8 NOISSUE - Bump github.com/redis/go-redis/v9 from 9.17.3 to 9.18.0 (#3356)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 10:09:57 +01:00
dependabot[bot] 73a640e646 NOISSUE - Bump github.com/lib/pq from 1.11.1 to 1.11.2 (#3358)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 10:07:51 +01:00
dependabot[bot] 6ca535155a NOISSUE - Bump github.com/caarlos0/env/v11 from 11.3.1 to 11.4.0 (#3363)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 14:19:03 +01:00
dependabot[bot] 4a449629b3 NOISUE - Bump github.com/authzed/authzed-go from 1.7.0 to 1.8.0 (#3362)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-24 09:02:40 +01:00
dependabot[bot] 1cd00fbd83 NOISSUE - Bump github.com/pion/dtls/v3 from 3.1.1 to 3.1.2 (#3357)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 16:29:10 +01:00
Felix Gateru a871fa926e SMQ-3361 - Enable TLS and mTLS termination on MQTT adapter (#3360)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2026-02-23 16:14:31 +01:00
dependabot[bot] c7fad0d2ca Bump github.com/pion/dtls/v3 from 3.1.0 to 3.1.1 (#3351)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 19:47:31 +01:00
dependabot[bot] 51e782693d NOISSUE - Bump github.com/pion/dtls/v3 from 3.0.10 to 3.1.0 (#3350)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 14:53:31 +01:00
Dušan Borovčanin e3a3e052d2 NOISSUE - Use the latest stable Go version instead of RC (#3349)
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-02-10 13:12:46 +01:00
dependabot[bot] 1e8437cfc2 NOISSUE - Bump github.com/authzed/spicedb from 1.48.0 to 1.49.1 (#3347)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 12:51:10 +01:00
dependabot[bot] f8f4ccac79 NOISSUE - Bump golang.org/x/crypto from 0.47.0 to 0.48.0 (#3348)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 12:29:04 +01:00
dependabot[bot] 56bb07273d NOISSUE - Bump github.com/go-chi/chi/v5 from 5.2.4 to 5.2.5 (#3344)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dušan Borovčanin <borovcanindusan1@gmail.com>
2026-02-09 17:48:37 +01:00
dependabot[bot] aba4d6a0b7 NOISSUE - Bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp from 0.64.0 to 0.65.0 (#3345)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-09 17:47:29 +01:00
dependabot[bot] b948edc573 Bump golang.org/x/oauth2 from 0.34.0 to 0.35.0 (#3346)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-09 12:26:38 +01:00
Arvindh 20bafe0077 NOISSUE - Fix Invitations and remove Domain check in AuthZ (#3340)
Signed-off-by: Arvindh <arvindh91@gmail.com>
2026-02-04 20:56:41 +01:00
Felix Gateru 4e77ae65e2 SMQ-2875 - Add listing entities by multiple tag values with support for AND/OR conditions (#3154)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2026-02-04 17:13:48 +01:00
Steve Munene 61c120f947 SMQ-2627 - Align PATs with new architecture (#3295)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2026-02-03 14:06:30 +01:00
dependabot[bot] 0d4f4c9266 NOISSUE - Bump go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc from 0.64.0 to 0.65.0 (#3334)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 11:42:55 +01:00
dependabot[bot] 668338dfa2 NOISSUE - Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp from 1.39.0 to 1.40.0 (#3335)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 11:41:17 +01:00
dependabot[bot] 38ea004e30 NOISSUE - Bump go.opentelemetry.io/otel/sdk from 1.39.0 to 1.40.0 (#3337)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 11:30:51 +01:00
dependabot[bot] 7b99baeb02 NOISSUE - Bump github.com/lib/pq from 1.10.9 to 1.11.1 (#3331)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 15:06:31 +01:00
Steve Munene cd31b1c6bd NOISSUE - Refactor makefile (#3318)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
Signed-off-by: dorcaslitunya <anonolitunya@gmail.com>
Co-authored-by: dorcaslitunya <anonolitunya@gmail.com>
2026-01-29 09:50:15 +01:00
dependabot[bot] 1a348b11dd NOISSUE - Bump github.com/dgraph-io/ristretto/v2 from 2.3.0 to 2.4.0 (#3324)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-27 13:57:19 +01:00
dependabot[bot] f552e08661 NOISSUE - Bump github.com/redis/go-redis/v9 from 9.17.2 to 9.17.3 (#3325)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-27 12:17:45 +01:00
dependabot[bot] 5606cdbe47 NOISSUE - Bump github.com/plgd-dev/go-coap/v3 from 3.4.1 to 3.4.2 (#3326)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-27 12:00:39 +01:00
Felix Gateru 59d9f91813 NOISSUE - Remove public metadata, introduce private metadata (#3322)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2026-01-23 14:42:54 +01:00
Felix Gateru 5b913dd46b SMQ-3108 - Add support for public and private metadata for users and clients (#3155)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2026-01-22 08:55:25 +01:00
Steve Munene 9293de7636 NOISSUE - Fix certs network (#3321)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2026-01-21 14:06:55 +01:00
Steve Munene 2dd8c44aa3 NOISSUE - Fix certs override file (#3317)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2026-01-21 09:40:54 +01:00
Felix Gateru 28fc9738df SMQ-3303 - Update API schema request bodies (#3309)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2026-01-19 12:10:46 +01:00
Steve Munene ad1d67a678 NOISSUE - Fetch certs (#3316)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2026-01-19 11:38:20 +01:00
dependabot[bot] 1086f3e208 NOISSUE - Bump golang.org/x/crypto from 0.46.0 to 0.47.0 (#3308)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-19 09:59:59 +01:00
dependabot[bot] b896c03f5f NOISSUE - Bump golang from 1.25.5-alpine3.22 to 1.26rc2-alpine3.22 in /docker in the docker-dependency group (#3314)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-19 09:38:40 +01:00
dependabot[bot] d78c1e8d20 NOISSUE - Bump github.com/go-chi/chi/v5 from 5.2.3 to 5.2.4 (#3315)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-19 09:38:11 +01:00
Felix Gateru 551d44d58e NOISSUE - Fix publisher id on message published by user (#3312)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2026-01-15 17:34:11 +01:00
Felix Gateru 927230dbb4 NOISSUE - Allow superadmin to send messages over channel (#3310)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2026-01-14 18:38:44 +01:00
dependabot[bot] c7996ce6cf NOISSUE - Bump github.com/pion/dtls/v3 from 3.0.9 to 3.0.10 (#3307)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-12 08:52:20 +01:00
dusan bf7a965195 NOISSUE - Update license file
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2025-12-31 10:52:10 +01:00
Felix Gateru 67c28ff134 SMQ-2800 - Add WebSocket support to HTTP adapter (#2937)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Co-authored-by: Arvindh <arvindh91@gmail.com>
2025-12-31 10:43:52 +01:00
Arvindh a526a2ccd5 NOISSUE - Add internal type to find NestError and Add Auth key algorithm to journal docker compose (#3304)
Signed-off-by: Arvindh <arvindh91@gmail.com>
2025-12-30 16:20:35 +01:00
dependabot[bot] c679224596 NOISSUE - Bump google.golang.org/grpc from 1.77.0 to 1.78.0 (#3306)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-29 12:11:18 +01:00
dependabot[bot] e1c881d2ad NOISSUE - Bump github.com/jackc/pgx/v5 from 5.7.6 to 5.8.0 (#3305)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-29 11:38:25 +01:00
dusan 9a0d8062be NOISSUE - Add make_stable target
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2025-12-29 10:40:08 +01:00
dusan 25d8b86f2b Revert "NOISSUE - Use fixed version in default Docker config"
This reverts commit 24ca513eff.

We are going to stick with the latest for dev env.
2025-12-29 09:40:56 +01:00
dusan 24ca513eff NOISSUE - Use fixed version in default Docker config
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2025-12-29 09:32:55 +01:00
Dušan Borovčanin 52510d8c62 NOISSUE - Improve JWKS (#3301)
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2025-12-26 18:15:12 +01:00
Dušan Borovčanin 6b6bab79c6 NOISSUE - Update Auth service tests (#3300)
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2025-12-24 18:44:26 +01:00
Felix Gateru 6a5d28c65a SMQ-1672 - Add asymmetric key authentication (#3228)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2025-12-23 21:16:06 +01:00
Felix Gateru 59f8d4e4d7 NOISSUE : Revert removal of errors Unwrap function (#3299)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2025-12-23 15:37:51 +01:00
Felix Gateru e23555ee2a SMQ-3036 - Add events tests for channels,clients, domains and groups (#3279)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2025-12-23 12:18:39 +01:00
Felix Gateru cb337a38fd SMQ-3036 - Add nested mocks and generated proto files to ignore in codecov.yaml (#3280)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2025-12-23 12:17:38 +01:00
Felix Gateru f9b503f1d6 SMQ-3125 - Add ordering tests (#3297)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2025-12-22 15:01:57 +01:00
Steve Munene 0fe5a6d6e8 SMQ-2632 - Remove PAT logics from middleware (#3291)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2025-12-22 11:10:16 +01:00
dependabot[bot] f612109f24 NOISSUE - Bump github.com/nats-io/nats.go from 1.47.0 to 1.48.0 (#3296)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-22 08:48:27 +01:00
Arvindh 3fcf2e5369 SMQ-1744 - Error handling with TypedError created on top existing Error (#3170)
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
Co-authored-by: Felix Gateru <felix.gateru@gmail.com>
2025-12-22 08:31:52 +01:00
Steve Munene 1355bc8bb7 SMQ-2757 - Combine Authorization and AuthorizationPAT to single gRPC endpoint and combine service functions (#3292)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2025-12-19 17:51:06 +01:00
Veddy 1e7e65d642 SMQ-3290 - Fix the bug that groups queries too slowly at multiple levels (#3293)
Signed-off-by: Veddy <veddy@qq.com>
Co-authored-by: veddy <veddy@qq.com>
2025-12-18 09:47:36 +01:00
Steve Munene 3a5c1e12df NOISSUE - New operation permission (#3281)
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
Co-authored-by: Arvindh <arvindh91@gmail.com>
2025-12-17 14:29:28 +01:00
dependabot[bot] 61b95a28c2 NOISSUE - Bump google.golang.org/protobuf from 1.36.10 to 1.36.11 (#3288)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 13:10:41 +01:00
dependabot[bot] 4c40b8b82c NOISSUE - Bump github.com/authzed/spicedb from 1.47.1 to 1.48.0 (#3289)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 13:10:05 +01:00
dependabot[bot] 955c556fe6 NOISSUE - Bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp from 0.63.0 to 0.64.0 (#3283)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 18:40:45 +01:00
dependabot[bot] dc437a8296 NOISSUE - Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp from 1.38.0 to 1.39.0 (#3284)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 18:38:18 +01:00
dependabot[bot] 314537f7c5 NOISSUE - Bump go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc from 0.63.0 to 0.64.0 (#3286)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 18:36:43 +01:00
dependabot[bot] 7e5fd0cfa8 NOISSUE - Bump github.com/pion/dtls/v3 from 3.0.8 to 3.0.9 (#3287)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 17:25:45 +01:00
dependabot[bot] 6a2c8a3ca6 NOISSUE - Bump the gh-dependency group in /.github/workflows with 2 updates (#3285)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 17:21:26 +01:00
dependabot[bot] 2bea4412d8 NOISSUE - Bump golang.org/x/crypto from 0.45.0 to 0.46.0 (#3282)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 17:20:51 +01:00
dusan c8cb0daa6c NOISSUE - Update Go version detection
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2025-12-09 12:28:25 +01:00
dusan f8017c8b9f NOISSUE - Update Go version
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2025-12-09 12:18:01 +01:00
dependabot[bot] 27fc32c153 NOISSUE - Bump golang from 1.25.4-alpine3.22 to 1.25.5-alpine3.22 in /docker in the docker-dependency group (#3271)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-09 12:11:37 +01:00
dependabot[bot] 9aa9487207 NOISSUE - Bump golang.org/x/oauth2 from 0.33.0 to 0.34.0 (#3276)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-09 12:11:08 +01:00
dependabot[bot] 1292095cdb NOISSUE - Bump github.com/pion/dtls/v3 from 3.0.7 to 3.0.8 (#3273)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-09 12:06:04 +01:00
dependabot[bot] 856ef1ba49 NOISSUE - Bump github.com/spf13/cobra from 1.10.1 to 1.10.2 (#3272)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-09 12:04:25 +01:00
dependabot[bot] 146028fb98 NOISSUE - Bump golang.org/x/sync from 0.18.0 to 0.19.0 (#3277)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-09 12:01:20 +01:00
dependabot[bot] 6cb3e635c9 NOISSUE - Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace from 1.38.0 to 1.39.0 (#3278)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 22:02:00 +01:00
dependabot[bot] 175d27c4b6 NOISSUE - Bump the gh-dependency group in /.github/workflows with 2 updates (#3274)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 18:44:39 +01:00
Arvindh eec6b8be07 NOISSUE - Fix migrations error (#3275)
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
Co-authored-by: Felix Gateru <felix.gateru@gmail.com>
2025-12-08 18:43:51 +01:00
Dušan Borovčanin e48e5dee1f NOISSUE - Improve CI (#3270)
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2025-12-08 10:12:12 +01:00
Dušan Borovčanin 0df8e8451c NOISSUE - Make CI scripts run in parallel (#3269)
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2025-12-02 22:58:23 +01:00
dependabot[bot] e77e3a3bb6 NOISSUE - Bump github.com/redis/go-redis/v9 from 9.17.1 to 9.17.2 (#3268)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 01:37:53 +01:00
Dušan Borovčanin 3e6b771850 NOISSUE - Improve Swagger GH actions (#3267)
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2025-12-02 00:13:42 +01:00
Nataly Musilah 15e756a5a3 SMQ-3026 - Update READMEs and make them use the same l&f (#3239)
Signed-off-by: Musilah <nataleigh.nk@gmail.com>
2025-12-01 18:52:53 +01:00
Steve Munene c5a4336d43 SMQ-3094 - Improve test coverage in channels and journals (#3238)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2025-12-01 18:15:57 +01:00
Felix Gateru b05ae65e24 SMQ-3095 - Improve test coverage in clients repository (#3156)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2025-12-01 17:57:43 +01:00
Felix Gateru 1a8d11acdd SMQ-3095 - Add users events tests (#3240)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2025-12-01 17:51:24 +01:00
Steve Munene 27b72db52e SMQ-3233 - Move callout to seperate middleware (#3244)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2025-12-01 17:40:49 +01:00
dependabot[bot] e75ce59998 NOISSUE - Bump github.com/redis/go-redis/v9 from 9.17.0 to 9.17.1 (#3263)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 17:13:36 +01:00
Felix Gateru cd281c2589 SMQ-2799 - Add support for basic auth for HTTP and WS adapters (#3049)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2025-12-01 16:51:49 +01:00
Felix Gateru 9d13d5b528 SMQ-3224 - Add consistent error for assigning channel and client with parent group to parent group (#3225)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2025-12-01 16:32:58 +01:00
Felix Gateru f7dcaa949b SMQ-2997 - Allow listing root groups with groups that the user has access to (#3226)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
2025-12-01 14:15:31 +01:00
dusan fa080ab0f8 NOISSUE - Add DeepWiki link
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2025-11-29 14:08:35 +01:00
dusan 5807cf1754 NOISSUE - Add HTTP server timeouts
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2025-11-28 18:21:25 +01:00
dusan f96ddab9dd NOISSUE - Update Abstract Machines link
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2025-11-28 15:34:19 +01:00
Steve Munene 914d491a13 NOISSUE - Update search in CLI (#3261)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2025-11-28 15:31:21 +01:00
Washington Kigani Kamadi 843a0cae1f SMQ-3234 - Add notifications service (#3254)
Signed-off-by: WashingtonKK <washingtonkigan@gmail.com>
Signed-off-by: dusan <borovcanindusan1@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Co-authored-by: Arvindh <arvindh91@gmail.com>
Co-authored-by: dusan <borovcanindusan1@gmail.com>
2025-11-28 15:26:55 +01:00
Arvindh 9ba9a12084 NOISSUE - Enable oAuth users to change and reset password (#3259)
Signed-off-by: Arvindh <arvindh91@gmail.com>
2025-11-27 18:02:57 +01:00
Steve Munene 5ed6b258a0 NOISSUE - Update health sdk (#3256)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2025-11-26 15:39:34 +01:00
Steve Munene eb83edbcde SMQ-3253 - Update CLI usage (#3252)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2025-11-25 20:02:23 +01:00
dependabot[bot] 5c12bc8c91 NOISSUE - Bump github.com/plgd-dev/go-coap/v3 from 3.4.0 to 3.4.1 (#3246)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 12:44:33 +01:00
dependabot[bot] 589209eae2 NOISSUE - Bump github.com/redis/go-redis/v9 from 9.16.0 to 9.17.0 (#3247)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 12:44:00 +01:00
dependabot[bot] a2e660b8a1 NOISSUE - Bump google.golang.org/grpc from 1.76.0 to 1.77.0 (#3249)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 12:42:43 +01:00
dependabot[bot] 21731aef37 NOISSUE - Bump github.com/rubenv/sql-migrate from 1.8.0 to 1.8.1 (#3248)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 12:42:11 +01:00
dependabot[bot] 2f55947305 NOISSUE - Bump actions/checkout from 5 to 6 in /.github/workflows in the gh-dependency group (#3251)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 11:42:34 +01:00
dependabot[bot] 80806c4745 NOISSUE - Bump github.com/authzed/authzed-go from 1.6.0 to 1.7.0 (#3250)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 11:20:29 +01:00
dependabot[bot] 1947f6e78e NOISSUE - Bump github.com/authzed/spicedb from 1.47.0 to 1.47.1 (#3245)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-23 22:29:55 +01:00
Dušan Borovčanin 5b0c556b60 NOISSUE - Security fixes (#3243)
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2025-11-20 23:51:23 +01:00
dependabot[bot] 5891e4bb75 NOISSUE - Bump golang.org/x/crypto from 0.44.0 to 0.45.0 (#3242)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-20 10:19:46 +01:00
dependabot[bot] 299742096a NOISSUE - Bump golang.org/x/crypto from 0.43.0 to 0.44.0 (#3236)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-19 09:20:17 +01:00
dependabot[bot] 27c57b192b NOISSUE - Bump golang from 1.25.3-alpine to 1.25.4-alpine in /docker in the docker-dependency group (#3229)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-11 22:06:12 +01:00
dependabot[bot] 8083cec373 NOISSUE - Bump golang.org/x/oauth2 from 0.32.0 to 0.33.0 (#3231)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-11 21:49:12 +01:00
dependabot[bot] e2d9b1ddf9 NOISSUe - Bump golangci/golangci-lint-action from 8 to 9 in /.github/workflows in the gh-dependency group (#3232)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 13:56:39 +01:00
dependabot[bot] 6092bc2cf0 NOISSUE - Bump golang.org/x/sync from 0.17.0 to 0.18.0 (#3230)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 13:55:45 +01:00
b1ackd0t 071d23a5f0 SMQ-3173 - Use buildx for Multi-Platform Docker Image Support (#3181)
Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>
2025-11-10 13:42:42 +01:00
dependabot[bot] 7bb4b298c5 NOISSUE - Bump github.com/opencontainers/runc from 1.2.3 to 1.2.8 (#3227)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-07 16:09:41 +01:00
dependabot[bot] cac6c648d3 NOISSUE - Bump github.com/gofrs/uuid/v5 from 5.3.2 to 5.4.0 (#3223)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 11:07:32 +01:00
dependabot[bot] 4ee7736b32 NOISSUE - Bump github.com/redis/go-redis/v9 from 9.14.1 to 9.16.0 (#3206)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 15:18:05 +01:00
dependabot[bot] 8ed8915e4f NOISSUE - Bump github.com/authzed/spicedb from 1.46.0 to 1.47.0 (#3208)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 12:40:48 +01:00
387 changed files with 35922 additions and 12340 deletions
+3 -2
View File
@@ -5,9 +5,10 @@
coverage:
ignore:
- "tools/*"
- "api/grpc/**"
- "cmd/*"
- "coap/*"
- "*/mocks/*"
- "coap/**"
- "**/mocks/*"
- "*/middleware/*"
- "pkg/sid/mock.go"
- "pkg/tracing/utils.go"
+181
View File
@@ -0,0 +1,181 @@
<!--
Copyright (c) Abstract Machines
SPDX-License-Identifier: Apache-2.0
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SuperMQ API Documentation</title>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.30.3/swagger-ui.css">
<style>
body {
margin: 0;
padding: 0;
}
.topbar {
display: none;
}
.service-selector {
background: #1b1b1b;
padding: 20px;
text-align: center;
}
.service-selector h1 {
color: #fff;
margin: 0 0 15px 0;
font-family: sans-serif;
}
.service-dropdown-container {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
.service-dropdown-container label {
color: #fff;
font-family: sans-serif;
font-size: 14px;
font-weight: 500;
}
.service-dropdown {
background: #2d2d2d;
color: white;
border: 1px solid #4990e2;
padding: 10px 40px 10px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
min-width: 200px;
appearance: none;
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><path fill="%23ffffff" d="M6 9L1 4h10z"/></svg>');
background-repeat: no-repeat;
background-position: right 12px center;
}
.service-dropdown:hover {
background-color: #3a3a3a;
border-color: #357abd;
}
.service-dropdown:focus {
outline: none;
border-color: #4990e2;
box-shadow: 0 0 0 2px rgba(73, 144, 226, 0.3);
}
/* Responsive styles for mobile */
@media (max-width: 768px) {
.service-selector {
padding: 15px 10px;
}
.service-selector h1 {
font-size: 1.5rem;
margin: 0 0 12px 0;
}
.service-dropdown-container {
flex-direction: column;
gap: 8px;
}
.service-dropdown-container label {
font-size: 13px;
}
.service-dropdown {
width: 100%;
max-width: 300px;
min-width: auto;
font-size: 13px;
padding: 8px 35px 8px 12px;
}
}
@media (max-width: 480px) {
.service-selector h1 {
font-size: 1.25rem;
}
.service-dropdown {
max-width: 250px;
}
}
</style>
</head>
<body>
<div class="service-selector">
<h1>SuperMQ API Documentation</h1>
<div class="service-dropdown-container">
<label for="serviceDropdown">Select Service:</label>
<select id="serviceDropdown" class="service-dropdown"></select>
</div>
</div>
<div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.30.3/swagger-ui-bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.30.3/swagger-ui-standalone-preset.js"></script>
<script>
// Available API specifications
const APIs = APIS_PLACEHOLDER;
// Get the service from URL query parameter, default to first service
function getServiceFromURL() {
const params = new URLSearchParams(window.location.search);
const service = params.get('service');
return service && APIs.includes(service) ? service : APIs[0];
}
// Update URL with selected service
function updateURL(service) {
const url = new URL(window.location);
url.searchParams.set('service', service);
window.history.pushState({}, '', url);
}
// Create service selector dropdown
function createServiceDropdown() {
const dropdown = document.getElementById('serviceDropdown');
const currentService = getServiceFromURL();
APIs.forEach(api => {
const option = document.createElement('option');
option.value = api;
let serviceName = api.replace('.yaml', '').replace(/^\w/, c => c.toUpperCase());
if (serviceName.toLowerCase() === 'http') {
serviceName = 'HTTP';
}
option.textContent = serviceName;
if (api === currentService) {
option.selected = true;
}
dropdown.appendChild(option);
});
// Handle dropdown change
dropdown.addEventListener('change', (e) => {
const selectedApi = e.target.value;
loadSwaggerUI(selectedApi);
updateURL(selectedApi);
});
}
// Load Swagger UI with specified API spec
function loadSwaggerUI(apiSpec) {
SwaggerUIBundle({
url: apiSpec,
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
}
// Initialize
createServiceDropdown();
loadSwaggerUI(getServiceFromURL());
</script>
</body>
</html>
+94 -58
View File
@@ -4,6 +4,21 @@
name: Property Based Tests
on:
push:
branches:
- main
paths:
- ".github/workflows/api-tests.yaml"
- "api/**"
- "auth/api/http/**"
- "channels/api/http/**"
- "clients/api/http/**"
- "domains/api/http/**"
- "groups/api/http/**"
- "http/api/**"
- "journal/api/**"
- "users/api/**"
- "apidocs/openapi/**"
pull_request:
branches:
- main
@@ -18,6 +33,11 @@ on:
- "http/api/**"
- "journal/api/**"
- "users/api/**"
- "apidocs/openapi/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
TOKENS_URL: http://localhost:9002/users/tokens/issue
@@ -39,19 +59,80 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get Go version from go.mod
id: go-version
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: 1.25.x
go-version: ${{ steps.go-version.outputs.version }}
cache-dependency-path: "go.sum"
- name: Check for changes in specific paths
uses: dorny/paths-filter@v3
id: changes
with:
filters: |
workflow:
- ".github/workflows/api-tests.yaml"
journal:
- "apidocs/openapi/journal.yaml"
- "journal/api/**"
auth:
- "apidocs/openapi/auth.yaml"
- "auth/api/http/**"
domains:
- "apidocs/openapi/domains.yaml"
- "domains/api/http/**"
http:
- "apidocs/openapi/http.yaml"
- "http/api/**"
clients:
- "apidocs/openapi/clients.yaml"
- "clients/api/http/**"
channels:
- "apidocs/openapi/channels.yaml"
- "channels/api/http/**"
groups:
- "apidocs/openapi/groups.yaml"
- "groups/api/http/**"
users:
- "apidocs/openapi/users.yaml"
- "users/api/**"
- name: Build images
run: make all -j $(nproc) && make dockers_dev -j $(nproc)
- name: Start containers
run: make run up args="-d" && make run_addons up args="-d"
run: make run_latest up args="-d" && make run_addons up args="-d"
- name: Wait for services to be ready
run: |
echo "Waiting for services to start..."
sleep 15
# Check if services are responding
for i in {1..30}; do
if curl -f -s http://localhost:9002/health > /dev/null 2>&1; then
echo "Services are ready!"
break
fi
echo "Waiting for services... ($i/30)"
sleep 2
done
- name: Set access token
run: |
@@ -61,62 +142,17 @@ jobs:
export CLIENT_SECRET=$(supermq-cli provision test | /usr/bin/grep -Eo '"secret": "[^"]+"' | awk 'NR % 2 == 0' | sed 's/"secret": "\(.*\)"/\1/')
echo "CLIENT_SECRET=$CLIENT_SECRET" >> $GITHUB_ENV
- name: Check for changes in specific paths
uses: dorny/paths-filter@v3
id: changes
with:
filters: |
journal:
- ".github/workflows/api-tests.yaml"
- "apidocs/openapi/journal.yaml"
- "journal/api/**"
auth:
- ".github/workflows/api-tests.yaml"
- "apidocs/openapi/auth.yaml"
- "auth/api/http/**"
domains:
- ".github/workflows/api-tests.yaml"
- "apidocs/openapi/domains.yaml"
- "domains/api/http/**"
http:
- ".github/workflows/api-tests.yaml"
- "apidocs/openapi/http.yaml"
- "http/api/**"
clients:
- ".github/workflows/api-tests.yaml"
- "apidocs/openapi/clients.yaml"
- "clients/api/http/**"
channels:
- ".github/workflows/api-tests.yaml"
- "apidocs/openapi/channels.yaml"
- "channels/api/http/**"
groups:
- ".github/workflows/api-tests.yaml"
- "apidocs/openapi/groups.yaml"
- "groups/api/http/**"
users:
- ".github/workflows/api-tests.yaml"
- "apidocs/openapi/users.yaml"
- "users/api/**"
- name: Run Users API tests
if: steps.changes.outputs.users == 'true'
if: steps.changes.outputs.users == 'true' || steps.changes.outputs.workflow == 'true'
uses: schemathesis/action@v2.1.0
with:
schema: apidocs/openapi/users.yaml
base-url: ${{ env.USERS_URL }}
checks: all
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --exclude-operation-id=requestPasswordReset --phases=examples,stateful'
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --exclude-operation-id=requestPasswordReset --phases=examples'
- name: Run Groups API tests
if: steps.changes.outputs.groups == 'true'
if: steps.changes.outputs.groups == 'true' || steps.changes.outputs.workflow == 'true'
uses: schemathesis/action@v2.1.0
with:
schema: apidocs/openapi/groups.yaml
@@ -125,7 +161,7 @@ jobs:
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
- name: Run Clients API tests
if: steps.changes.outputs.clients == 'true'
if: steps.changes.outputs.clients == 'true' || steps.changes.outputs.workflow == 'true'
uses: schemathesis/action@v2.1.0
with:
schema: apidocs/openapi/clients.yaml
@@ -134,7 +170,7 @@ jobs:
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
- name: Run Channels API tests
if: steps.changes.outputs.channels == 'true'
if: steps.changes.outputs.channels == 'true' || steps.changes.outputs.workflow == 'true'
uses: schemathesis/action@v2.1.0
with:
schema: apidocs/openapi/channels.yaml
@@ -143,7 +179,7 @@ jobs:
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
- name: Run HTTP Adapter API tests
if: steps.changes.outputs.http == 'true'
if: steps.changes.outputs.http == 'true' || steps.changes.outputs.workflow == 'true'
uses: schemathesis/action@v2.1.0
with:
schema: apidocs/openapi/http.yaml
@@ -152,7 +188,7 @@ jobs:
args: '--header "Authorization: Client ${{ env.CLIENT_SECRET }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
- name: Run Auth API tests
if: steps.changes.outputs.auth == 'true'
if: steps.changes.outputs.auth == 'true' || steps.changes.outputs.workflow == 'true'
uses: schemathesis/action@v2.1.0
with:
schema: apidocs/openapi/auth.yaml
@@ -161,7 +197,7 @@ jobs:
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
- name: Run Domains API tests
if: steps.changes.outputs.domains == 'true'
if: steps.changes.outputs.domains == 'true' || steps.changes.outputs.workflow == 'true'
uses: schemathesis/action@v2.1.0
with:
schema: apidocs/openapi/domains.yaml
@@ -170,7 +206,7 @@ jobs:
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
- name: Run Journal API tests
if: steps.changes.outputs.journal == 'true'
if: steps.changes.outputs.journal == 'true' || steps.changes.outputs.workflow == 'true'
uses: schemathesis/action@v2.1.0
with:
schema: apidocs/openapi/journal.yaml
@@ -180,4 +216,4 @@ jobs:
- name: Stop containers
if: always()
run: make run down args="-v" && make run_addons down args="-v"
run: make run_latest down args="-v" && make run_addons down args="-v"
+24 -38
View File
@@ -7,45 +7,46 @@ on:
push:
branches:
- main
paths-ignore:
- '**.md'
- 'docs/**'
- '.github/workflows/**'
- 'LICENSE'
- 'MAINTAINERS'
- 'CODEOWNERS'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
lint-and-build:
uses: ./.github/workflows/lint-and-build.yaml
build-and-push:
name: Build and Push
name: Build and Push Docker Images
runs-on: ubuntu-latest
needs: [lint-and-build]
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
- name: Fetch tags for the build
run: |
git fetch --prune --unshallow --tags
- name: Get Go version from go.mod
id: go-version
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: 1.25.x
go-version: ${{ steps.go-version.outputs.version }}
cache-dependency-path: "go.sum"
- name: Set GOBIN
run: echo "GOBIN=$HOME/.local/bin" >> $GITHUB_ENV
- name: Add GOBIN to PATH
run: echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Run tests
run: |
make test
- name: Upload coverage
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV }}
files: ./coverage/*.out
verbose: true
- name: Set up Docker Build
uses: docker/setup-buildx-action@v3
@@ -56,22 +57,7 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Compile check for rabbitmq
run: |
SMQ_MESSAGE_BROKER_TYPE=msg_rabbitmq make mqtt
- name: Compile check for redis
run: |
SMQ_ES_TYPE=es_redis make mqtt
- name: Build and push Docker images
run: |
make latest -j $(nproc)
- name: Trigger Helm Chart Deployment
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: peter-evans/repository-dispatch@v4
with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: absmach/amdm
event-type: deploy-latest-smq-images
+21 -30
View File
@@ -4,9 +4,6 @@
name: Check the consistency of generated files
on:
push:
branches:
- main
pull_request:
branches:
- main
@@ -14,14 +11,22 @@ on:
jobs:
check-generated-files:
runs-on: ubuntu-latest
env:
PROTOC_VERSION: "33.0"
PROTOC_GEN_GO_VERSION: "v1.36.11"
PROTOC_GEN_GO_GRPC_VERSION: "v1.6.0"
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Get Go version from go.mod
id: go-version
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: 1.25.x
go-version: ${{ steps.go-version.outputs.version }}
cache-dependency-path: "go.sum"
- name: Set GOBIN
@@ -69,32 +74,18 @@ jobs:
- "journal/journal.go"
- "consumers/notifier.go"
- name: Set up protoc
- name: Install Protoc
if: steps.changes.outputs.proto == 'true'
uses: arduino/setup-protoc@v3
with:
version: ${{ env.PROTOC_VERSION }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Go Protobuf Plugins
if: steps.changes.outputs.proto == 'true'
run: |
PROTOC_VERSION=33.0
PROTOC_GEN_VERSION=v1.36.10
PROTOC_GRPC_VERSION=v1.5.1
# Export the variables so they are available in future steps
echo "PROTOC_VERSION=$PROTOC_VERSION" >> $GITHUB_ENV
echo "PROTOC_GEN_VERSION=$PROTOC_GEN_VERSION" >> $GITHUB_ENV
echo "PROTOC_GRPC_VERSION=$PROTOC_GRPC_VERSION" >> $GITHUB_ENV
# Download and install protoc
PROTOC_ZIP=protoc-$PROTOC_VERSION-linux-x86_64.zip
curl -0L -o $PROTOC_ZIP https://github.com/protocolbuffers/protobuf/releases/download/v$PROTOC_VERSION/$PROTOC_ZIP
unzip -o $PROTOC_ZIP -d protoc3
sudo mv protoc3/bin/* /usr/local/bin/
sudo mv protoc3/include/* /usr/local/include/
rm -rf $PROTOC_ZIP protoc3
# Install protoc-gen-go and protoc-gen-go-grpc
go install google.golang.org/protobuf/cmd/protoc-gen-go@$PROTOC_GEN_VERSION
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@$PROTOC_GRPC_VERSION
# Add protoc to the PATH
export PATH=$PATH:/usr/local/bin/protoc
go install google.golang.org/protobuf/cmd/protoc-gen-go@$PROTOC_GEN_GO_VERSION
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@$PROTOC_GEN_GO_GRPC_VERSION
- name: Check Protobuf is up to Date
if: steps.changes.outputs.proto == 'true'
@@ -110,7 +101,7 @@ jobs:
echo "Error: Proto file and generated Go file $p are out of sync!"
echo "Here is the difference:"
diff $p $p.tmp || true
echo "Please run 'make proto' with protoc version $PROTOC_VERSION, protoc-gen-go version $PROTOC_GEN_VERSION and protoc-gen-go-grpc version $PROTOC_GRPC_VERSION and commit the changes."
echo "Please run 'make proto' with protoc version $PROTOC_VERSION, protoc-gen-go version $PROTOC_GEN_GO_VERSION and protoc-gen-go-grpc version $PROTOC_GEN_GO_GRPC_VERSION and commit the changes."
exit 1
fi
done
+1 -4
View File
@@ -4,9 +4,6 @@
name: Check License Header
on:
push:
branches:
- main
pull_request:
branches:
- main
@@ -16,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Check License Header
run: |
+86
View File
@@ -0,0 +1,86 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
name: Lint and Build
on:
workflow_call:
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Get Go version from go.mod
id: go-version
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ steps.go-version.outputs.version }}
cache-dependency-path: "go.sum"
- name: Run linters
uses: golangci/golangci-lint-action@v9.2.0
with:
version: v2.10.1
args: --config ./tools/config/.golangci.yaml
build:
name: Build All Binaries
runs-on: ubuntu-latest
needs: lint
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Get Go version from go.mod
id: go-version
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ steps.go-version.outputs.version }}
cache-dependency-path: "go.sum"
- name: Build all binaries
run: |
make all -j $(nproc)
compile-check:
name: Compile Check ${{ matrix.variant.name }}
runs-on: ubuntu-latest
needs: lint
strategy:
fail-fast: true
matrix:
variant:
- name: rabbitmq
env: SMQ_MESSAGE_BROKER_TYPE=msg_rabbitmq
target: mqtt
- name: redis
env: SMQ_ES_TYPE=es_redis
target: mqtt
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Get Go version from go.mod
id: go-version
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ steps.go-version.outputs.version }}
cache-dependency-path: "go.sum"
- name: Compile check for ${{ matrix.variant.name }}
run: |
${{ matrix.variant.env }} make ${{ matrix.variant.target }}
+23 -9
View File
@@ -8,24 +8,38 @@ on:
branches:
- main
permissions:
contents: write
jobs:
swagger-ui:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Swagger UI action
id: swagger-ui-action
uses: blokovi/swagger-ui-action@main
with:
dir: "./apidocs/openapi"
pattern: "*.yaml"
debug: "true"
- name: Build Swagger UI
run: |
# Create output directory
mkdir -p swagger-ui
# Copy OpenAPI YAML files and schemas directory
cp apidocs/openapi/*.yaml swagger-ui/
cp -r apidocs/openapi/schemas swagger-ui/
# Get list of YAML files
cd apidocs/openapi
YAML_FILES=$(ls *.yaml | jq -R -s -c 'split("\n")[:-1]')
cd ../..
# Generate index.html from template
sed "s|APIS_PLACEHOLDER|$YAML_FILES|g" .github/swagger-ui-template.html > swagger-ui/index.html
echo "Generated Swagger UI with APIs: $YAML_FILES"
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: swagger-ui
publish_dir: ./swagger-ui
cname: docs.api.supermq.absmach.eu
+155 -141
View File
@@ -4,24 +4,30 @@
name: CI Pipeline
on:
push:
branches:
- main
paths-ignore:
- '**.md'
- 'docs/**'
- 'LICENSE'
- 'MAINTAINERS'
- 'CODEOWNERS'
pull_request:
branches:
- main
jobs:
lint-and-build: # Linting and building are combined to save time for setting up Go
name: Lint and Build
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check-certs:
name: Check Certs
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: 1.25.x
cache-dependency-path: "go.sum"
uses: actions/checkout@v6
- name: Fetch Certs
run: |
@@ -34,6 +40,23 @@ jobs:
exit 0
fi
lint-proto:
name: Lint Proto
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Get Go version from go.mod
id: go-version
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ steps.go-version.outputs.version }}
cache-dependency-path: "go.sum"
- name: Install protolint
run: |
go install github.com/yoheimuta/protolint/cmd/protolint@latest
@@ -42,46 +65,28 @@ jobs:
run: |
protolint .
- name: Run linters
uses: golangci/golangci-lint-action@v8
with:
version: v2.4.0
args: --config ./tools/config/.golangci.yaml
lint-and-build:
needs: [check-certs, lint-proto]
uses: ./.github/workflows/lint-and-build.yaml
- name: Build all Binaries
run: |
make all -j $(nproc)
- name: Compile check for rabbitmq
run: |
SMQ_MESSAGE_BROKER_TYPE=msg_rabbitmq make mqtt
- name: Compile check for redis
run: |
SMQ_ES_TYPE=es_redis make mqtt
run-tests:
name: Run tests
detect-changes:
name: Detect Changes
runs-on: ubuntu-latest
needs: lint-and-build
outputs:
modules: ${{ steps.set-matrix.outputs.modules }}
workflow_changed: ${{ steps.changes.outputs.workflow }}
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: 1.25.x
cache-dependency-path: "go.sum"
- name: Check for changes in specific paths
uses: dorny/paths-filter@v3
id: changes
with:
base: main
filters: |
workflow:
- ".github/workflows/tests.yaml"
@@ -235,131 +240,140 @@ jobs:
- "pkg/uuid/**"
- "pkg/events/**"
ws:
- "ws/**"
- "cmd/ws/**"
notifications:
- "notifications/**"
- "cmd/notifications/**"
- "auth.pb.go"
- "auth_grpc.pb.go"
- "clients/**"
- "pkg/messaging/**"
- "consumers/notifier.go"
- "pkg/events/**"
- name: Create coverage directory
run: |
mkdir coverage
api:
- "api/**"
- name: Run journal tests
if: steps.changes.outputs.journal == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/journal.out ./journal/...
consumers:
- "consumers/**"
- name: Run auth tests
if: steps.changes.outputs.auth == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/auth.out ./auth/...
readers:
- "readers/**"
- name: Run domains tests
if: steps.changes.outputs.domains == 'true' || steps.changes.outputs.workflow == 'true'
- name: Set matrix for changed modules
id: set-matrix
run: |
go test --race -v -count=1 -coverprofile=coverage/domains.out ./domains/...
modules=()
- name: Run cli tests
if: steps.changes.outputs.cli == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/cli.out ./cli/...
if [[ "${{ steps.changes.outputs.workflow }}" == "true" || "${{ steps.changes.outputs.pkg-errors }}" == "true" ]]; then
# If workflow or pkg/errors changed, test everything
modules=("auth" "channels" "cli" "clients" "coap" "domains" "groups" "http" "internal" "journal" "logger" "mqtt" "pkg-errors" "pkg-events" "pkg-grpcclient" "pkg-messaging" "pkg-sdk" "pkg-transformers" "pkg-ulid" "pkg-uuid" "users" "notifications" "api" "consumers" "readers")
else
# Add only changed modules
[[ "${{ steps.changes.outputs.auth }}" == "true" ]] && modules+=("auth")
[[ "${{ steps.changes.outputs.channels }}" == "true" ]] && modules+=("channels")
[[ "${{ steps.changes.outputs.cli }}" == "true" ]] && modules+=("cli")
[[ "${{ steps.changes.outputs.clients }}" == "true" ]] && modules+=("clients")
[[ "${{ steps.changes.outputs.coap }}" == "true" ]] && modules+=("coap")
[[ "${{ steps.changes.outputs.domains }}" == "true" ]] && modules+=("domains")
[[ "${{ steps.changes.outputs.groups }}" == "true" ]] && modules+=("groups")
[[ "${{ steps.changes.outputs.http }}" == "true" ]] && modules+=("http")
[[ "${{ steps.changes.outputs.internal }}" == "true" ]] && modules+=("internal")
[[ "${{ steps.changes.outputs.journal }}" == "true" ]] && modules+=("journal")
[[ "${{ steps.changes.outputs.logger }}" == "true" ]] && modules+=("logger")
[[ "${{ steps.changes.outputs.mqtt }}" == "true" ]] && modules+=("mqtt")
[[ "${{ steps.changes.outputs.pkg-errors }}" == "true" ]] && modules+=("pkg-errors")
[[ "${{ steps.changes.outputs.pkg-events }}" == "true" ]] && modules+=("pkg-events")
[[ "${{ steps.changes.outputs.pkg-grpcclient }}" == "true" ]] && modules+=("pkg-grpcclient")
[[ "${{ steps.changes.outputs.pkg-messaging }}" == "true" ]] && modules+=("pkg-messaging")
[[ "${{ steps.changes.outputs.pkg-sdk }}" == "true" ]] && modules+=("pkg-sdk")
[[ "${{ steps.changes.outputs.pkg-transformers }}" == "true" ]] && modules+=("pkg-transformers")
[[ "${{ steps.changes.outputs.pkg-ulid }}" == "true" ]] && modules+=("pkg-ulid")
[[ "${{ steps.changes.outputs.pkg-uuid }}" == "true" ]] && modules+=("pkg-uuid")
[[ "${{ steps.changes.outputs.users }}" == "true" ]] && modules+=("users")
[[ "${{ steps.changes.outputs.notifications }}" == "true" ]] && modules+=("notifications")
[[ "${{ steps.changes.outputs.api }}" == "true" ]] && modules+=("api")
[[ "${{ steps.changes.outputs.consumers }}" == "true" ]] && modules+=("consumers")
[[ "${{ steps.changes.outputs.readers }}" == "true" ]] && modules+=("readers")
fi
- name: Run CoAP tests
if: steps.changes.outputs.coap == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/coap.out ./coap/...
# Convert to JSON array
json_modules=$(printf '%s\n' "${modules[@]}" | jq -R . | jq -s -c .)
echo "modules=$json_modules" >> $GITHUB_OUTPUT
echo "Testing modules: $json_modules"
- name: Run HTTP tests
if: steps.changes.outputs.http == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/http.out ./http/...
run-tests:
name: Test ${{ matrix.module }}
runs-on: ubuntu-latest
needs: [lint-and-build, detect-changes]
if: needs.detect-changes.outputs.modules != '[]'
strategy:
fail-fast: true
max-parallel: 20
matrix:
module: ${{ fromJSON(needs.detect-changes.outputs.modules) }}
- name: Run internal tests
if: steps.changes.outputs.internal == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/internal.out ./internal/...
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Run logger tests
if: steps.changes.outputs.logger == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/logger.out ./logger/...
- name: Get Go version from go.mod
id: go-version
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
- name: Run MQTT tests
if: steps.changes.outputs.mqtt == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/mqtt.out ./mqtt/...
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ steps.go-version.outputs.version }}
cache-dependency-path: "go.sum"
- name: Run pkg errors tests
if: steps.changes.outputs.pkg-errors == 'true' || steps.changes.outputs.workflow == 'true'
- name: Verify dependencies
run: |
go test --race -v -count=1 -coverprofile=coverage/pkg-errors.out ./pkg/errors/...
go mod download
go mod verify
- name: Run pkg events tests
if: steps.changes.outputs.pkg-events == 'true' || steps.changes.outputs.workflow == 'true'
- name: Run ${{ matrix.module }} tests
run: |
go test --race -v -count=1 -coverprofile=coverage/pkg-events.out ./pkg/events/...
# Map module names to directories
case "${{ matrix.module }}" in
pkg-errors) dir="pkg/errors" ;;
pkg-events) dir="pkg/events" ;;
pkg-grpcclient) dir="pkg/grpcclient" ;;
pkg-messaging) dir="pkg/messaging" ;;
pkg-sdk) dir="pkg/sdk" ;;
pkg-transformers) dir="pkg/transformers" ;;
pkg-ulid) dir="pkg/ulid" ;;
pkg-uuid) dir="pkg/uuid" ;;
*) dir="${{ matrix.module }}" ;;
esac
- name: Run pkg grpcclient tests
if: steps.changes.outputs.pkg-grpcclient == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/pkg-grpcclient.out ./pkg/grpcclient/...
- name: Run pkg messaging tests
if: steps.changes.outputs.pkg-messaging == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/pkg-messaging.out ./pkg/messaging/...
- name: Run pkg sdk tests
if: steps.changes.outputs.pkg-sdk == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/pkg-sdk.out ./pkg/sdk/...
- name: Run pkg transformers tests
if: steps.changes.outputs.pkg-transformers == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/pkg-transformers.out ./pkg/transformers/...
- name: Run pkg ulid tests
if: steps.changes.outputs.pkg-ulid == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/pkg-ulid.out ./pkg/ulid/...
- name: Run pkg uuid tests
if: steps.changes.outputs.pkg-uuid == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/pkg-uuid.out ./pkg/uuid/...
- name: Run clients tests
if: steps.changes.outputs.clients == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/clients.out ./clients/...
- name: Run channels tests
if: steps.changes.outputs.channels == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/channels.out ./channels/...
- name: Run users tests
if: steps.changes.outputs.users == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/users.out ./users/...
- name: Run groups tests
if: steps.changes.outputs.groups == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/groups.out ./groups/...
- name: Run WebSocket tests
if: steps.changes.outputs.ws == 'true' || steps.changes.outputs.workflow == 'true'
run: |
go test --race -v -count=1 -coverprofile=coverage/ws.out ./ws/...
go test -mod=readonly --race -v -count=1 -failfast -coverprofile=coverage-${{ matrix.module }}.out ./$dir/...
- name: Upload coverage
uses: actions/upload-artifact@v7
with:
name: coverage-${{ matrix.module }}
path: coverage-${{ matrix.module }}.out
retention-days: 1
upload-coverage:
name: Upload Coverage
runs-on: ubuntu-latest
needs: run-tests
if: always() && needs.run-tests.result != 'cancelled'
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Download all coverage artifacts
uses: actions/download-artifact@v8
with:
pattern: coverage-*
path: coverage
merge-multiple: true
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV }}
directory: ./coverage
files: ./coverage/*.out
fail_ci_if_error: false
verbose: true
+1 -1
View File
@@ -5,7 +5,7 @@ hosted on the [Abstract Machines Organization](https://github.com/absmach) on Gi
This project adheres to the [Contributor Covenant 1.2](http://contributor-covenant.org/version/1/2/0).
By participating, you are expected to uphold this code. Please report unacceptable behavior to
[abuse@abstractmachines.fr](mailto:abuse@abstractmachines.fr).
[abuse@absmach.eu](mailto:abuse@absmach.eu).
## Reporting issues
+1 -1
View File
@@ -176,7 +176,7 @@
END OF TERMS AND CONDITIONS
Copyright 2015-2020 SuperMQ
Copyright 2015-2026 SuperMQ
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+11 -26
View File
@@ -1,30 +1,15 @@
# SuperMQ follows the timeless, highly efficient and totally unfair system
# known as [Benevolent dictator for
# life](https://en.wikipedia.org/wiki/Benevolent_Dictator_for_Life), with
# Drasko DRASKOVIC in the role of BDFL.
# SuperMQ Maintainers
[bdfl]
[[drasko]]
Name = "Drasko Draskovic"
Email = "drasko@abstractmachines.fr"
GitHub = "drasko"
SuperMQ follows a BDFL model for dead-lock situations; day-to-day decisions happen through discussion and pull requests.
# However, this role serves only in dead-lock events, or in a special and very rare cases
# when BDFL completely disagrees with the decisions made.
# In the normal flow of events, decisions on the project design are made through discussions,
# most often on the Pull Requests.
#
# Maintainers have the special role in the project in managing and accepting PRs,
# overall leading the project and making design decisions on the maintained subsystems.
#
# A reference list of all maintainers of the SuperMQ project.
## BDFL
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
- **Drasko Draskovic** — [drasko@absmach.eu](mailto:drasko@absmach.eu) — [GitHub: drasko](https://github.com/drasko)
[maintainers]
[[dusan]]
Name = "Dusan Borovcanin"
Email = "dusan.borovcanin@abstractmachines.fr"
GitHub = "dborovcanin"
## Maintainers
Add yourself below in alphabetical order.
| Name | Email | GitHub |
| --- | --- | --- |
| Dusan Borovcanin | [dusan.borovcanin@absmach.eu](mailto:dusan.borovcanin@absmach.eu) | [dborovcanin](https://github.com/dborovcanin) |
+70 -14
View File
@@ -3,13 +3,15 @@
SMQ_DOCKER_IMAGE_NAME_PREFIX ?= supermq
BUILD_DIR ?= build
SERVICES = auth users clients groups channels domains http coap ws cli mqtt journal
SERVICES = auth users clients groups channels domains http coap cli mqtt journal notifications
TEST_API_SERVICES = journal auth certs http clients users channels groups domains
TEST_API = $(addprefix test_api_,$(TEST_API_SERVICES))
DOCKERS = $(addprefix docker_,$(SERVICES))
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
CGO_ENABLED ?= 0
GOARCH ?= amd64
GOOS ?= linux
DETECTED_ARCH := $(shell uname -m)
VERSION ?= $(shell git describe --abbrev=0 --tags 2>/dev/null || echo 'unknown')
COMMIT ?= $(shell git rev-parse HEAD)
TIME ?= $(shell date +%F_%T)
@@ -22,7 +24,7 @@ DOCKER_COMPOSE_COMMANDS_SUPPORTED := up down config restart
DEFAULT_DOCKER_COMPOSE_COMMAND := up
GRPC_MTLS_CERT_FILES_EXISTS = 0
MOCKERY = $(GOBIN)/mockery
MOCKERY_VERSION=3.5.5
MOCKERY_VERSION=3.6.4
PKG_PROTO_GEN_OUT_DIR=api/grpc
INTERNAL_PROTO_DIR=internal/proto
INTERNAL_PROTO_FILES := $(shell find $(INTERNAL_PROTO_DIR) -name "*.proto" | sed 's|$(INTERNAL_PROTO_DIR)/||')
@@ -73,10 +75,50 @@ define make_docker_dev
-f docker/Dockerfile.dev ./build
endef
define run_with_arch_detection
@echo "Detecting architecture..."
@if [ "$(DETECTED_ARCH)" = "arm64" ] || [ "$(DETECTED_ARCH)" = "aarch64" ]; then \
echo "ARM64 architecture detected."; \
git checkout $(1); \
GOARCH=arm64 $(MAKE) dockers; \
for svc in $(SERVICES); do \
docker tag supermq/$$svc supermq/$$svc:latest; \
docker tag supermq/$$svc docker.io/supermq/$$svc:latest; \
done; \
sed -i.bak 's/^SMQ_RELEASE_TAG=.*/SMQ_RELEASE_TAG=latest/' docker/.env && rm -f docker/.env.bak; \
docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args); \
else \
echo "x86_64 architecture detected."; \
git checkout $(1); \
sed -i.bak 's/^SMQ_RELEASE_TAG=.*/SMQ_RELEASE_TAG=$(2)/' docker/.env && rm -f docker/.env.bak; \
docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args); \
fi
endef
ADDON_SERVICES = journal certs
EXTERNAL_SERVICES = prometheus
# Detect OS and architecture for cross-platform compatibility
UNAME_S := $(shell uname -s)
UNAME_M := $(shell uname -m)
# macOS BSD sed vs GNU sed compatibility
ifeq ($(UNAME_S),Darwin)
SED_INPLACE := sed -i ''
else
SED_INPLACE := sed -i
endif
# Apple Silicon (arm64) Docker platform compatibility
# Pre-built images are amd64 only, so we need to use emulation on Apple Silicon
ifeq ($(UNAME_S),Darwin)
ifeq ($(UNAME_M),arm64)
DOCKER_PLATFORM := DOCKER_DEFAULT_PLATFORM=linux/amd64
endif
endif
DOCKER_PLATFORM ?=
ifneq ($(filter run%,$(firstword $(MAKECMDGOALS))),)
temp_args := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
DOCKER_COMPOSE_COMMAND := $(if $(filter $(DOCKER_COMPOSE_COMMANDS_SUPPORTED),$(temp_args)), $(filter $(DOCKER_COMPOSE_COMMANDS_SUPPORTED),$(temp_args)), $(DEFAULT_DOCKER_COMPOSE_COMMAND))
@@ -99,7 +141,7 @@ FILTERED_SERVICES = $(filter-out $(RUN_ADDON_ARGS), $(SERVICES))
all: $(SERVICES)
.PHONY: all $(SERVICES) dockers dockers_dev latest release run run_addons grpc_mtls_certs check_mtls check_certs test_api mocks
.PHONY: all $(SERVICES) dockers dockers_dev latest release run_latest run_stable run_addons grpc_mtls_certs check_mtls check_certs test_api mocks
clean:
rm -rf ${BUILD_DIR}
@@ -123,11 +165,8 @@ mocks: $(MOCKERY)
$(MOCKERY):
@mkdir -p $(GOBIN)
@mkdir -p mockery
@echo ">> downloading mockery $(MOCKERY_VERSION)..."
@curl -sL https://github.com/vektra/mockery/releases/download/v$(MOCKERY_VERSION)/mockery_$(MOCKERY_VERSION)_Linux_x86_64.tar.gz | tar -xz -C mockery
@mv mockery/mockery $(GOBIN)
@rm -r mockery
@echo ">> installing mockery $(MOCKERY_VERSION)..."
@go install github.com/vektra/mockery/v3@v$(MOCKERY_VERSION)
DIRS = consumers readers postgres internal
test: mocks
@@ -215,6 +254,15 @@ changelog:
latest: dockers
$(call docker_push,latest)
publish_arch:
$(MAKE) dockers GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
for svc in $(SERVICES); do \
docker tag $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(VERSION)-$(GOARCH); \
docker tag $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc:latest-$(GOARCH); \
docker push $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(VERSION)-$(GOARCH); \
docker push $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc:latest-$(GOARCH); \
done
release:
$(eval version = $(shell git describe --abbrev=0 --tags))
git checkout $(version)
@@ -258,19 +306,27 @@ endif
fetch_certs:
@./scripts/certs.sh
run: check_certs
docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
run_latest: check_certs
git checkout main
$(SED_INPLACE) 's/^SMQ_RELEASE_TAG=.*/SMQ_RELEASE_TAG=latest/' docker/.env
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
run_stable: check_certs
$(eval version = $(shell git describe --abbrev=0 --tags))
git checkout $(version)
$(SED_INPLACE) 's/^SMQ_RELEASE_TAG=.*/SMQ_RELEASE_TAG=$(version)/' docker/.env
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
run_addons: check_certs
$(foreach SVC,$(RUN_ADDON_ARGS),$(if $(filter $(SVC),$(ADDON_SERVICES) $(EXTERNAL_SERVICES)),,$(error Invalid Service $(SVC))))
@docker compose -f docker/docker-compose.yaml --env-file ./docker/.env -p $(DOCKER_PROJECT) up -d auth domains jaeger
@$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file ./docker/.env -p $(DOCKER_PROJECT) up -d auth domains jaeger
@for SVC in $(RUN_ADDON_ARGS); do \
if [ "$$SVC" = "certs" ]; then \
docker compose -f docker/addons/$$SVC/docker-compose.yaml -f docker/certs-docker-compose-override.yaml --env-file ./docker/.env --env-file ./docker/addons/$$SVC/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args) & \
$(DOCKER_PLATFORM) docker compose -f docker/addons/$$SVC/docker-compose.yaml -f docker/certs-docker-compose-override.yaml --env-file ./docker/.env --env-file ./docker/addons/$$SVC/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args) & \
else \
SMQ_ADDONS_CERTS_PATH_PREFIX="../." docker compose -f docker/addons/$$SVC/docker-compose.yaml -p $(DOCKER_PROJECT) --env-file ./docker/.env $(DOCKER_COMPOSE_COMMAND) $(args) & \
SMQ_ADDONS_CERTS_PATH_PREFIX="../." $(DOCKER_PLATFORM) docker compose -f docker/addons/$$SVC/docker-compose.yaml -p $(DOCKER_PROJECT) --env-file ./docker/.env $(DOCKER_COMPOSE_COMMAND) $(args) & \
fi; \
done
run_live: check_certs
GOPATH=$(go env GOPATH) docker compose -f docker/docker-compose.yaml -f docker/docker-compose-live.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
GOPATH=$(go env GOPATH) $(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml -f docker/docker-compose-live.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
+64 -29
View File
@@ -1,25 +1,24 @@
<div align="center">
# SuperMQ
# SuperMQ
**Planetary event-driven infrastructure**
### Planetary event-driven infrastructure
**Made with ❤️ by [Abstract Machines](https://abstractmachines.fr/)**
**Made with ❤️ by [Abstract Machines](https://absmach.eu/)**
[![Build Status](https://github.com/absmach/supermq/actions/workflows/build.yaml/badge.svg?branch=main)](https://github.com/absmach/supermq/actions/workflows/build.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/absmach/supermq)](https://goreportcard.com/report/github.com/absmach/supermq)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/absmach/supermq)
[![Check License Header](https://github.com/absmach/supermq/actions/workflows/check-license.yaml/badge.svg?branch=main)](https://github.com/absmach/supermq/actions/workflows/check-license.yaml)
[![Check Generated Files](https://github.com/absmach/supermq/actions/workflows/check-generated-files.yaml/badge.svg?branch=main)](https://github.com/absmach/supermq/actions/workflows/check-generated-files.yaml)
[![Coverage](https://codecov.io/gh/absmach/supermq/graph/badge.svg?token=nPCEr5nW8S)](https://codecov.io/gh/absmach/supermq)
[![License](https://img.shields.io/badge/license-Apache%20v2.0-blue.svg)](LICENSE)
[![Matrix](https://img.shields.io/matrix/supermq%3Amatrix.org?label=Chat&style=flat&logo=matrix&logoColor=white)](https://matrix.to/#/#supermq:matrix.org)
[![Build Status](https://github.com/absmach/supermq/actions/workflows/build.yaml/badge.svg?branch=main)](https://github.com/absmach/supermq/actions/workflows/build.yaml)
[![Check License Header](https://github.com/absmach/supermq/actions/workflows/check-license.yaml/badge.svg?branch=main)](https://github.com/absmach/supermq/actions/workflows/check-license.yaml)
[![Check Generated Files](https://github.com/absmach/supermq/actions/workflows/check-generated-files.yaml/badge.svg?branch=main)](https://github.com/absmach/supermq/actions/workflows/check-generated-files.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/absmach/supermq)](https://goreportcard.com/report/github.com/absmach/supermq)
[![Coverage](https://codecov.io/gh/absmach/supermq/graph/badge.svg?token=nPCEr5nW8S)](https://codecov.io/gh/absmach/supermq)
[![License](https://img.shields.io/badge/license-Apache%20v2.0-blue.svg)](LICENSE)
[![Matrix](https://img.shields.io/matrix/supermq%3Amatrix.org?label=Chat&style=flat&logo=matrix&logoColor=white)](https://matrix.to/#/#supermq:matrix.org)
### [Guide](https://docs.supermq.abstractmachines.fr) | [Contributing](CONTRIBUTING.md) | [Website](https://abstractmachines.fr/) | [Chat](https://matrix.to/#/#supermq:matrix.org)
### [Guide](https://docs.supermq.absmach.eu) | [Contributing](CONTRIBUTING.md) | [Website](https://absmach.eu/) | [Chat](https://matrix.to/#/#supermq:matrix.org)
</div>
## Introduction 📖
SuperMQ is a distributed, highly scalable, and secure open-source cloud platform for messaging and event-driven architecture (EDA). It is a planetarily distributed, highly scalable, and secure platform that serves as a robust foundation for building advanced real-time and reactive systems.
@@ -46,25 +45,62 @@ SuperMQ bridges the gap between various network protocols (HTTP, MQTT, WebSocket
## Installation 🛠️
Clone the repository and start SuperMQ services:
There are multiple ways to run SuperMQ.
First, clone the repository and position to it:
```bash
git clone https://github.com/absmach/supermq.git
cd supermq
```
To run the latest stable (tagged) version, use:
```bash
# Run with latest stable tagged version
make run_stable
```
To run the latest version, use:
```bash
# Run with latest development version (from main branch)
make run_latest
```
The `make run_stable` command will:
- Checkout the repository to the latest git tag
- Update the version in the environment configuration
- Start the services with the stable release
**Note:** After running `make run_stable`, you'll be on a detached HEAD state. To return to your working branch:
```bash
git checkout main
```
### Running on Apple Silicon (M1/M2/M3) Macs
When running SuperMQ on Apple Silicon Macs, the Makefile will automatically detect your ARM64 architecture and build Docker images locally.
**If using Docker Desktop:**
1. **Enable Apple Virtualization Framework**: In Docker Desktop, go to:
- Settings → General → Enable "Use the new Virtualization framework"
2. **Enable Rosetta for x86_64 Emulation**: In Docker Desktop, go to:
- Settings → General → Enable "Use Rosetta for x86_64/amd64 emulation on Apple Silicon"
After enabling these options, restart Docker Desktop, then run `make run_stable` or `make run_latest` as usual.
To manually run SuperMQ, clone the repository and start all core services:
```bash
docker compose -f docker/docker-compose.yaml --env-file docker/.env up
```
Or use the [Makefile](Makefile) for a simpler command:
```bash
make run
```
For production deployments, check our [Kubernetes guide](https://docs.supermq.abstractmachines.fr/kubernetes). ⚙️
### Usage 📤📥
#### Using the CLI:
**Using the CLI :**
```bash
make cli
@@ -73,7 +109,7 @@ make cli
This command retrieves the status of the SuperMQ server and outputs it to the console.
#### Using HTTP with Curl:
**Using HTTP with Curl :**
```bash
curl -X GET http://localhost:8080/status
@@ -81,11 +117,11 @@ curl -X GET http://localhost:8080/status
This request fetches the server status over HTTP and provides a JSON response.
See our [CLI documentation](https://docs.supermq.abstractmachines.fr/cli) for more details.
See our [CLI documentation](https://docs.supermq.absmach.eu/cli) for more details.
## Documentation 📚
The official documentation is hosted at [SuperMQ docs page](https://docs.supermq.abstractmachines.fr).
The official documentation is hosted at [SuperMQ docs page](https://docs.supermq.absmach.eu/).
Documentation is auto-generated, check out the instructions in the [docs repository](https://github.com/absmach/supermq-docs).
If you spot an error or a need for corrections, please let us know - or even better: send us a PR! 💌
@@ -104,7 +140,7 @@ Join our community:
## Professional Support 💼
Need help deploying SuperMQ or integrating it into your system? Reach out to **[Abstract Machines](https://abstractmachines.fr/)** for professional support and guidance.
Need help deploying SuperMQ or integrating it into your system? Reach out to **[Abstract Machines](https://absmach.eu/)** for professional support and guidance.
## License 📜
@@ -115,4 +151,3 @@ SuperMQ is open-source software licensed under the [Apache License 2.0](LICENSE)
Special thanks to the amazing contributors who make SuperMQ possible. Check out the [MAINTAINERS](MAINTAINERS) file to see the team behind the magic.
Ready to build the future of messaging and event-driven systems? Let's get started! 🚀
+252 -196
View File
@@ -3,7 +3,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc-gen-go v1.36.11
// protoc v6.33.0
// source: auth/v1/auth.proto
@@ -70,10 +70,10 @@ func (x *AuthNReq) GetToken() string {
type AuthNRes struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // token id
UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // user id
UserRole uint32 `protobuf:"varint,3,opt,name=user_role,json=userRole,proto3" json:"user_role,omitempty"` // user role
Verified bool `protobuf:"varint,4,opt,name=verified,proto3" json:"verified,omitempty"` // verified user
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
UserRole uint32 `protobuf:"varint,3,opt,name=user_role,json=userRole,proto3" json:"user_role,omitempty"`
Verified bool `protobuf:"varint,4,opt,name=verified,proto3" json:"verified,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -136,24 +136,209 @@ func (x *AuthNRes) GetVerified() bool {
return false
}
type AuthZReq struct {
type PolicyReq struct {
state protoimpl.MessageState `protogen:"open.v1"`
Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` // Domain
SubjectType string `protobuf:"bytes,2,opt,name=subject_type,json=subjectType,proto3" json:"subject_type,omitempty"` // Client or User
SubjectKind string `protobuf:"bytes,3,opt,name=subject_kind,json=subjectKind,proto3" json:"subject_kind,omitempty"` // ID or Token
SubjectRelation string `protobuf:"bytes,4,opt,name=subject_relation,json=subjectRelation,proto3" json:"subject_relation,omitempty"` // Subject relation
Subject string `protobuf:"bytes,5,opt,name=subject,proto3" json:"subject,omitempty"` // Subject value (id or token, depending on kind)
Relation string `protobuf:"bytes,6,opt,name=relation,proto3" json:"relation,omitempty"` // Relation to filter
Permission string `protobuf:"bytes,7,opt,name=permission,proto3" json:"permission,omitempty"` // Action
Object string `protobuf:"bytes,8,opt,name=object,proto3" json:"object,omitempty"` // Object ID
ObjectType string `protobuf:"bytes,9,opt,name=object_type,json=objectType,proto3" json:"object_type,omitempty"` // Client, User, Group
Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"`
SubjectType string `protobuf:"bytes,2,opt,name=subject_type,json=subjectType,proto3" json:"subject_type,omitempty"`
SubjectKind string `protobuf:"bytes,3,opt,name=subject_kind,json=subjectKind,proto3" json:"subject_kind,omitempty"`
SubjectRelation string `protobuf:"bytes,4,opt,name=subject_relation,json=subjectRelation,proto3" json:"subject_relation,omitempty"`
Subject string `protobuf:"bytes,5,opt,name=subject,proto3" json:"subject,omitempty"`
Relation string `protobuf:"bytes,6,opt,name=relation,proto3" json:"relation,omitempty"`
Permission string `protobuf:"bytes,7,opt,name=permission,proto3" json:"permission,omitempty"`
Object string `protobuf:"bytes,8,opt,name=object,proto3" json:"object,omitempty"`
ObjectType string `protobuf:"bytes,9,opt,name=object_type,json=objectType,proto3" json:"object_type,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *PolicyReq) Reset() {
*x = PolicyReq{}
mi := &file_auth_v1_auth_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *PolicyReq) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PolicyReq) ProtoMessage() {}
func (x *PolicyReq) ProtoReflect() protoreflect.Message {
mi := &file_auth_v1_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 PolicyReq.ProtoReflect.Descriptor instead.
func (*PolicyReq) Descriptor() ([]byte, []int) {
return file_auth_v1_auth_proto_rawDescGZIP(), []int{2}
}
func (x *PolicyReq) GetDomain() string {
if x != nil {
return x.Domain
}
return ""
}
func (x *PolicyReq) GetSubjectType() string {
if x != nil {
return x.SubjectType
}
return ""
}
func (x *PolicyReq) GetSubjectKind() string {
if x != nil {
return x.SubjectKind
}
return ""
}
func (x *PolicyReq) GetSubjectRelation() string {
if x != nil {
return x.SubjectRelation
}
return ""
}
func (x *PolicyReq) GetSubject() string {
if x != nil {
return x.Subject
}
return ""
}
func (x *PolicyReq) GetRelation() string {
if x != nil {
return x.Relation
}
return ""
}
func (x *PolicyReq) GetPermission() string {
if x != nil {
return x.Permission
}
return ""
}
func (x *PolicyReq) GetObject() string {
if x != nil {
return x.Object
}
return ""
}
func (x *PolicyReq) GetObjectType() string {
if x != nil {
return x.ObjectType
}
return ""
}
type PATReq struct {
state protoimpl.MessageState `protogen:"open.v1"`
PatId string `protobuf:"bytes,1,opt,name=pat_id,json=patId,proto3" json:"pat_id,omitempty"`
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
Operation string `protobuf:"bytes,3,opt,name=operation,proto3" json:"operation,omitempty"`
UserId string `protobuf:"bytes,4,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
EntityId string `protobuf:"bytes,5,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"`
EntityType string `protobuf:"bytes,6,opt,name=entity_type,json=entityType,proto3" json:"entity_type,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *PATReq) Reset() {
*x = PATReq{}
mi := &file_auth_v1_auth_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *PATReq) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PATReq) ProtoMessage() {}
func (x *PATReq) ProtoReflect() protoreflect.Message {
mi := &file_auth_v1_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 PATReq.ProtoReflect.Descriptor instead.
func (*PATReq) Descriptor() ([]byte, []int) {
return file_auth_v1_auth_proto_rawDescGZIP(), []int{3}
}
func (x *PATReq) GetPatId() string {
if x != nil {
return x.PatId
}
return ""
}
func (x *PATReq) GetDomain() string {
if x != nil {
return x.Domain
}
return ""
}
func (x *PATReq) GetOperation() string {
if x != nil {
return x.Operation
}
return ""
}
func (x *PATReq) GetUserId() string {
if x != nil {
return x.UserId
}
return ""
}
func (x *PATReq) GetEntityId() string {
if x != nil {
return x.EntityId
}
return ""
}
func (x *PATReq) GetEntityType() string {
if x != nil {
return x.EntityType
}
return ""
}
type AuthZReq struct {
state protoimpl.MessageState `protogen:"open.v1"`
PolicyReq *PolicyReq `protobuf:"bytes,1,opt,name=policy_req,json=policyReq,proto3" json:"policy_req,omitempty"`
PatReq *PATReq `protobuf:"bytes,2,opt,name=pat_req,json=patReq,proto3,oneof" json:"pat_req,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AuthZReq) Reset() {
*x = AuthZReq{}
mi := &file_auth_v1_auth_proto_msgTypes[2]
mi := &file_auth_v1_auth_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -165,7 +350,7 @@ func (x *AuthZReq) String() string {
func (*AuthZReq) ProtoMessage() {}
func (x *AuthZReq) ProtoReflect() protoreflect.Message {
mi := &file_auth_v1_auth_proto_msgTypes[2]
mi := &file_auth_v1_auth_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -178,154 +363,21 @@ func (x *AuthZReq) ProtoReflect() protoreflect.Message {
// Deprecated: Use AuthZReq.ProtoReflect.Descriptor instead.
func (*AuthZReq) Descriptor() ([]byte, []int) {
return file_auth_v1_auth_proto_rawDescGZIP(), []int{2}
return file_auth_v1_auth_proto_rawDescGZIP(), []int{4}
}
func (x *AuthZReq) GetDomain() string {
func (x *AuthZReq) GetPolicyReq() *PolicyReq {
if x != nil {
return x.Domain
return x.PolicyReq
}
return ""
return nil
}
func (x *AuthZReq) GetSubjectType() string {
func (x *AuthZReq) GetPatReq() *PATReq {
if x != nil {
return x.SubjectType
return x.PatReq
}
return ""
}
func (x *AuthZReq) GetSubjectKind() string {
if x != nil {
return x.SubjectKind
}
return ""
}
func (x *AuthZReq) GetSubjectRelation() string {
if x != nil {
return x.SubjectRelation
}
return ""
}
func (x *AuthZReq) GetSubject() string {
if x != nil {
return x.Subject
}
return ""
}
func (x *AuthZReq) GetRelation() string {
if x != nil {
return x.Relation
}
return ""
}
func (x *AuthZReq) GetPermission() string {
if x != nil {
return x.Permission
}
return ""
}
func (x *AuthZReq) GetObject() string {
if x != nil {
return x.Object
}
return ""
}
func (x *AuthZReq) GetObjectType() string {
if x != nil {
return x.ObjectType
}
return ""
}
type AuthZPatReq struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // User id
PatId string `protobuf:"bytes,2,opt,name=pat_id,json=patId,proto3" json:"pat_id,omitempty"` // Pat id
EntityType uint32 `protobuf:"varint,3,opt,name=entity_type,json=entityType,proto3" json:"entity_type,omitempty"` // Entity type
OptionalDomainId string `protobuf:"bytes,4,opt,name=optional_domain_id,json=optionalDomainId,proto3" json:"optional_domain_id,omitempty"` // Optional domain id
Operation uint32 `protobuf:"varint,6,opt,name=operation,proto3" json:"operation,omitempty"` // Operation
EntityId string `protobuf:"bytes,7,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` // EntityID
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AuthZPatReq) Reset() {
*x = AuthZPatReq{}
mi := &file_auth_v1_auth_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AuthZPatReq) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AuthZPatReq) ProtoMessage() {}
func (x *AuthZPatReq) ProtoReflect() protoreflect.Message {
mi := &file_auth_v1_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 AuthZPatReq.ProtoReflect.Descriptor instead.
func (*AuthZPatReq) Descriptor() ([]byte, []int) {
return file_auth_v1_auth_proto_rawDescGZIP(), []int{3}
}
func (x *AuthZPatReq) GetUserId() string {
if x != nil {
return x.UserId
}
return ""
}
func (x *AuthZPatReq) GetPatId() string {
if x != nil {
return x.PatId
}
return ""
}
func (x *AuthZPatReq) GetEntityType() uint32 {
if x != nil {
return x.EntityType
}
return 0
}
func (x *AuthZPatReq) GetOptionalDomainId() string {
if x != nil {
return x.OptionalDomainId
}
return ""
}
func (x *AuthZPatReq) GetOperation() uint32 {
if x != nil {
return x.Operation
}
return 0
}
func (x *AuthZPatReq) GetEntityId() string {
if x != nil {
return x.EntityId
}
return ""
return nil
}
type AuthZRes struct {
@@ -338,7 +390,7 @@ type AuthZRes struct {
func (x *AuthZRes) Reset() {
*x = AuthZRes{}
mi := &file_auth_v1_auth_proto_msgTypes[4]
mi := &file_auth_v1_auth_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -350,7 +402,7 @@ func (x *AuthZRes) String() string {
func (*AuthZRes) ProtoMessage() {}
func (x *AuthZRes) ProtoReflect() protoreflect.Message {
mi := &file_auth_v1_auth_proto_msgTypes[4]
mi := &file_auth_v1_auth_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -363,7 +415,7 @@ func (x *AuthZRes) ProtoReflect() protoreflect.Message {
// Deprecated: Use AuthZRes.ProtoReflect.Descriptor instead.
func (*AuthZRes) Descriptor() ([]byte, []int) {
return file_auth_v1_auth_proto_rawDescGZIP(), []int{4}
return file_auth_v1_auth_proto_rawDescGZIP(), []int{5}
}
func (x *AuthZRes) GetAuthorized() bool {
@@ -391,8 +443,8 @@ const file_auth_v1_auth_proto_rawDesc = "" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x17\n" +
"\auser_id\x18\x02 \x01(\tR\x06userId\x12\x1b\n" +
"\tuser_role\x18\x03 \x01(\rR\buserRole\x12\x1a\n" +
"\bverified\x18\x04 \x01(\bR\bverified\"\xa2\x02\n" +
"\bAuthZReq\x12\x16\n" +
"\bverified\x18\x04 \x01(\bR\bverified\"\xa3\x02\n" +
"\tPolicyReq\x12\x16\n" +
"\x06domain\x18\x01 \x01(\tR\x06domain\x12!\n" +
"\fsubject_type\x18\x02 \x01(\tR\vsubjectType\x12!\n" +
"\fsubject_kind\x18\x03 \x01(\tR\vsubjectKind\x12)\n" +
@@ -404,25 +456,29 @@ const file_auth_v1_auth_proto_rawDesc = "" +
"permission\x12\x16\n" +
"\x06object\x18\b \x01(\tR\x06object\x12\x1f\n" +
"\vobject_type\x18\t \x01(\tR\n" +
"objectType\"\xc7\x01\n" +
"\vAuthZPatReq\x12\x17\n" +
"\auser_id\x18\x01 \x01(\tR\x06userId\x12\x15\n" +
"\x06pat_id\x18\x02 \x01(\tR\x05patId\x12\x1f\n" +
"\ventity_type\x18\x03 \x01(\rR\n" +
"entityType\x12,\n" +
"\x12optional_domain_id\x18\x04 \x01(\tR\x10optionalDomainId\x12\x1c\n" +
"\toperation\x18\x06 \x01(\rR\toperation\x12\x1b\n" +
"\tentity_id\x18\a \x01(\tR\bentityId\":\n" +
"objectType\"\xac\x01\n" +
"\x06PATReq\x12\x15\n" +
"\x06pat_id\x18\x01 \x01(\tR\x05patId\x12\x16\n" +
"\x06domain\x18\x02 \x01(\tR\x06domain\x12\x1c\n" +
"\toperation\x18\x03 \x01(\tR\toperation\x12\x17\n" +
"\auser_id\x18\x04 \x01(\tR\x06userId\x12\x1b\n" +
"\tentity_id\x18\x05 \x01(\tR\bentityId\x12\x1f\n" +
"\ventity_type\x18\x06 \x01(\tR\n" +
"entityType\"x\n" +
"\bAuthZReq\x121\n" +
"\n" +
"policy_req\x18\x01 \x01(\v2\x12.auth.v1.PolicyReqR\tpolicyReq\x12-\n" +
"\apat_req\x18\x02 \x01(\v2\x0f.auth.v1.PATReqH\x00R\x06patReq\x88\x01\x01B\n" +
"\n" +
"\b_pat_req\":\n" +
"\bAuthZRes\x12\x1e\n" +
"\n" +
"authorized\x18\x01 \x01(\bR\n" +
"authorized\x12\x0e\n" +
"\x02id\x18\x02 \x01(\tR\x02id2\xf0\x01\n" +
"\x02id\x18\x02 \x01(\tR\x02id2z\n" +
"\vAuthService\x123\n" +
"\tAuthorize\x12\x11.auth.v1.AuthZReq\x1a\x11.auth.v1.AuthZRes\"\x00\x129\n" +
"\fAuthorizePAT\x12\x14.auth.v1.AuthZPatReq\x1a\x11.auth.v1.AuthZRes\"\x00\x126\n" +
"\fAuthenticate\x12\x11.auth.v1.AuthNReq\x1a\x11.auth.v1.AuthNRes\"\x00\x129\n" +
"\x0fAuthenticatePAT\x12\x11.auth.v1.AuthNReq\x1a\x11.auth.v1.AuthNRes\"\x00B-Z+github.com/absmach/supermq/api/grpc/auth/v1b\x06proto3"
"\tAuthorize\x12\x11.auth.v1.AuthZReq\x1a\x11.auth.v1.AuthZRes\"\x00\x126\n" +
"\fAuthenticate\x12\x11.auth.v1.AuthNReq\x1a\x11.auth.v1.AuthNRes\"\x00B-Z+github.com/absmach/supermq/api/grpc/auth/v1b\x06proto3"
var (
file_auth_v1_auth_proto_rawDescOnce sync.Once
@@ -436,28 +492,27 @@ func file_auth_v1_auth_proto_rawDescGZIP() []byte {
return file_auth_v1_auth_proto_rawDescData
}
var file_auth_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_auth_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_auth_v1_auth_proto_goTypes = []any{
(*AuthNReq)(nil), // 0: auth.v1.AuthNReq
(*AuthNRes)(nil), // 1: auth.v1.AuthNRes
(*AuthZReq)(nil), // 2: auth.v1.AuthZReq
(*AuthZPatReq)(nil), // 3: auth.v1.AuthZPatReq
(*AuthZRes)(nil), // 4: auth.v1.AuthZRes
(*AuthNReq)(nil), // 0: auth.v1.AuthNReq
(*AuthNRes)(nil), // 1: auth.v1.AuthNRes
(*PolicyReq)(nil), // 2: auth.v1.PolicyReq
(*PATReq)(nil), // 3: auth.v1.PATReq
(*AuthZReq)(nil), // 4: auth.v1.AuthZReq
(*AuthZRes)(nil), // 5: auth.v1.AuthZRes
}
var file_auth_v1_auth_proto_depIdxs = []int32{
2, // 0: auth.v1.AuthService.Authorize:input_type -> auth.v1.AuthZReq
3, // 1: auth.v1.AuthService.AuthorizePAT:input_type -> auth.v1.AuthZPatReq
0, // 2: auth.v1.AuthService.Authenticate:input_type -> auth.v1.AuthNReq
0, // 3: auth.v1.AuthService.AuthenticatePAT:input_type -> auth.v1.AuthNReq
4, // 4: auth.v1.AuthService.Authorize:output_type -> auth.v1.AuthZRes
4, // 5: auth.v1.AuthService.AuthorizePAT:output_type -> auth.v1.AuthZRes
1, // 6: auth.v1.AuthService.Authenticate:output_type -> auth.v1.AuthNRes
1, // 7: auth.v1.AuthService.AuthenticatePAT:output_type -> auth.v1.AuthNRes
4, // [4:8] is the sub-list for method output_type
0, // [0:4] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
2, // 0: auth.v1.AuthZReq.policy_req:type_name -> auth.v1.PolicyReq
3, // 1: auth.v1.AuthZReq.pat_req:type_name -> auth.v1.PATReq
4, // 2: auth.v1.AuthService.Authorize:input_type -> auth.v1.AuthZReq
0, // 3: auth.v1.AuthService.Authenticate:input_type -> auth.v1.AuthNReq
5, // 4: auth.v1.AuthService.Authorize:output_type -> auth.v1.AuthZRes
1, // 5: auth.v1.AuthService.Authenticate:output_type -> auth.v1.AuthNRes
4, // [4:6] is the sub-list for method output_type
2, // [2:4] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_auth_v1_auth_proto_init() }
@@ -465,13 +520,14 @@ func file_auth_v1_auth_proto_init() {
if File_auth_v1_auth_proto != nil {
return
}
file_auth_v1_auth_proto_msgTypes[4].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_auth_v1_auth_proto_rawDesc), len(file_auth_v1_auth_proto_rawDesc)),
NumEnums: 0,
NumMessages: 5,
NumMessages: 6,
NumExtensions: 0,
NumServices: 1,
},
+6 -82
View File
@@ -3,7 +3,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc-gen-go-grpc v1.6.0
// - protoc v6.33.0
// source: auth/v1/auth.proto
@@ -22,10 +22,8 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
AuthService_Authorize_FullMethodName = "/auth.v1.AuthService/Authorize"
AuthService_AuthorizePAT_FullMethodName = "/auth.v1.AuthService/AuthorizePAT"
AuthService_Authenticate_FullMethodName = "/auth.v1.AuthService/Authenticate"
AuthService_AuthenticatePAT_FullMethodName = "/auth.v1.AuthService/AuthenticatePAT"
AuthService_Authorize_FullMethodName = "/auth.v1.AuthService/Authorize"
AuthService_Authenticate_FullMethodName = "/auth.v1.AuthService/Authenticate"
)
// AuthServiceClient is the client API for AuthService service.
@@ -36,9 +34,7 @@ const (
// and authorization functionalities for SuperMQ services.
type AuthServiceClient interface {
Authorize(ctx context.Context, in *AuthZReq, opts ...grpc.CallOption) (*AuthZRes, error)
AuthorizePAT(ctx context.Context, in *AuthZPatReq, opts ...grpc.CallOption) (*AuthZRes, error)
Authenticate(ctx context.Context, in *AuthNReq, opts ...grpc.CallOption) (*AuthNRes, error)
AuthenticatePAT(ctx context.Context, in *AuthNReq, opts ...grpc.CallOption) (*AuthNRes, error)
}
type authServiceClient struct {
@@ -59,16 +55,6 @@ func (c *authServiceClient) Authorize(ctx context.Context, in *AuthZReq, opts ..
return out, nil
}
func (c *authServiceClient) AuthorizePAT(ctx context.Context, in *AuthZPatReq, opts ...grpc.CallOption) (*AuthZRes, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AuthZRes)
err := c.cc.Invoke(ctx, AuthService_AuthorizePAT_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) Authenticate(ctx context.Context, in *AuthNReq, opts ...grpc.CallOption) (*AuthNRes, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AuthNRes)
@@ -79,16 +65,6 @@ func (c *authServiceClient) Authenticate(ctx context.Context, in *AuthNReq, opts
return out, nil
}
func (c *authServiceClient) AuthenticatePAT(ctx context.Context, in *AuthNReq, opts ...grpc.CallOption) (*AuthNRes, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AuthNRes)
err := c.cc.Invoke(ctx, AuthService_AuthenticatePAT_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.
@@ -97,9 +73,7 @@ func (c *authServiceClient) AuthenticatePAT(ctx context.Context, in *AuthNReq, o
// and authorization functionalities for SuperMQ services.
type AuthServiceServer interface {
Authorize(context.Context, *AuthZReq) (*AuthZRes, error)
AuthorizePAT(context.Context, *AuthZPatReq) (*AuthZRes, error)
Authenticate(context.Context, *AuthNReq) (*AuthNRes, error)
AuthenticatePAT(context.Context, *AuthNReq) (*AuthNRes, error)
mustEmbedUnimplementedAuthServiceServer()
}
@@ -111,16 +85,10 @@ type AuthServiceServer interface {
type UnimplementedAuthServiceServer struct{}
func (UnimplementedAuthServiceServer) Authorize(context.Context, *AuthZReq) (*AuthZRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method Authorize not implemented")
}
func (UnimplementedAuthServiceServer) AuthorizePAT(context.Context, *AuthZPatReq) (*AuthZRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method AuthorizePAT not implemented")
return nil, status.Error(codes.Unimplemented, "method Authorize not implemented")
}
func (UnimplementedAuthServiceServer) Authenticate(context.Context, *AuthNReq) (*AuthNRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method Authenticate not implemented")
}
func (UnimplementedAuthServiceServer) AuthenticatePAT(context.Context, *AuthNReq) (*AuthNRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method AuthenticatePAT not implemented")
return nil, status.Error(codes.Unimplemented, "method Authenticate not implemented")
}
func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {}
func (UnimplementedAuthServiceServer) testEmbeddedByValue() {}
@@ -133,7 +101,7 @@ type UnsafeAuthServiceServer interface {
}
func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) {
// If the following call pancis, it indicates UnimplementedAuthServiceServer was
// If the following call panics, 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.
@@ -161,24 +129,6 @@ func _AuthService_Authorize_Handler(srv interface{}, ctx context.Context, dec fu
return interceptor(ctx, in, info, handler)
}
func _AuthService_AuthorizePAT_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AuthZPatReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).AuthorizePAT(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_AuthorizePAT_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).AuthorizePAT(ctx, req.(*AuthZPatReq))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_Authenticate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AuthNReq)
if err := dec(in); err != nil {
@@ -197,24 +147,6 @@ func _AuthService_Authenticate_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler)
}
func _AuthService_AuthenticatePAT_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AuthNReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).AuthenticatePAT(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_AuthenticatePAT_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).AuthenticatePAT(ctx, req.(*AuthNReq))
}
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)
@@ -226,18 +158,10 @@ var AuthService_ServiceDesc = grpc.ServiceDesc{
MethodName: "Authorize",
Handler: _AuthService_Authorize_Handler,
},
{
MethodName: "AuthorizePAT",
Handler: _AuthService_AuthorizePAT_Handler,
},
{
MethodName: "Authenticate",
Handler: _AuthService_Authenticate_Handler,
},
{
MethodName: "AuthenticatePAT",
Handler: _AuthService_AuthenticatePAT_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "auth/v1/auth.proto",
+1 -1
View File
@@ -3,7 +3,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc-gen-go v1.36.11
// protoc v6.33.0
// source: channels/v1/channels.proto
+7 -7
View File
@@ -3,7 +3,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc-gen-go-grpc v1.6.0
// - protoc v6.33.0
// source: channels/v1/channels.proto
@@ -119,19 +119,19 @@ type ChannelsServiceServer interface {
type UnimplementedChannelsServiceServer struct{}
func (UnimplementedChannelsServiceServer) Authorize(context.Context, *AuthzReq) (*AuthzRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method Authorize not implemented")
return nil, status.Error(codes.Unimplemented, "method Authorize not implemented")
}
func (UnimplementedChannelsServiceServer) RemoveClientConnections(context.Context, *RemoveClientConnectionsReq) (*RemoveClientConnectionsRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveClientConnections not implemented")
return nil, status.Error(codes.Unimplemented, "method RemoveClientConnections not implemented")
}
func (UnimplementedChannelsServiceServer) UnsetParentGroupFromChannels(context.Context, *UnsetParentGroupFromChannelsReq) (*UnsetParentGroupFromChannelsRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method UnsetParentGroupFromChannels not implemented")
return nil, status.Error(codes.Unimplemented, "method UnsetParentGroupFromChannels not implemented")
}
func (UnimplementedChannelsServiceServer) RetrieveEntity(context.Context, *v1.RetrieveEntityReq) (*v1.RetrieveEntityRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method RetrieveEntity not implemented")
return nil, status.Error(codes.Unimplemented, "method RetrieveEntity not implemented")
}
func (UnimplementedChannelsServiceServer) RetrieveIDByRoute(context.Context, *v1.RetrieveIDByRouteReq) (*v1.RetrieveEntityRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method RetrieveIDByRoute not implemented")
return nil, status.Error(codes.Unimplemented, "method RetrieveIDByRoute not implemented")
}
func (UnimplementedChannelsServiceServer) mustEmbedUnimplementedChannelsServiceServer() {}
func (UnimplementedChannelsServiceServer) testEmbeddedByValue() {}
@@ -144,7 +144,7 @@ type UnsafeChannelsServiceServer interface {
}
func RegisterChannelsServiceServer(s grpc.ServiceRegistrar, srv ChannelsServiceServer) {
// If the following call pancis, it indicates UnimplementedChannelsServiceServer was
// If the following call panics, it indicates UnimplementedChannelsServiceServer 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.
+1 -1
View File
@@ -3,7 +3,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc-gen-go v1.36.11
// protoc v6.33.0
// source: clients/v1/clients.proto
+9 -9
View File
@@ -3,7 +3,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc-gen-go-grpc v1.6.0
// - protoc v6.33.0
// source: clients/v1/clients.proto
@@ -153,25 +153,25 @@ type ClientsServiceServer interface {
type UnimplementedClientsServiceServer struct{}
func (UnimplementedClientsServiceServer) Authenticate(context.Context, *AuthnReq) (*AuthnRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method Authenticate not implemented")
return nil, status.Error(codes.Unimplemented, "method Authenticate not implemented")
}
func (UnimplementedClientsServiceServer) RetrieveEntity(context.Context, *v1.RetrieveEntityReq) (*v1.RetrieveEntityRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method RetrieveEntity not implemented")
return nil, status.Error(codes.Unimplemented, "method RetrieveEntity not implemented")
}
func (UnimplementedClientsServiceServer) RetrieveEntities(context.Context, *v1.RetrieveEntitiesReq) (*v1.RetrieveEntitiesRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method RetrieveEntities not implemented")
return nil, status.Error(codes.Unimplemented, "method RetrieveEntities not implemented")
}
func (UnimplementedClientsServiceServer) AddConnections(context.Context, *v1.AddConnectionsReq) (*v1.AddConnectionsRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method AddConnections not implemented")
return nil, status.Error(codes.Unimplemented, "method AddConnections not implemented")
}
func (UnimplementedClientsServiceServer) RemoveConnections(context.Context, *v1.RemoveConnectionsReq) (*v1.RemoveConnectionsRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveConnections not implemented")
return nil, status.Error(codes.Unimplemented, "method RemoveConnections not implemented")
}
func (UnimplementedClientsServiceServer) RemoveChannelConnections(context.Context, *RemoveChannelConnectionsReq) (*RemoveChannelConnectionsRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveChannelConnections not implemented")
return nil, status.Error(codes.Unimplemented, "method RemoveChannelConnections not implemented")
}
func (UnimplementedClientsServiceServer) UnsetParentGroupFromClient(context.Context, *UnsetParentGroupFromClientReq) (*UnsetParentGroupFromClientRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method UnsetParentGroupFromClient not implemented")
return nil, status.Error(codes.Unimplemented, "method UnsetParentGroupFromClient not implemented")
}
func (UnimplementedClientsServiceServer) mustEmbedUnimplementedClientsServiceServer() {}
func (UnimplementedClientsServiceServer) testEmbeddedByValue() {}
@@ -184,7 +184,7 @@ type UnsafeClientsServiceServer interface {
}
func RegisterClientsServiceServer(s grpc.ServiceRegistrar, srv ClientsServiceServer) {
// If the following call pancis, it indicates UnimplementedClientsServiceServer was
// If the following call panics, it indicates UnimplementedClientsServiceServer 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.
+1 -1
View File
@@ -3,7 +3,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc-gen-go v1.36.11
// protoc v6.33.0
// source: common/v1/common.proto
+1 -1
View File
@@ -3,7 +3,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc-gen-go v1.36.11
// protoc v6.33.0
// source: domains/v1/domains.proto
+5 -5
View File
@@ -3,7 +3,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc-gen-go-grpc v1.6.0
// - protoc v6.33.0
// source: domains/v1/domains.proto
@@ -99,13 +99,13 @@ type DomainsServiceServer interface {
type UnimplementedDomainsServiceServer struct{}
func (UnimplementedDomainsServiceServer) DeleteUserFromDomains(context.Context, *DeleteUserReq) (*DeleteUserRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteUserFromDomains not implemented")
return nil, status.Error(codes.Unimplemented, "method DeleteUserFromDomains not implemented")
}
func (UnimplementedDomainsServiceServer) RetrieveStatus(context.Context, *v1.RetrieveEntityReq) (*v1.RetrieveEntityRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method RetrieveStatus not implemented")
return nil, status.Error(codes.Unimplemented, "method RetrieveStatus not implemented")
}
func (UnimplementedDomainsServiceServer) RetrieveIDByRoute(context.Context, *v1.RetrieveIDByRouteReq) (*v1.RetrieveEntityRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method RetrieveIDByRoute not implemented")
return nil, status.Error(codes.Unimplemented, "method RetrieveIDByRoute not implemented")
}
func (UnimplementedDomainsServiceServer) mustEmbedUnimplementedDomainsServiceServer() {}
func (UnimplementedDomainsServiceServer) testEmbeddedByValue() {}
@@ -118,7 +118,7 @@ type UnsafeDomainsServiceServer interface {
}
func RegisterDomainsServiceServer(s grpc.ServiceRegistrar, srv DomainsServiceServer) {
// If the following call pancis, it indicates UnimplementedDomainsServiceServer was
// If the following call panics, it indicates UnimplementedDomainsServiceServer 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.
+1 -1
View File
@@ -3,7 +3,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc-gen-go v1.36.11
// protoc v6.33.0
// source: groups/v1/groups.proto
+3 -3
View File
@@ -3,7 +3,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc-gen-go-grpc v1.6.0
// - protoc v6.33.0
// source: groups/v1/groups.proto
@@ -73,7 +73,7 @@ type GroupsServiceServer interface {
type UnimplementedGroupsServiceServer struct{}
func (UnimplementedGroupsServiceServer) RetrieveEntity(context.Context, *v1.RetrieveEntityReq) (*v1.RetrieveEntityRes, error) {
return nil, status.Errorf(codes.Unimplemented, "method RetrieveEntity not implemented")
return nil, status.Error(codes.Unimplemented, "method RetrieveEntity not implemented")
}
func (UnimplementedGroupsServiceServer) mustEmbedUnimplementedGroupsServiceServer() {}
func (UnimplementedGroupsServiceServer) testEmbeddedByValue() {}
@@ -86,7 +86,7 @@ type UnsafeGroupsServiceServer interface {
}
func RegisterGroupsServiceServer(s grpc.ServiceRegistrar, srv GroupsServiceServer) {
// If the following call pancis, it indicates UnimplementedGroupsServiceServer was
// If the following call panics, it indicates UnimplementedGroupsServiceServer 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.
+284 -24
View File
@@ -3,7 +3,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc-gen-go v1.36.11
// protoc v6.33.0
// source: token/v1/token.proto
@@ -30,6 +30,7 @@ type IssueReq struct {
UserRole uint32 `protobuf:"varint,2,opt,name=user_role,json=userRole,proto3" json:"user_role,omitempty"`
Type uint32 `protobuf:"varint,3,opt,name=type,proto3" json:"type,omitempty"`
Verified bool `protobuf:"varint,4,opt,name=verified,proto3" json:"verified,omitempty"`
Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -92,6 +93,13 @@ func (x *IssueReq) GetVerified() bool {
return false
}
func (x *IssueReq) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
type RefreshReq struct {
state protoimpl.MessageState `protogen:"open.v1"`
RefreshToken string `protobuf:"bytes,1,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"`
@@ -144,6 +152,58 @@ func (x *RefreshReq) GetVerified() bool {
return false
}
type RevokeReq struct {
state protoimpl.MessageState `protogen:"open.v1"`
TokenId string `protobuf:"bytes,1,opt,name=token_id,json=tokenId,proto3" json:"token_id,omitempty"`
UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RevokeReq) Reset() {
*x = RevokeReq{}
mi := &file_token_v1_token_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RevokeReq) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RevokeReq) ProtoMessage() {}
func (x *RevokeReq) ProtoReflect() protoreflect.Message {
mi := &file_token_v1_token_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 RevokeReq.ProtoReflect.Descriptor instead.
func (*RevokeReq) Descriptor() ([]byte, []int) {
return file_token_v1_token_proto_rawDescGZIP(), []int{2}
}
func (x *RevokeReq) GetTokenId() string {
if x != nil {
return x.TokenId
}
return ""
}
func (x *RevokeReq) GetUserId() string {
if x != nil {
return x.UserId
}
return ""
}
// If a token is not carrying any information itself, the type
// field can be used to determine how to validate the token.
// Also, different tokens can be encoded in different ways.
@@ -158,7 +218,7 @@ type Token struct {
func (x *Token) Reset() {
*x = Token{}
mi := &file_token_v1_token_proto_msgTypes[2]
mi := &file_token_v1_token_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -170,7 +230,7 @@ func (x *Token) String() string {
func (*Token) ProtoMessage() {}
func (x *Token) ProtoReflect() protoreflect.Message {
mi := &file_token_v1_token_proto_msgTypes[2]
mi := &file_token_v1_token_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -183,7 +243,7 @@ func (x *Token) ProtoReflect() protoreflect.Message {
// Deprecated: Use Token.ProtoReflect.Descriptor instead.
func (*Token) Descriptor() ([]byte, []int) {
return file_token_v1_token_proto_rawDescGZIP(), []int{2}
return file_token_v1_token_proto_rawDescGZIP(), []int{3}
}
func (x *Token) GetAccessToken() string {
@@ -207,29 +267,219 @@ func (x *Token) GetAccessType() string {
return ""
}
type RevokeRes struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RevokeRes) Reset() {
*x = RevokeRes{}
mi := &file_token_v1_token_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RevokeRes) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RevokeRes) ProtoMessage() {}
func (x *RevokeRes) ProtoReflect() protoreflect.Message {
mi := &file_token_v1_token_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 RevokeRes.ProtoReflect.Descriptor instead.
func (*RevokeRes) Descriptor() ([]byte, []int) {
return file_token_v1_token_proto_rawDescGZIP(), []int{4}
}
type ListUserRefreshTokensReq struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListUserRefreshTokensReq) Reset() {
*x = ListUserRefreshTokensReq{}
mi := &file_token_v1_token_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListUserRefreshTokensReq) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListUserRefreshTokensReq) ProtoMessage() {}
func (x *ListUserRefreshTokensReq) ProtoReflect() protoreflect.Message {
mi := &file_token_v1_token_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 ListUserRefreshTokensReq.ProtoReflect.Descriptor instead.
func (*ListUserRefreshTokensReq) Descriptor() ([]byte, []int) {
return file_token_v1_token_proto_rawDescGZIP(), []int{5}
}
func (x *ListUserRefreshTokensReq) GetUserId() string {
if x != nil {
return x.UserId
}
return ""
}
type ListUserRefreshTokensRes struct {
state protoimpl.MessageState `protogen:"open.v1"`
RefreshTokens []*RefreshToken `protobuf:"bytes,1,rep,name=refresh_tokens,json=refreshTokens,proto3" json:"refresh_tokens,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListUserRefreshTokensRes) Reset() {
*x = ListUserRefreshTokensRes{}
mi := &file_token_v1_token_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListUserRefreshTokensRes) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListUserRefreshTokensRes) ProtoMessage() {}
func (x *ListUserRefreshTokensRes) ProtoReflect() protoreflect.Message {
mi := &file_token_v1_token_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 ListUserRefreshTokensRes.ProtoReflect.Descriptor instead.
func (*ListUserRefreshTokensRes) Descriptor() ([]byte, []int) {
return file_token_v1_token_proto_rawDescGZIP(), []int{6}
}
func (x *ListUserRefreshTokensRes) GetRefreshTokens() []*RefreshToken {
if x != nil {
return x.RefreshTokens
}
return nil
}
type RefreshToken struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RefreshToken) Reset() {
*x = RefreshToken{}
mi := &file_token_v1_token_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RefreshToken) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RefreshToken) ProtoMessage() {}
func (x *RefreshToken) ProtoReflect() protoreflect.Message {
mi := &file_token_v1_token_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 RefreshToken.ProtoReflect.Descriptor instead.
func (*RefreshToken) Descriptor() ([]byte, []int) {
return file_token_v1_token_proto_rawDescGZIP(), []int{7}
}
func (x *RefreshToken) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *RefreshToken) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
var File_token_v1_token_proto protoreflect.FileDescriptor
const file_token_v1_token_proto_rawDesc = "" +
"\n" +
"\x14token/v1/token.proto\x12\btoken.v1\"p\n" +
"\x14token/v1/token.proto\x12\btoken.v1\"\x92\x01\n" +
"\bIssueReq\x12\x17\n" +
"\auser_id\x18\x01 \x01(\tR\x06userId\x12\x1b\n" +
"\tuser_role\x18\x02 \x01(\rR\buserRole\x12\x12\n" +
"\x04type\x18\x03 \x01(\rR\x04type\x12\x1a\n" +
"\bverified\x18\x04 \x01(\bR\bverified\"M\n" +
"\bverified\x18\x04 \x01(\bR\bverified\x12 \n" +
"\vdescription\x18\x05 \x01(\tR\vdescription\"M\n" +
"\n" +
"RefreshReq\x12#\n" +
"\rrefresh_token\x18\x01 \x01(\tR\frefreshToken\x12\x1a\n" +
"\bverified\x18\x02 \x01(\bR\bverified\"\x87\x01\n" +
"\bverified\x18\x02 \x01(\bR\bverified\"?\n" +
"\tRevokeReq\x12\x19\n" +
"\btoken_id\x18\x01 \x01(\tR\atokenId\x12\x17\n" +
"\auser_id\x18\x02 \x01(\tR\x06userId\"\x87\x01\n" +
"\x05Token\x12!\n" +
"\faccess_token\x18\x01 \x01(\tR\vaccessToken\x12(\n" +
"\rrefresh_token\x18\x02 \x01(\tH\x00R\frefreshToken\x88\x01\x01\x12\x1f\n" +
"\vaccess_type\x18\x03 \x01(\tR\n" +
"accessTypeB\x10\n" +
"\x0e_refresh_token2r\n" +
"\x0e_refresh_token\"\v\n" +
"\tRevokeRes\"3\n" +
"\x18ListUserRefreshTokensReq\x12\x17\n" +
"\auser_id\x18\x01 \x01(\tR\x06userId\"Y\n" +
"\x18ListUserRefreshTokensRes\x12=\n" +
"\x0erefresh_tokens\x18\x01 \x03(\v2\x16.token.v1.RefreshTokenR\rrefreshTokens\"@\n" +
"\fRefreshToken\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12 \n" +
"\vdescription\x18\x02 \x01(\tR\vdescription2\x8b\x02\n" +
"\fTokenService\x12.\n" +
"\x05Issue\x12\x12.token.v1.IssueReq\x1a\x0f.token.v1.Token\"\x00\x122\n" +
"\aRefresh\x12\x14.token.v1.RefreshReq\x1a\x0f.token.v1.Token\"\x00B.Z,github.com/absmach/supermq/api/grpc/token/v1b\x06proto3"
"\aRefresh\x12\x14.token.v1.RefreshReq\x1a\x0f.token.v1.Token\"\x00\x124\n" +
"\x06Revoke\x12\x13.token.v1.RevokeReq\x1a\x13.token.v1.RevokeRes\"\x00\x12a\n" +
"\x15ListUserRefreshTokens\x12\".token.v1.ListUserRefreshTokensReq\x1a\".token.v1.ListUserRefreshTokensRes\"\x00B.Z,github.com/absmach/supermq/api/grpc/token/v1b\x06proto3"
var (
file_token_v1_token_proto_rawDescOnce sync.Once
@@ -243,22 +493,32 @@ func file_token_v1_token_proto_rawDescGZIP() []byte {
return file_token_v1_token_proto_rawDescData
}
var file_token_v1_token_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_token_v1_token_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_token_v1_token_proto_goTypes = []any{
(*IssueReq)(nil), // 0: token.v1.IssueReq
(*RefreshReq)(nil), // 1: token.v1.RefreshReq
(*Token)(nil), // 2: token.v1.Token
(*IssueReq)(nil), // 0: token.v1.IssueReq
(*RefreshReq)(nil), // 1: token.v1.RefreshReq
(*RevokeReq)(nil), // 2: token.v1.RevokeReq
(*Token)(nil), // 3: token.v1.Token
(*RevokeRes)(nil), // 4: token.v1.RevokeRes
(*ListUserRefreshTokensReq)(nil), // 5: token.v1.ListUserRefreshTokensReq
(*ListUserRefreshTokensRes)(nil), // 6: token.v1.ListUserRefreshTokensRes
(*RefreshToken)(nil), // 7: token.v1.RefreshToken
}
var file_token_v1_token_proto_depIdxs = []int32{
0, // 0: token.v1.TokenService.Issue:input_type -> token.v1.IssueReq
1, // 1: token.v1.TokenService.Refresh:input_type -> token.v1.RefreshReq
2, // 2: token.v1.TokenService.Issue:output_type -> token.v1.Token
2, // 3: token.v1.TokenService.Refresh:output_type -> token.v1.Token
2, // [2:4] is the sub-list for method output_type
0, // [0:2] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
7, // 0: token.v1.ListUserRefreshTokensRes.refresh_tokens:type_name -> token.v1.RefreshToken
0, // 1: token.v1.TokenService.Issue:input_type -> token.v1.IssueReq
1, // 2: token.v1.TokenService.Refresh:input_type -> token.v1.RefreshReq
2, // 3: token.v1.TokenService.Revoke:input_type -> token.v1.RevokeReq
5, // 4: token.v1.TokenService.ListUserRefreshTokens:input_type -> token.v1.ListUserRefreshTokensReq
3, // 5: token.v1.TokenService.Issue:output_type -> token.v1.Token
3, // 6: token.v1.TokenService.Refresh:output_type -> token.v1.Token
4, // 7: token.v1.TokenService.Revoke:output_type -> token.v1.RevokeRes
6, // 8: token.v1.TokenService.ListUserRefreshTokens:output_type -> token.v1.ListUserRefreshTokensRes
5, // [5:9] is the sub-list for method output_type
1, // [1:5] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_token_v1_token_proto_init() }
@@ -266,14 +526,14 @@ func file_token_v1_token_proto_init() {
if File_token_v1_token_proto != nil {
return
}
file_token_v1_token_proto_msgTypes[2].OneofWrappers = []any{}
file_token_v1_token_proto_msgTypes[3].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_token_v1_token_proto_rawDesc), len(file_token_v1_token_proto_rawDesc)),
NumEnums: 0,
NumMessages: 3,
NumMessages: 8,
NumExtensions: 0,
NumServices: 1,
},
+82 -6
View File
@@ -3,7 +3,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc-gen-go-grpc v1.6.0
// - protoc v6.33.0
// source: token/v1/token.proto
@@ -22,8 +22,10 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
TokenService_Issue_FullMethodName = "/token.v1.TokenService/Issue"
TokenService_Refresh_FullMethodName = "/token.v1.TokenService/Refresh"
TokenService_Issue_FullMethodName = "/token.v1.TokenService/Issue"
TokenService_Refresh_FullMethodName = "/token.v1.TokenService/Refresh"
TokenService_Revoke_FullMethodName = "/token.v1.TokenService/Revoke"
TokenService_ListUserRefreshTokens_FullMethodName = "/token.v1.TokenService/ListUserRefreshTokens"
)
// TokenServiceClient is the client API for TokenService service.
@@ -32,6 +34,8 @@ const (
type TokenServiceClient interface {
Issue(ctx context.Context, in *IssueReq, opts ...grpc.CallOption) (*Token, error)
Refresh(ctx context.Context, in *RefreshReq, opts ...grpc.CallOption) (*Token, error)
Revoke(ctx context.Context, in *RevokeReq, opts ...grpc.CallOption) (*RevokeRes, error)
ListUserRefreshTokens(ctx context.Context, in *ListUserRefreshTokensReq, opts ...grpc.CallOption) (*ListUserRefreshTokensRes, error)
}
type tokenServiceClient struct {
@@ -62,12 +66,34 @@ func (c *tokenServiceClient) Refresh(ctx context.Context, in *RefreshReq, opts .
return out, nil
}
func (c *tokenServiceClient) Revoke(ctx context.Context, in *RevokeReq, opts ...grpc.CallOption) (*RevokeRes, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RevokeRes)
err := c.cc.Invoke(ctx, TokenService_Revoke_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *tokenServiceClient) ListUserRefreshTokens(ctx context.Context, in *ListUserRefreshTokensReq, opts ...grpc.CallOption) (*ListUserRefreshTokensRes, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListUserRefreshTokensRes)
err := c.cc.Invoke(ctx, TokenService_ListUserRefreshTokens_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// TokenServiceServer is the server API for TokenService service.
// All implementations must embed UnimplementedTokenServiceServer
// for forward compatibility.
type TokenServiceServer interface {
Issue(context.Context, *IssueReq) (*Token, error)
Refresh(context.Context, *RefreshReq) (*Token, error)
Revoke(context.Context, *RevokeReq) (*RevokeRes, error)
ListUserRefreshTokens(context.Context, *ListUserRefreshTokensReq) (*ListUserRefreshTokensRes, error)
mustEmbedUnimplementedTokenServiceServer()
}
@@ -79,10 +105,16 @@ type TokenServiceServer interface {
type UnimplementedTokenServiceServer struct{}
func (UnimplementedTokenServiceServer) Issue(context.Context, *IssueReq) (*Token, error) {
return nil, status.Errorf(codes.Unimplemented, "method Issue not implemented")
return nil, status.Error(codes.Unimplemented, "method Issue not implemented")
}
func (UnimplementedTokenServiceServer) Refresh(context.Context, *RefreshReq) (*Token, error) {
return nil, status.Errorf(codes.Unimplemented, "method Refresh not implemented")
return nil, status.Error(codes.Unimplemented, "method Refresh not implemented")
}
func (UnimplementedTokenServiceServer) Revoke(context.Context, *RevokeReq) (*RevokeRes, error) {
return nil, status.Error(codes.Unimplemented, "method Revoke not implemented")
}
func (UnimplementedTokenServiceServer) ListUserRefreshTokens(context.Context, *ListUserRefreshTokensReq) (*ListUserRefreshTokensRes, error) {
return nil, status.Error(codes.Unimplemented, "method ListUserRefreshTokens not implemented")
}
func (UnimplementedTokenServiceServer) mustEmbedUnimplementedTokenServiceServer() {}
func (UnimplementedTokenServiceServer) testEmbeddedByValue() {}
@@ -95,7 +127,7 @@ type UnsafeTokenServiceServer interface {
}
func RegisterTokenServiceServer(s grpc.ServiceRegistrar, srv TokenServiceServer) {
// If the following call pancis, it indicates UnimplementedTokenServiceServer was
// If the following call panics, it indicates UnimplementedTokenServiceServer 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.
@@ -141,6 +173,42 @@ func _TokenService_Refresh_Handler(srv interface{}, ctx context.Context, dec fun
return interceptor(ctx, in, info, handler)
}
func _TokenService_Revoke_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RevokeReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TokenServiceServer).Revoke(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: TokenService_Revoke_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TokenServiceServer).Revoke(ctx, req.(*RevokeReq))
}
return interceptor(ctx, in, info, handler)
}
func _TokenService_ListUserRefreshTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListUserRefreshTokensReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TokenServiceServer).ListUserRefreshTokens(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: TokenService_ListUserRefreshTokens_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TokenServiceServer).ListUserRefreshTokens(ctx, req.(*ListUserRefreshTokensReq))
}
return interceptor(ctx, in, info, handler)
}
// TokenService_ServiceDesc is the grpc.ServiceDesc for TokenService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -156,6 +224,14 @@ var TokenService_ServiceDesc = grpc.ServiceDesc{
MethodName: "Refresh",
Handler: _TokenService_Refresh_Handler,
},
{
MethodName: "Revoke",
Handler: _TokenService_Revoke_Handler,
},
{
MethodName: "ListUserRefreshTokens",
Handler: _TokenService_ListUserRefreshTokens_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "token/v1/token.proto",
+428
View File
@@ -0,0 +1,428 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.33.0
// source: users/v1/users.proto
package v1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
structpb "google.golang.org/protobuf/types/known/structpb"
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 RetrieveUsersReq struct {
state protoimpl.MessageState `protogen:"open.v1"`
Ids []string `protobuf:"bytes,1,rep,name=ids,proto3" json:"ids,omitempty"`
Offset uint64 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"`
Limit uint64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RetrieveUsersReq) Reset() {
*x = RetrieveUsersReq{}
mi := &file_users_v1_users_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RetrieveUsersReq) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RetrieveUsersReq) ProtoMessage() {}
func (x *RetrieveUsersReq) ProtoReflect() protoreflect.Message {
mi := &file_users_v1_users_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 RetrieveUsersReq.ProtoReflect.Descriptor instead.
func (*RetrieveUsersReq) Descriptor() ([]byte, []int) {
return file_users_v1_users_proto_rawDescGZIP(), []int{0}
}
func (x *RetrieveUsersReq) GetIds() []string {
if x != nil {
return x.Ids
}
return nil
}
func (x *RetrieveUsersReq) GetOffset() uint64 {
if x != nil {
return x.Offset
}
return 0
}
func (x *RetrieveUsersReq) GetLimit() uint64 {
if x != nil {
return x.Limit
}
return 0
}
type RetrieveUsersRes struct {
state protoimpl.MessageState `protogen:"open.v1"`
Total uint64 `protobuf:"varint,1,opt,name=total,proto3" json:"total,omitempty"`
Limit uint64 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"`
Offset uint64 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"`
Users []*User `protobuf:"bytes,4,rep,name=users,proto3" json:"users,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RetrieveUsersRes) Reset() {
*x = RetrieveUsersRes{}
mi := &file_users_v1_users_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RetrieveUsersRes) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RetrieveUsersRes) ProtoMessage() {}
func (x *RetrieveUsersRes) ProtoReflect() protoreflect.Message {
mi := &file_users_v1_users_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 RetrieveUsersRes.ProtoReflect.Descriptor instead.
func (*RetrieveUsersRes) Descriptor() ([]byte, []int) {
return file_users_v1_users_proto_rawDescGZIP(), []int{1}
}
func (x *RetrieveUsersRes) GetTotal() uint64 {
if x != nil {
return x.Total
}
return 0
}
func (x *RetrieveUsersRes) GetLimit() uint64 {
if x != nil {
return x.Limit
}
return 0
}
func (x *RetrieveUsersRes) GetOffset() uint64 {
if x != nil {
return x.Offset
}
return 0
}
func (x *RetrieveUsersRes) GetUsers() []*User {
if x != nil {
return x.Users
}
return nil
}
type User struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
FirstName string `protobuf:"bytes,2,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"`
LastName string `protobuf:"bytes,3,opt,name=last_name,json=lastName,proto3" json:"last_name,omitempty"`
Tags []string `protobuf:"bytes,4,rep,name=tags,proto3" json:"tags,omitempty"`
Metadata *structpb.Struct `protobuf:"bytes,5,opt,name=metadata,proto3" json:"metadata,omitempty"`
PrivateMetadata *structpb.Struct `protobuf:"bytes,6,opt,name=private_metadata,json=privateMetadata,proto3" json:"private_metadata,omitempty"`
Status uint32 `protobuf:"varint,7,opt,name=status,proto3" json:"status,omitempty"`
Role uint32 `protobuf:"varint,8,opt,name=role,proto3" json:"role,omitempty"`
ProfilePicture string `protobuf:"bytes,9,opt,name=profile_picture,json=profilePicture,proto3" json:"profile_picture,omitempty"`
Username string `protobuf:"bytes,10,opt,name=username,proto3" json:"username,omitempty"`
Email string `protobuf:"bytes,11,opt,name=email,proto3" json:"email,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,13,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
UpdatedBy string `protobuf:"bytes,14,opt,name=updated_by,json=updatedBy,proto3" json:"updated_by,omitempty"`
VerifiedAt *timestamppb.Timestamp `protobuf:"bytes,15,opt,name=verified_at,json=verifiedAt,proto3" json:"verified_at,omitempty"`
AuthProvider string `protobuf:"bytes,16,opt,name=auth_provider,json=authProvider,proto3" json:"auth_provider,omitempty"`
Permissions []string `protobuf:"bytes,17,rep,name=permissions,proto3" json:"permissions,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *User) Reset() {
*x = User{}
mi := &file_users_v1_users_proto_msgTypes[2]
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_users_v1_users_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 User.ProtoReflect.Descriptor instead.
func (*User) Descriptor() ([]byte, []int) {
return file_users_v1_users_proto_rawDescGZIP(), []int{2}
}
func (x *User) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *User) GetFirstName() string {
if x != nil {
return x.FirstName
}
return ""
}
func (x *User) GetLastName() string {
if x != nil {
return x.LastName
}
return ""
}
func (x *User) GetTags() []string {
if x != nil {
return x.Tags
}
return nil
}
func (x *User) GetMetadata() *structpb.Struct {
if x != nil {
return x.Metadata
}
return nil
}
func (x *User) GetPrivateMetadata() *structpb.Struct {
if x != nil {
return x.PrivateMetadata
}
return nil
}
func (x *User) GetStatus() uint32 {
if x != nil {
return x.Status
}
return 0
}
func (x *User) GetRole() uint32 {
if x != nil {
return x.Role
}
return 0
}
func (x *User) GetProfilePicture() string {
if x != nil {
return x.ProfilePicture
}
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) GetCreatedAt() *timestamppb.Timestamp {
if x != nil {
return x.CreatedAt
}
return nil
}
func (x *User) GetUpdatedAt() *timestamppb.Timestamp {
if x != nil {
return x.UpdatedAt
}
return nil
}
func (x *User) GetUpdatedBy() string {
if x != nil {
return x.UpdatedBy
}
return ""
}
func (x *User) GetVerifiedAt() *timestamppb.Timestamp {
if x != nil {
return x.VerifiedAt
}
return nil
}
func (x *User) GetAuthProvider() string {
if x != nil {
return x.AuthProvider
}
return ""
}
func (x *User) GetPermissions() []string {
if x != nil {
return x.Permissions
}
return nil
}
var File_users_v1_users_proto protoreflect.FileDescriptor
const file_users_v1_users_proto_rawDesc = "" +
"\n" +
"\x14users/v1/users.proto\x12\busers.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"R\n" +
"\x10RetrieveUsersReq\x12\x10\n" +
"\x03ids\x18\x01 \x03(\tR\x03ids\x12\x16\n" +
"\x06offset\x18\x02 \x01(\x04R\x06offset\x12\x14\n" +
"\x05limit\x18\x03 \x01(\x04R\x05limit\"|\n" +
"\x10RetrieveUsersRes\x12\x14\n" +
"\x05total\x18\x01 \x01(\x04R\x05total\x12\x14\n" +
"\x05limit\x18\x02 \x01(\x04R\x05limit\x12\x16\n" +
"\x06offset\x18\x03 \x01(\x04R\x06offset\x12$\n" +
"\x05users\x18\x04 \x03(\v2\x0e.users.v1.UserR\x05users\"\xff\x04\n" +
"\x04User\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" +
"\n" +
"first_name\x18\x02 \x01(\tR\tfirstName\x12\x1b\n" +
"\tlast_name\x18\x03 \x01(\tR\blastName\x12\x12\n" +
"\x04tags\x18\x04 \x03(\tR\x04tags\x123\n" +
"\bmetadata\x18\x05 \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12B\n" +
"\x10private_metadata\x18\x06 \x01(\v2\x17.google.protobuf.StructR\x0fprivateMetadata\x12\x16\n" +
"\x06status\x18\a \x01(\rR\x06status\x12\x12\n" +
"\x04role\x18\b \x01(\rR\x04role\x12'\n" +
"\x0fprofile_picture\x18\t \x01(\tR\x0eprofilePicture\x12\x1a\n" +
"\busername\x18\n" +
" \x01(\tR\busername\x12\x14\n" +
"\x05email\x18\v \x01(\tR\x05email\x129\n" +
"\n" +
"created_at\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" +
"\n" +
"updated_at\x18\r \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x12\x1d\n" +
"\n" +
"updated_by\x18\x0e \x01(\tR\tupdatedBy\x12;\n" +
"\vverified_at\x18\x0f \x01(\v2\x1a.google.protobuf.TimestampR\n" +
"verifiedAt\x12#\n" +
"\rauth_provider\x18\x10 \x01(\tR\fauthProvider\x12 \n" +
"\vpermissions\x18\x11 \x03(\tR\vpermissions2Y\n" +
"\fUsersService\x12I\n" +
"\rRetrieveUsers\x12\x1a.users.v1.RetrieveUsersReq\x1a\x1a.users.v1.RetrieveUsersRes\"\x00B.Z,github.com/absmach/supermq/api/grpc/users/v1b\x06proto3"
var (
file_users_v1_users_proto_rawDescOnce sync.Once
file_users_v1_users_proto_rawDescData []byte
)
func file_users_v1_users_proto_rawDescGZIP() []byte {
file_users_v1_users_proto_rawDescOnce.Do(func() {
file_users_v1_users_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_users_v1_users_proto_rawDesc), len(file_users_v1_users_proto_rawDesc)))
})
return file_users_v1_users_proto_rawDescData
}
var file_users_v1_users_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_users_v1_users_proto_goTypes = []any{
(*RetrieveUsersReq)(nil), // 0: users.v1.RetrieveUsersReq
(*RetrieveUsersRes)(nil), // 1: users.v1.RetrieveUsersRes
(*User)(nil), // 2: users.v1.User
(*structpb.Struct)(nil), // 3: google.protobuf.Struct
(*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp
}
var file_users_v1_users_proto_depIdxs = []int32{
2, // 0: users.v1.RetrieveUsersRes.users:type_name -> users.v1.User
3, // 1: users.v1.User.metadata:type_name -> google.protobuf.Struct
3, // 2: users.v1.User.private_metadata:type_name -> google.protobuf.Struct
4, // 3: users.v1.User.created_at:type_name -> google.protobuf.Timestamp
4, // 4: users.v1.User.updated_at:type_name -> google.protobuf.Timestamp
4, // 5: users.v1.User.verified_at:type_name -> google.protobuf.Timestamp
0, // 6: users.v1.UsersService.RetrieveUsers:input_type -> users.v1.RetrieveUsersReq
1, // 7: users.v1.UsersService.RetrieveUsers:output_type -> users.v1.RetrieveUsersRes
7, // [7:8] is the sub-list for method output_type
6, // [6:7] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
}
func init() { file_users_v1_users_proto_init() }
func file_users_v1_users_proto_init() {
if File_users_v1_users_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_users_v1_users_proto_rawDesc), len(file_users_v1_users_proto_rawDesc)),
NumEnums: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_users_v1_users_proto_goTypes,
DependencyIndexes: file_users_v1_users_proto_depIdxs,
MessageInfos: file_users_v1_users_proto_msgTypes,
}.Build()
File_users_v1_users_proto = out.File
file_users_v1_users_proto_goTypes = nil
file_users_v1_users_proto_depIdxs = nil
}
+130
View File
@@ -0,0 +1,130 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.0
// - protoc v6.33.0
// source: users/v1/users.proto
package v1
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 (
UsersService_RetrieveUsers_FullMethodName = "/users.v1.UsersService/RetrieveUsers"
)
// UsersServiceClient is the client API for UsersService 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.
//
// UsersService exposes user-related functionality over gRPC.
type UsersServiceClient interface {
// RetrieveUsers fetches users for the provided IDs.
RetrieveUsers(ctx context.Context, in *RetrieveUsersReq, opts ...grpc.CallOption) (*RetrieveUsersRes, error)
}
type usersServiceClient struct {
cc grpc.ClientConnInterface
}
func NewUsersServiceClient(cc grpc.ClientConnInterface) UsersServiceClient {
return &usersServiceClient{cc}
}
func (c *usersServiceClient) RetrieveUsers(ctx context.Context, in *RetrieveUsersReq, opts ...grpc.CallOption) (*RetrieveUsersRes, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RetrieveUsersRes)
err := c.cc.Invoke(ctx, UsersService_RetrieveUsers_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// UsersServiceServer is the server API for UsersService service.
// All implementations must embed UnimplementedUsersServiceServer
// for forward compatibility.
//
// UsersService exposes user-related functionality over gRPC.
type UsersServiceServer interface {
// RetrieveUsers fetches users for the provided IDs.
RetrieveUsers(context.Context, *RetrieveUsersReq) (*RetrieveUsersRes, error)
mustEmbedUnimplementedUsersServiceServer()
}
// UnimplementedUsersServiceServer 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 UnimplementedUsersServiceServer struct{}
func (UnimplementedUsersServiceServer) RetrieveUsers(context.Context, *RetrieveUsersReq) (*RetrieveUsersRes, error) {
return nil, status.Error(codes.Unimplemented, "method RetrieveUsers not implemented")
}
func (UnimplementedUsersServiceServer) mustEmbedUnimplementedUsersServiceServer() {}
func (UnimplementedUsersServiceServer) testEmbeddedByValue() {}
// UnsafeUsersServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to UsersServiceServer will
// result in compilation errors.
type UnsafeUsersServiceServer interface {
mustEmbedUnimplementedUsersServiceServer()
}
func RegisterUsersServiceServer(s grpc.ServiceRegistrar, srv UsersServiceServer) {
// If the following call panics, it indicates UnimplementedUsersServiceServer 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(&UsersService_ServiceDesc, srv)
}
func _UsersService_RetrieveUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RetrieveUsersReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UsersServiceServer).RetrieveUsers(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UsersService_RetrieveUsers_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UsersServiceServer).RetrieveUsers(ctx, req.(*RetrieveUsersReq))
}
return interceptor(ctx, in, info, handler)
}
// UsersService_ServiceDesc is the grpc.ServiceDesc for UsersService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var UsersService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "users.v1.UsersService",
HandlerType: (*UsersServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "RetrieveUsers",
Handler: _UsersService_RetrieveUsers_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "users/v1/users.proto",
}
+38 -139
View File
@@ -16,7 +16,6 @@ import (
"github.com/absmach/supermq/clients"
"github.com/absmach/supermq/groups"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/absmach/supermq/users"
"github.com/gofrs/uuid/v5"
)
@@ -38,6 +37,7 @@ const (
MetadataKey = "metadata"
NameKey = "name"
TagKey = "tag"
TagsKey = "tags"
StatusKey = "status"
ClientKey = "client"
@@ -184,148 +184,47 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
return
}
var wrapper error
if errors.Contains(err, apiutil.ErrValidation) {
wrapper, err = errors.Unwrap(err)
}
switch {
case errors.Contains(err, errors.ErrTryAgain):
w.WriteHeader(http.StatusUnprocessableEntity)
case errors.Contains(err, errors.ErrEmailAlreadyExists),
errors.Contains(err, errors.ErrUsernameNotAvailable),
errors.Contains(err, errors.ErrRouteNotAvailable),
errors.Contains(err, errors.ErrChannelRouteNotAvailable),
errors.Contains(err, errors.ErrDomainRouteNotAvailable),
errors.Contains(err, svcerr.ErrExternalAuthProviderCouldNotChangePassword),
errors.Contains(err, svcerr.ErrExternalAuthProviderCouldNotResetPassword),
errors.Contains(err, svcerr.ErrExternalAuthProviderCouldNotUpdate):
switch retErr := err.(type) {
case *errors.RequestError:
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(err, svcerr.ErrAuthorization),
errors.Contains(err, svcerr.ErrDomainAuthorization),
errors.Contains(err, svcerr.ErrUnauthorizedPAT),
errors.Contains(err, svcerr.ErrSuperAdminAction):
err = unwrap(err)
w.WriteHeader(http.StatusForbidden)
case errors.Contains(err, svcerr.ErrAuthentication),
errors.Contains(err, apiutil.ErrBearerToken),
errors.Contains(err, svcerr.ErrLogin),
errors.Contains(err, apiutil.ErrUnsupportedTokenType):
err = unwrap(err)
if err := json.NewEncoder(w).Encode(retErr); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
return
case *errors.AuthNError:
w.WriteHeader(http.StatusUnauthorized)
case errors.Contains(err, svcerr.ErrMalformedEntity),
errors.Contains(err, apiutil.ErrMalformedPolicy),
errors.Contains(err, apiutil.ErrMissingSecret),
errors.Contains(err, errors.ErrMalformedEntity),
errors.Contains(err, apiutil.ErrMissingID),
errors.Contains(err, apiutil.ErrInvalidVerification),
errors.Contains(err, apiutil.ErrMissingName),
errors.Contains(err, apiutil.ErrMissingEmail),
errors.Contains(err, apiutil.ErrInvalidEmail),
errors.Contains(err, apiutil.ErrMissingHost),
errors.Contains(err, apiutil.ErrInvalidResetPass),
errors.Contains(err, apiutil.ErrEmptyList),
errors.Contains(err, apiutil.ErrMissingMemberKind),
errors.Contains(err, apiutil.ErrMissingMemberType),
errors.Contains(err, apiutil.ErrLimitSize),
errors.Contains(err, apiutil.ErrBearerKey),
errors.Contains(err, svcerr.ErrInvalidStatus),
errors.Contains(err, apiutil.ErrNameSize),
errors.Contains(err, apiutil.ErrInvalidIDFormat),
errors.Contains(err, apiutil.ErrInvalidQueryParams),
errors.Contains(err, apiutil.ErrMissingRelation),
errors.Contains(err, apiutil.ErrValidation),
errors.Contains(err, apiutil.ErrMissingPass),
errors.Contains(err, apiutil.ErrMissingConfPass),
errors.Contains(err, apiutil.ErrPasswordFormat),
errors.Contains(err, svcerr.ErrInvalidRole),
errors.Contains(err, svcerr.ErrInvalidPolicy),
errors.Contains(err, apiutil.ErrInvitationState),
errors.Contains(err, apiutil.ErrInvalidAPIKey),
errors.Contains(err, svcerr.ErrViewEntity),
errors.Contains(err, apiutil.ErrMissingCertData),
errors.Contains(err, apiutil.ErrInvalidContact),
errors.Contains(err, apiutil.ErrInvalidTopic),
errors.Contains(err, apiutil.ErrInvalidCertData),
errors.Contains(err, apiutil.ErrEmptyMessage),
errors.Contains(err, apiutil.ErrInvalidLevel),
errors.Contains(err, apiutil.ErrInvalidDirection),
errors.Contains(err, apiutil.ErrInvalidEntityType),
errors.Contains(err, apiutil.ErrMissingEntityType),
errors.Contains(err, apiutil.ErrInvalidTimeFormat),
errors.Contains(err, svcerr.ErrSearch),
errors.Contains(err, apiutil.ErrEmptySearchQuery),
errors.Contains(err, apiutil.ErrLenSearchQuery),
errors.Contains(err, apiutil.ErrMissingDomainID),
errors.Contains(err, apiutil.ErrMissingUserID),
errors.Contains(err, apiutil.ErrMissingPATID),
errors.Contains(err, apiutil.ErrMissingUsername),
errors.Contains(err, apiutil.ErrMissingUsernameEmail),
errors.Contains(err, apiutil.ErrMissingFirstName),
errors.Contains(err, apiutil.ErrMissingLastName),
errors.Contains(err, apiutil.ErrInvalidUsername),
errors.Contains(err, apiutil.ErrMissingIdentity),
errors.Contains(err, apiutil.ErrInvalidProfilePictureURL),
errors.Contains(err, apiutil.ErrSelfParentingNotAllowed),
errors.Contains(err, apiutil.ErrMissingChildrenGroupIDs),
errors.Contains(err, apiutil.ErrMissingParentGroupID),
errors.Contains(err, apiutil.ErrMissingConnectionType),
errors.Contains(err, apiutil.ErrMissingRoleName),
errors.Contains(err, apiutil.ErrMissingRoleID),
errors.Contains(err, apiutil.ErrMissingPolicyEntityType),
errors.Contains(err, apiutil.ErrMissingRoleMembers),
errors.Contains(err, apiutil.ErrMissingDescription),
errors.Contains(err, apiutil.ErrMissingEntityID),
errors.Contains(err, apiutil.ErrInvalidRouteFormat),
errors.Contains(err, svcerr.ErrRetainOneMember),
errors.Contains(err, apiutil.ErrMissingRoute):
err = unwrap(err)
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(err, svcerr.ErrCreateEntity),
errors.Contains(err, svcerr.ErrUpdateEntity),
errors.Contains(err, svcerr.ErrRemoveEntity),
errors.Contains(err, svcerr.ErrEnableClient),
errors.Contains(err, svcerr.ErrEnableUser),
errors.Contains(err, svcerr.ErrDisableUser):
err = unwrap(err)
w.WriteHeader(http.StatusUnprocessableEntity)
case errors.Contains(err, svcerr.ErrNotFound):
err = unwrap(err)
w.WriteHeader(http.StatusNotFound)
case errors.Contains(err, errors.ErrStatusAlreadyAssigned),
errors.Contains(err, svcerr.ErrInvitationAlreadyRejected),
errors.Contains(err, svcerr.ErrInvitationAlreadyAccepted),
errors.Contains(err, svcerr.ErrConflict):
err = unwrap(err)
w.WriteHeader(http.StatusConflict)
case errors.Contains(err, apiutil.ErrUnsupportedContentType):
err = unwrap(err)
if err := json.NewEncoder(w).Encode(retErr); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
return
case *errors.AuthZError:
w.WriteHeader(http.StatusForbidden)
if err := json.NewEncoder(w).Encode(retErr); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
return
case *errors.MediaTypeError:
w.WriteHeader(http.StatusUnsupportedMediaType)
if err := json.NewEncoder(w).Encode(retErr); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
return
case *errors.ServiceError:
w.WriteHeader(http.StatusUnprocessableEntity)
if err := json.NewEncoder(w).Encode(retErr); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
return
case *errors.NotFoundError:
w.WriteHeader(http.StatusNotFound)
if err := json.NewEncoder(w).Encode(retErr); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
return
case *errors.InternalError:
w.WriteHeader(http.StatusInternalServerError)
return
default:
w.WriteHeader(http.StatusInternalServerError)
}
if wrapper != nil {
err = errors.Wrap(wrapper, err)
}
if errorVal, ok := err.(errors.Error); ok {
if err := json.NewEncoder(w).Encode(errorVal); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
}
}
func unwrap(err error) error {
wrapper, err := errors.Unwrap(err)
if wrapper != nil {
return wrapper
}
return err
}
+75 -92
View File
@@ -218,121 +218,104 @@ func TestEncodeResponse(t *testing.T) {
func TestEncodeError(t *testing.T) {
cases := []struct {
desc string
errs []error
code int
desc string
err error
code int
hasBody bool
checkError bool
}{
{
desc: "BadRequest",
errs: []error{
apiutil.ErrMissingSecret,
svcerr.ErrMalformedEntity,
errors.ErrMalformedEntity,
apiutil.ErrMissingID,
apiutil.ErrEmptyList,
apiutil.ErrMissingMemberType,
apiutil.ErrMissingMemberKind,
apiutil.ErrLimitSize,
apiutil.ErrNameSize,
svcerr.ErrViewEntity,
},
code: http.StatusBadRequest,
desc: "RequestError - Missing Secret",
err: apiutil.ErrMissingSecret,
code: http.StatusBadRequest,
hasBody: true,
},
{
desc: "BadRequest with validation error",
errs: []error{
errors.Wrap(apiutil.ErrValidation, apiutil.ErrMissingSecret),
errors.Wrap(apiutil.ErrValidation, svcerr.ErrMalformedEntity),
errors.Wrap(apiutil.ErrValidation, errors.ErrMalformedEntity),
errors.Wrap(apiutil.ErrValidation, apiutil.ErrMissingID),
errors.Wrap(apiutil.ErrValidation, apiutil.ErrEmptyList),
errors.Wrap(apiutil.ErrValidation, apiutil.ErrMissingMemberType),
errors.Wrap(apiutil.ErrValidation, apiutil.ErrMissingMemberKind),
errors.Wrap(apiutil.ErrValidation, apiutil.ErrLimitSize),
errors.Wrap(apiutil.ErrValidation, apiutil.ErrNameSize),
},
code: http.StatusBadRequest,
desc: "RequestError - Missing ID",
err: apiutil.ErrMissingID,
code: http.StatusBadRequest,
hasBody: true,
},
{
desc: "Unauthorized",
errs: []error{
svcerr.ErrAuthentication,
svcerr.ErrAuthentication,
apiutil.ErrBearerToken,
},
code: http.StatusUnauthorized,
},
{
desc: "NotFound",
errs: []error{
svcerr.ErrNotFound,
},
code: http.StatusNotFound,
desc: "RequestError - Empty List",
err: apiutil.ErrEmptyList,
code: http.StatusBadRequest,
hasBody: true,
},
{
desc: "Conflict",
errs: []error{
svcerr.ErrConflict,
svcerr.ErrConflict,
},
code: http.StatusConflict,
desc: "RequestError - Conflict",
err: svcerr.ErrConflict,
code: http.StatusBadRequest,
hasBody: true,
},
{
desc: "Forbidden",
errs: []error{
svcerr.ErrAuthorization,
svcerr.ErrAuthorization,
svcerr.ErrDomainAuthorization,
},
code: http.StatusForbidden,
desc: "NotFoundError - Not Found",
err: svcerr.ErrNotFound,
code: http.StatusNotFound,
hasBody: true,
},
{
desc: "UnsupportedMediaType",
errs: []error{
apiutil.ErrUnsupportedContentType,
},
code: http.StatusUnsupportedMediaType,
desc: "AuthNError - Authentication Failed",
err: svcerr.ErrAuthentication,
code: http.StatusUnauthorized,
hasBody: true,
},
{
desc: "StatusUnprocessableEntity",
errs: []error{
svcerr.ErrCreateEntity,
svcerr.ErrUpdateEntity,
svcerr.ErrRemoveEntity,
},
code: http.StatusUnprocessableEntity,
desc: "AuthZError - Authorization Failed",
err: svcerr.ErrAuthorization,
code: http.StatusForbidden,
hasBody: true,
},
{
desc: "InternalServerError",
errs: []error{
errors.New("test"),
},
code: http.StatusInternalServerError,
desc: "AuthZError - Domain Authorization Failed",
err: svcerr.ErrDomainAuthorization,
code: http.StatusForbidden,
hasBody: true,
},
{
desc: "MediaTypeError - Unsupported Content Type",
err: apiutil.ErrUnsupportedContentType,
code: http.StatusUnsupportedMediaType,
hasBody: true,
},
{
desc: "ServiceError - Create Entity Failed",
err: svcerr.ErrCreateEntity,
code: http.StatusUnprocessableEntity,
hasBody: true,
},
{
desc: "ServiceError - Update Entity Failed",
err: svcerr.ErrUpdateEntity,
code: http.StatusUnprocessableEntity,
hasBody: true,
},
{
desc: "ServiceError - Remove Entity Failed",
err: svcerr.ErrRemoveEntity,
code: http.StatusUnprocessableEntity,
hasBody: true,
},
{
desc: "InternalError",
err: errors.NewInternalError(),
code: http.StatusInternalServerError,
hasBody: false,
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
responseWriter := newResponseWriter()
for _, err := range c.errs {
api.EncodeError(context.Background(), err, responseWriter)
assert.Equal(t, c.code, responseWriter.StatusCode())
message := body{}
jerr := json.Unmarshal(responseWriter.Body(), &message)
assert.NoError(t, jerr)
var wrapper error
switch errors.Contains(err, apiutil.ErrValidation) {
case true:
wrapper, err = errors.Unwrap(err)
assert.Equal(t, err.Error(), message.Error)
assert.Equal(t, wrapper.Error(), message.Message)
case false:
assert.Equal(t, err.Error(), message.Message)
}
api.EncodeError(context.Background(), c.err, responseWriter)
assert.Equal(t, c.code, responseWriter.StatusCode())
if !c.hasBody {
return
}
message := body{}
jerr := json.Unmarshal(responseWriter.Body(), &message)
assert.NoError(t, jerr)
assert.NotEmpty(t, message.Message)
})
}
}
+91 -88
View File
@@ -10,268 +10,271 @@ import "github.com/absmach/supermq/pkg/errors"
// errors are logged twice.
var (
// ErrValidation indicates that an error was returned by the API.
ErrValidation = errors.New("something went wrong with the request")
ErrValidation = errors.NewRequestError("something went wrong with the request")
// ErrBearerToken indicates missing or invalid bearer user token.
ErrBearerToken = errors.New("missing or invalid bearer user token")
ErrBearerToken = errors.NewAuthNError("missing or invalid bearer user token")
// ErrBearerKey indicates missing or invalid bearer entity key.
ErrBearerKey = errors.New("missing or invalid bearer entity key")
ErrBearerKey = errors.NewAuthNError("missing or invalid bearer entity key")
// ErrMissingID indicates missing entity ID.
ErrMissingID = errors.New("missing entity id")
ErrMissingID = errors.NewRequestError("missing entity id")
// ErrMissingEntityID indicates missing entity ID.
ErrMissingEntityID = errors.New("missing entity id")
ErrMissingEntityID = errors.NewRequestError("missing entity id")
// ErrMissingClientID indicates missing client ID.
ErrMissingClientID = errors.New("missing cient id")
ErrMissingClientID = errors.NewRequestError("missing client id")
// ErrMissingChannelID indicates missing client ID.
ErrMissingChannelID = errors.New("missing channel id")
ErrMissingChannelID = errors.NewRequestError("missing channel id")
// ErrMissingConnectionType indicates missing connection tpye.
ErrMissingConnectionType = errors.New("missing connection type")
ErrMissingConnectionType = errors.NewRequestError("missing connection type")
// ErrMissingParentGroupID indicates missing parent group ID.
ErrMissingParentGroupID = errors.New("missing parent group id")
ErrMissingParentGroupID = errors.NewRequestError("missing parent group id")
// ErrMissingChildrenGroupIDs indicates missing children group IDs.
ErrMissingChildrenGroupIDs = errors.New("missing children group ids")
ErrMissingChildrenGroupIDs = errors.NewRequestError("missing children group ids")
// ErrSelfParentingNotAllowed indicates child id is same as parent id.
ErrSelfParentingNotAllowed = errors.New("self parenting not allowed")
ErrSelfParentingNotAllowed = errors.NewRequestError("self parenting not allowed")
// ErrInvalidChildGroupID indicates invalid child group ID.
ErrInvalidChildGroupID = errors.New("invalid child group id")
ErrInvalidChildGroupID = errors.NewRequestError("invalid child group id")
// ErrInvalidAuthKey indicates invalid auth key.
ErrInvalidAuthKey = errors.New("invalid auth key")
// ErrInvalidIDFormat indicates an invalid ID format.
ErrInvalidIDFormat = errors.New("invalid id format provided")
ErrInvalidIDFormat = errors.NewRequestError("invalid id format provided")
// ErrNameSize indicates that name size exceeds the max.
ErrNameSize = errors.New("invalid name size")
ErrNameSize = errors.NewRequestError("invalid name size")
// ErrEmailSize indicates that email size exceeds the max.
ErrEmailSize = errors.New("invalid email size")
ErrEmailSize = errors.NewRequestError("invalid email size")
// ErrInvalidRole indicates that an invalid role.
ErrInvalidRole = errors.New("invalid client role")
ErrInvalidRole = errors.NewRequestError("invalid client role")
// ErrLimitSize indicates that an invalid limit.
ErrLimitSize = errors.New("invalid limit size")
ErrLimitSize = errors.NewRequestError("invalid limit size")
// ErrLevel indicates that an invalid level.
ErrLevel = errors.New("invalid level")
ErrLevel = errors.NewRequestError("invalid level")
// ErrOffsetSize indicates an invalid offset.
ErrOffsetSize = errors.New("invalid offset size")
ErrOffsetSize = errors.NewRequestError("invalid offset size")
// ErrInvalidOrder indicates an invalid list order.
ErrInvalidOrder = errors.New("invalid list order provided")
ErrInvalidOrder = errors.NewRequestError("invalid list order provided")
// ErrInvalidDirection indicates an invalid list direction.
ErrInvalidDirection = errors.New("invalid list direction provided")
ErrInvalidDirection = errors.NewRequestError("invalid list direction provided")
// ErrInvalidMemberKind indicates an invalid member kind.
ErrInvalidMemberKind = errors.New("invalid member kind")
ErrInvalidMemberKind = errors.NewRequestError("invalid member kind")
// ErrEmptyList indicates that entity data is empty.
ErrEmptyList = errors.New("empty list provided")
ErrEmptyList = errors.NewRequestError("empty list provided")
// ErrMissingRoleName indicates that role name is empty.
ErrMissingRoleName = errors.New("empty role name")
ErrMissingRoleName = errors.NewRequestError("empty role name")
// ErrMissingRoleID indicates that role id is empty.
ErrMissingRoleID = errors.New("empty role id")
ErrMissingRoleID = errors.NewRequestError("empty role id")
// ErrMissingRoleOperations indicates that role operations are empty.
ErrMissingRoleOperations = errors.New("empty role operations")
ErrMissingRoleOperations = errors.NewRequestError("empty role operations")
// ErrMissingRoleMembers indicates that role members are empty.
ErrMissingRoleMembers = errors.New("empty role members")
ErrMissingRoleMembers = errors.NewRequestError("empty role members")
// ErrMalformedPolicy indicates that policies are malformed.
ErrMalformedPolicy = errors.New("malformed policy")
ErrMalformedPolicy = errors.NewRequestError("malformed policy")
// ErrMissingPolicySub indicates that policies are subject.
ErrMissingPolicySub = errors.New("malformed policy subject")
ErrMissingPolicySub = errors.NewRequestError("malformed policy subject")
// ErrMissingPolicyObj indicates missing policies object.
ErrMissingPolicyObj = errors.New("malformed policy object")
ErrMissingPolicyObj = errors.NewRequestError("malformed policy object")
// ErrMalformedPolicyAct indicates missing policies action.
ErrMalformedPolicyAct = errors.New("malformed policy action")
ErrMalformedPolicyAct = errors.NewRequestError("malformed policy action")
// ErrMissingPolicyEntityType indicates missing policies entity type.
ErrMissingPolicyEntityType = errors.New("missing policy entity type")
ErrMissingPolicyEntityType = errors.NewRequestError("missing policy entity type")
// ErrMalformedPolicyPer indicates missing policies relation.
ErrMalformedPolicyPer = errors.New("malformed policy permission")
ErrMalformedPolicyPer = errors.NewRequestError("malformed policy permission")
// ErrMissingCertData indicates missing cert data (ttl).
ErrMissingCertData = errors.New("missing certificate data")
ErrMissingCertData = errors.NewRequestError("missing certificate data")
// ErrInvalidCertData indicates invalid cert data (ttl).
ErrInvalidCertData = errors.New("invalid certificate data")
ErrInvalidCertData = errors.NewRequestError("invalid certificate data")
// ErrInvalidTopic indicates an invalid subscription topic.
ErrInvalidTopic = errors.New("invalid Subscription topic")
ErrInvalidTopic = errors.NewRequestError("invalid Subscription topic")
// ErrInvalidContact indicates an invalid subscription contract.
ErrInvalidContact = errors.New("invalid Subscription contact")
ErrInvalidContact = errors.NewRequestError("invalid Subscription contact")
// ErrMissingEmail indicates missing email.
ErrMissingEmail = errors.New("missing email")
ErrMissingEmail = errors.NewRequestError("missing email")
// ErrInvalidEmail indicates missing email.
ErrInvalidEmail = errors.New("invalid email")
ErrInvalidEmail = errors.NewRequestError("invalid email")
// ErrMissingHost indicates missing host.
ErrMissingHost = errors.New("missing host")
ErrMissingHost = errors.NewRequestError("missing host")
// ErrMissingPass indicates missing password.
ErrMissingPass = errors.New("missing password")
ErrMissingPass = errors.NewRequestError("missing password")
// ErrMissingConfPass indicates missing conf password.
ErrMissingConfPass = errors.New("missing conf password")
ErrMissingConfPass = errors.NewRequestError("missing conf password")
// ErrInvalidResetPass indicates an invalid reset password.
ErrInvalidResetPass = errors.New("invalid reset password")
ErrInvalidResetPass = errors.NewRequestError("invalid reset password")
// ErrInvalidComparator indicates an invalid comparator.
ErrInvalidComparator = errors.New("invalid comparator")
ErrInvalidComparator = errors.NewRequestError("invalid comparator")
// ErrMissingMemberIDs indicates missing member ids.
ErrMissingMemberIDs = errors.New("missing member ids")
ErrMissingMemberIDs = errors.NewRequestError("missing member ids")
// ErrMissingMemberType indicates missing group member type.
ErrMissingMemberType = errors.New("missing group member type")
ErrMissingMemberType = errors.NewRequestError("missing group member type")
// ErrMissingMemberKind indicates missing group member kind.
ErrMissingMemberKind = errors.New("missing group member kind")
ErrMissingMemberKind = errors.NewRequestError("missing group member kind")
// ErrMissingRelation indicates missing relation.
ErrMissingRelation = errors.New("missing relation")
ErrMissingRelation = errors.NewRequestError("missing relation")
// ErrInvalidRelation indicates an invalid relation.
ErrInvalidRelation = errors.New("invalid relation")
ErrInvalidRelation = errors.NewRequestError("invalid relation")
// ErrInvalidAPIKey indicates an invalid API key type.
ErrInvalidAPIKey = errors.New("invalid api key type")
ErrInvalidAPIKey = errors.NewRequestError("invalid api key type")
// ErrInvitationState indicates an invalid invitation state.
ErrInvitationState = errors.New("invalid invitation state")
ErrInvitationState = errors.NewRequestError("invalid invitation state")
// ErrMissingIdentity indicates missing entity Identity.
ErrMissingIdentity = errors.New("missing entity identity")
ErrMissingIdentity = errors.NewRequestError("missing entity identity")
// ErrMissingSecret indicates missing secret.
ErrMissingSecret = errors.New("missing secret")
ErrMissingSecret = errors.NewRequestError("missing secret")
// ErrPasswordFormat indicates weak password.
ErrPasswordFormat = errors.New("password does not meet the requirements")
ErrPasswordFormat = errors.NewRequestError("password does not meet the requirements")
// ErrMissingName indicates missing identity name.
ErrMissingName = errors.New("missing identity name")
ErrMissingName = errors.NewRequestError("missing identity name")
// ErrMissingRoute indicates missing route.
ErrMissingRoute = errors.New("missing route")
ErrMissingRoute = errors.NewRequestError("missing route")
// ErrInvalidLevel indicates an invalid group level.
ErrInvalidLevel = errors.New("invalid group level (should be between 0 and 5)")
ErrInvalidLevel = errors.NewRequestError("invalid group level (should be between 0 and 5)")
// ErrNotFoundParam indicates that the parameter was not found in the query.
ErrNotFoundParam = errors.New("parameter not found in the query")
ErrNotFoundParam = errors.NewRequestError("parameter not found in the query")
// ErrInvalidQueryParams indicates invalid query parameters.
ErrInvalidQueryParams = errors.New("invalid query parameters")
ErrInvalidQueryParams = errors.NewRequestError("invalid query parameters")
// ErrInvalidVisibilityType indicates invalid visibility type.
ErrInvalidVisibilityType = errors.New("invalid visibility type")
ErrInvalidVisibilityType = errors.NewRequestError("invalid visibility type")
// ErrUnsupportedContentType indicates unacceptable or lack of Content-Type.
ErrUnsupportedContentType = errors.New("unsupported content type")
ErrUnsupportedContentType = errors.NewMediaTypeError("unsupported content type")
// ErrRollbackTx indicates failed to rollback transaction.
ErrRollbackTx = errors.New("failed to rollback transaction")
ErrRollbackTx = errors.NewRequestError("failed to rollback transaction")
// ErrInvalidAggregation indicates invalid aggregation value.
ErrInvalidAggregation = errors.New("invalid aggregation value")
ErrInvalidAggregation = errors.NewRequestError("invalid aggregation value")
// ErrInvalidInterval indicates invalid interval value.
ErrInvalidInterval = errors.New("invalid interval value")
ErrInvalidInterval = errors.NewRequestError("invalid interval value")
// ErrMissingFrom indicates missing from value.
ErrMissingFrom = errors.New("missing from time value")
ErrMissingFrom = errors.NewRequestError("missing from time value")
// ErrMissingTo indicates missing to value.
ErrMissingTo = errors.New("missing to time value")
ErrMissingTo = errors.NewRequestError("missing to time value")
// ErrEmptyMessage indicates empty message.
ErrEmptyMessage = errors.New("empty message")
ErrEmptyMessage = errors.NewRequestError("empty message")
// ErrMissingEntityType indicates missing entity type.
ErrMissingEntityType = errors.New("missing entity type")
ErrMissingEntityType = errors.NewRequestError("missing entity type")
// ErrInvalidEntityType indicates invalid entity type.
ErrInvalidEntityType = errors.New("invalid entity type")
ErrInvalidEntityType = errors.NewRequestError("invalid entity type")
// ErrInvalidTimeFormat indicates invalid time format i.e not unix time.
ErrInvalidTimeFormat = errors.New("invalid time format use unix time")
ErrInvalidTimeFormat = errors.NewRequestError("invalid time format use unix time")
// ErrEmptySearchQuery indicates search query should not be empty.
ErrEmptySearchQuery = errors.New("search query must not be empty")
ErrEmptySearchQuery = errors.NewRequestError("search query must not be empty")
// ErrLenSearchQuery indicates search query length.
ErrLenSearchQuery = errors.New("search query must be at least 3 characters")
ErrLenSearchQuery = errors.NewRequestError("search query must be at least 3 characters")
// ErrMissingDomainID indicates missing domainID.
ErrMissingDomainID = errors.New("missing domainID")
ErrMissingDomainID = errors.NewRequestError("missing domainID")
// ErrMissingUsername indicates missing user name.
ErrMissingUsername = errors.New("missing username")
ErrMissingUsername = errors.NewRequestError("missing username")
// ErrInvalidUsername indicates invalid user name.
ErrInvalidUsername = errors.New("invalid username")
ErrInvalidUsername = errors.NewRequestError("invalid username")
// ErrMissingFirstName indicates missing first name.
ErrMissingFirstName = errors.New("missing first name")
ErrMissingFirstName = errors.NewRequestError("missing first name")
// ErrMissingLastName indicates missing last name.
ErrMissingLastName = errors.New("missing last name")
ErrMissingLastName = errors.NewRequestError("missing last name")
// ErrInvalidProfilePictureURL indicates that the profile picture url is invalid.
ErrInvalidProfilePictureURL = errors.New("invalid profile picture url")
ErrInvalidProfilePictureURL = errors.NewRequestError("invalid profile picture url")
ErrMultipleEntitiesFilter = errors.New("multiple entities are provided in filter are not supported")
ErrMultipleEntitiesFilter = errors.NewRequestError("multiple entities are provided in filter are not supported")
// ErrMissingDescription indicates missing description.
ErrMissingDescription = errors.New("missing description")
ErrMissingDescription = errors.NewRequestError("missing description")
// ErrUnsupportedTokenType indicates that this type of token is not supported.
ErrUnsupportedTokenType = errors.New("unsupported content token type")
ErrUnsupportedTokenType = errors.NewRequestError("unsupported content token type")
// ErrMissingUserID indicates missing user ID.
ErrMissingUserID = errors.New("missing user id")
ErrMissingUserID = errors.NewRequestError("missing user id")
// ErrMissingPATID indicates missing pat ID.
ErrMissingPATID = errors.New("missing pat id")
ErrMissingPATID = errors.NewRequestError("missing pat id")
// ErrInvalidNameFormat indicates invalid name format.
ErrInvalidNameFormat = errors.New("invalid name format")
ErrInvalidNameFormat = errors.NewRequestError("invalid name format")
// ErrInvalidRouteFormat indicates invalid route format.
ErrInvalidRouteFormat = errors.New("invalid route format")
ErrInvalidRouteFormat = errors.NewRequestError("invalid route format")
// ErrMissingUsernameEmail indicates missing user name / email.
ErrMissingUsernameEmail = errors.New("missing username / email")
ErrMissingUsernameEmail = errors.NewRequestError("missing username / email")
// ErrInvalidVerification indicates invalid email verification.
ErrInvalidVerification = errors.New("invalid verification")
ErrInvalidVerification = errors.NewRequestError("invalid verification")
// ErrEmailNotVerified indicates invalid email not verified.
ErrEmailNotVerified = errors.New("email not verified")
ErrEmailNotVerified = errors.NewRequestError("email not verified")
// ErrMalformedRequest indicates malformed request body.
ErrMalformedRequestBody = errors.NewRequestError("request body is not a valid JSON, expecting a valid JSON")
)
+9 -4
View File
@@ -8,11 +8,16 @@ import (
"strings"
)
// BearerPrefix represents the token prefix for Bearer authentication scheme.
const BearerPrefix = "Bearer "
const (
// BearerPrefix represents the token prefix for Bearer authentication scheme.
BearerPrefix = "Bearer "
// ClientPrefix represents the key prefix for Client authentication scheme.
const ClientPrefix = "Client "
// ClientPrefix represents the key prefix for Client authentication scheme.
ClientPrefix = "Client "
// BasicAuthPrefix represents the prefix for Basic authentication scheme.
BasicAuthPrefix = "Basic "
)
// ExtractBearerToken returns value of the bearer token. If there is no bearer token - an empty value is returned.
func ExtractBearerToken(r *http.Request) string {
+1 -1
View File
@@ -9,7 +9,7 @@ info:
contact:
name: SuperMQ Team
url: 'https://github.com/absmach/supermq'
email: info@abstractmachines.fr
email: info@absmach.eu
description: |
MQTT adapter provides an MQTT API for sending messages through the platform. MQTT adapter uses [mProxy](https://github.com/absmach/mproxy) for proxying traffic between client and MQTT broker.
Additionally, the MQTT adapter and the message broker are replicating the traffic between brokers.
+2 -2
View File
@@ -10,7 +10,7 @@ info:
contact:
name: SuperMQ Team
url: 'https://github.com/absmach/supermq'
email: info@abstractmachines.fr
email: info@absmach.eu
license:
name: Apache 2.0
url: 'https://github.com/absmach/supermq/blob/main/LICENSE'
@@ -29,7 +29,7 @@ servers:
default: localhost
port:
description: SuperMQ WebSocket Adapter port
default: '8186'
default: '8008'
channels:
'm/{domainPrefix}/c/{channelPrefix}/{subtopic}':
+1 -1
View File
@@ -2,4 +2,4 @@
This folder contains an OpenAPI specifications for SuperMQ API.
View specification in Swagger UI at [docs.api.supermq.abstractmachines.fr](https://docs.api.supermq.abstractmachines.fr)
View specification in Swagger UI at [docs.api.supermq.absmach.eu](https://docs.api.supermq.absmach.eu)
+50 -39
View File
@@ -9,7 +9,7 @@ info:
Some useful links:
- [The SuperMQ repository](https://github.com/absmach/supermq)
contact:
email: info@abstractmachines.fr
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/supermq/blob/main/LICENSE
@@ -24,17 +24,17 @@ tags:
description: Everything about your Keys.
externalDocs:
description: Find out more about keys
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
- name: PATs
description: Everything about your Personal Access Tokens.
externalDocs:
description: Find out more about Personal Access Tokens
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
- name: Health
description: Service health check endpoint.
externalDocs:
description: Find out more about health check
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
paths:
/keys:
@@ -81,6 +81,8 @@ paths:
description: Missing or invalid access token provided.
"404":
description: A non-existent entity request.
"422":
description: Service can't process request.
"500":
$ref: "#/components/responses/ServiceError"
@@ -100,6 +102,8 @@ paths:
description: Missing or invalid access token provided.
"404":
description: A non-existent entity request.
"422":
description: Service can't process request.
"500":
$ref: "#/components/responses/ServiceError"
@@ -124,7 +128,7 @@ paths:
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
get:
operationId: listPATs
tags:
@@ -144,7 +148,7 @@ paths:
description: Missing or invalid access token provided.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: clearAllPATs
tags:
@@ -159,7 +163,7 @@ paths:
description: Missing or invalid access token provided.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}:
get:
operationId: retrievePAT
@@ -181,7 +185,7 @@ paths:
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: deletePAT
tags:
@@ -200,7 +204,7 @@ paths:
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/name:
patch:
operationId: updatePATName
@@ -226,7 +230,7 @@ paths:
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/description:
patch:
operationId: updatePATDescription
@@ -252,7 +256,7 @@ paths:
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/secret/reset:
patch:
operationId: resetPATSecret
@@ -278,7 +282,7 @@ paths:
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/secret/revoke:
patch:
operationId: revokePATSecret
@@ -298,7 +302,7 @@ paths:
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/scope:
get:
operationId: listScopes
@@ -322,7 +326,7 @@ paths:
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: clearAllScopes
tags:
@@ -341,7 +345,7 @@ paths:
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/scope/add:
patch:
operationId: addScope
@@ -369,7 +373,7 @@ paths:
description: Database cannot process the request.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/scope/remove:
patch:
operationId: removeScope
@@ -396,7 +400,6 @@ paths:
"500":
$ref: "#/components/responses/ServiceError"
/health:
get:
summary: Retrieves service health check info.
@@ -465,7 +468,7 @@ components:
format: date-time
example: "2019-11-26T13:31:52Z"
description: Time when the PAT was revoked
PATsPage:
type: object
properties:
@@ -486,20 +489,10 @@ components:
items:
$ref: "#/components/schemas/PAT"
description: List of Personal Access Tokens
Scope:
type: object
properties:
id:
type: string
format: uuid
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
description: Scope unique identifier
pat_id:
type: string
format: uuid
example: "9118de62-c680-46b7-ad0a-21748a52833a"
description: PAT ID this scope belongs to
optional_domain_id:
type: string
format: uuid
@@ -507,7 +500,8 @@ components:
description: Optional domain ID for the scope
entity_type:
type: string
enum: [groups, channels, clients, domains, users, dashboards, messages]
enum:
[groups, channels, clients, domains, users, dashboards, messages]
example: "groups"
description: Type of entity the scope applies to
entity_id:
@@ -516,10 +510,21 @@ components:
description: ID of the entity the scope applies to. '*' means all entities of the specified type.
operation:
type: string
enum: [create, read, list, update, delete, share, unshare, publish, subscribe]
enum:
[
create,
read,
list,
update,
delete,
share,
unshare,
publish,
subscribe,
]
example: "read"
description: Operation allowed by this scope
ScopesPage:
type: object
properties:
@@ -679,9 +684,11 @@ components:
type: object
required:
- name
- duration
properties:
name:
type: string
minLength: 1
example: "My PAT"
description: Name of the Personal Access Token
description:
@@ -690,9 +697,10 @@ components:
description: Description of the Personal Access Token
duration:
type: string
pattern: "^[0-9]+(ns|us|µs|ms|s|m|h|d|w|y)$"
example: "30d"
description: Duration for which the PAT is valid. Format is a duration string (e.g. "30d", "24h", "1y").
UpdatePATNameRequest:
description: JSON-formatted document describing PAT name update request.
required: true
@@ -707,7 +715,7 @@ components:
type: string
example: "New PAT Name"
description: New name for the Personal Access Token
UpdatePATDescriptionRequest:
description: JSON-formatted document describing PAT description update request.
required: true
@@ -722,7 +730,7 @@ components:
type: string
example: "New PAT Description"
description: New description for the Personal Access Token
ResetPATSecretRequest:
description: JSON-formatted document describing PAT secret reset request.
required: true
@@ -730,12 +738,15 @@ components:
application/json:
schema:
type: object
required:
- duration
properties:
duration:
type: string
pattern: "^[0-9]+(ns|us|µs|ms|s|m|h|d|w|y)$"
example: "30d"
description: Duration for which the new PAT secret is valid. Format is a duration string (e.g. "30d", "24h", "1y").
AddScopeRequest:
description: JSON-formatted document describing add scope request.
required: true
@@ -751,7 +762,7 @@ components:
items:
$ref: "#/components/schemas/Scope"
description: List of scopes to add
RemoveScopeRequest:
description: JSON-formatted document describing remove scope request.
required: true
@@ -793,14 +804,14 @@ components:
application/json:
schema:
$ref: "#/components/schemas/PAT"
PATsPageRes:
description: Page of Personal Access Tokens.
content:
application/json:
schema:
$ref: "#/components/schemas/PATsPage"
ScopesPageRes:
description: Page of scopes.
content:
+41 -19
View File
@@ -9,7 +9,7 @@ info:
Some useful links:
- [The SuperMQ repository](https://github.com/absmach/supermq)
contact:
email: info@abstractmachines.fr
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/supermq/blob/main/LICENSE
@@ -24,17 +24,17 @@ tags:
description: CRUD operations for your channels
externalDocs:
description: Find out more about channels
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
- name: Connections
description: All operations involving channel and client connections
externalDocs:
description: Find out more about channel and client connections
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
- name: Health
description: Health check operations
externalDocs:
description: Find out more about health checks
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
paths:
/{domainID}/channels:
@@ -89,6 +89,7 @@ paths:
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/Order"
- $ref: "#/components/parameters/Direction"
- $ref: "#/components/parameters/Tags"
- $ref: "#/components/parameters/Metadata"
- $ref: "#/components/parameters/Status"
- $ref: "#/components/parameters/ChannelName"
@@ -101,6 +102,8 @@ paths:
- $ref: "#/components/parameters/Client"
- $ref: "#/components/parameters/Group"
- $ref: "#/components/parameters/User"
- $ref: "#/components/parameters/CreatedFrom"
- $ref: "#/components/parameters/CreatedTo"
responses:
"200":
$ref: "#/components/responses/ChannelPageRes"
@@ -656,9 +659,7 @@ components:
example: 10
description: Maximum number of items to return in one page.
required:
- channels
- total
- offset
ChannelUpdate:
type: object
@@ -705,7 +706,10 @@ components:
type: array
description: Connection types.
items:
example: publish
type: string
enum:
- publish
- subscribe
ChannelConnectionReqSchema:
type: object
@@ -719,7 +723,10 @@ components:
type: array
description: Connection types.
items:
example: publish
type: string
enum:
- publish
- subscribe
Error:
type: object
@@ -776,16 +783,12 @@ components:
Tags:
name: tags
description: Client tags.
description: Channel tags. Multiple tags can be specified separated by comma for OR condition and plus for AND condition.
in: query
schema:
type: array
minItems: 0
uniqueItems: true
items:
type: string
type: string
required: false
example: ["yello", "orange"]
example: "orange,yellow"
ChannelName:
name: name
@@ -831,7 +834,6 @@ components:
in: query
schema:
type: string
minimum: 0
required: false
Limit:
@@ -844,7 +846,7 @@ components:
maximum: 100
minimum: 1
required: false
example: "100"
example: 100
Offset:
name: offset
@@ -855,8 +857,8 @@ components:
default: 0
minimum: 0
required: false
example: "0"
example: 0
Order:
name: order
description: Field by which to order the results
@@ -944,6 +946,26 @@ components:
required: false
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
CreatedFrom:
name: created_from
description: Filter channels created from this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-01-01T00:00:00Z"
CreatedTo:
name: created_to
description: Filter channels created up to this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-12-31T23:59:59Z"
requestBodies:
ChannelCreateReq:
description: JSON-formatted document describing the new channel to be registered
+119 -21
View File
@@ -9,7 +9,7 @@ info:
Some useful links:
- [The SuperMQ repository](https://github.com/absmach/supermq)
contact:
email: info@abstractmachines.fr
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/supermq/blob/main/LICENSE
@@ -24,17 +24,17 @@ tags:
description: CRUD operations for your clients
externalDocs:
description: Find out more about clients
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
- name: Roles
description: All operations involving roles for clients
externalDocs:
description: Find out more about roles
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
- name: Health
description: Health check operations
externalDocs:
description: Find out more about health checks
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
paths:
/{domainID}/clients:
@@ -100,6 +100,8 @@ paths:
- $ref: "#/components/parameters/ConnectionType"
- $ref: "#/components/parameters/Group"
- $ref: "#/components/parameters/User"
- $ref: "#/components/parameters/CreatedFrom"
- $ref: "#/components/parameters/CreatedTo"
security:
- bearerAuth: []
responses:
@@ -181,7 +183,7 @@ paths:
patch:
operationId: updateClient
summary: Updates name and metadata of the client.
summary: Updates name, metadata, and private metadata of the client.
description: |
Update is performed by replacing the current resource data with values
provided in a request payload. Note that the client's type and ID
@@ -323,7 +325,7 @@ paths:
- bearerAuth: []
responses:
"200":
$ref: "#/components/responses/ClientRes"
$ref: "#/components/responses/DisabledClientRes"
"400":
description: Failed due to malformed client's ID.
"401":
@@ -943,10 +945,14 @@ components:
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
minimum: 8
description: Free-form account secret used for acquiring auth token(s).
private_metadata:
type: object
example: { "model": "example" }
description: Arbitrary, object-encoded client's data private to the client.
metadata:
type: object
example: { "model": "example" }
description: Arbitrary, object-encoded client's data.
description: Arbitrary, object-encoded client's data visible to other clients.
status:
type: string
description: Client Status
@@ -1001,10 +1007,14 @@ components:
type: string
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: Client secret password.
private_metadata:
type: object
example: { "model": "example" }
description: Arbitrary, object-encoded client's data private to the client.
metadata:
type: object
example: { "model": "example" }
description: Arbitrary, object-encoded client's data.
description: Arbitrary, object-encoded client's data visible to other clients.
status:
type: string
description: Client Status
@@ -1058,10 +1068,14 @@ components:
type: string
example: ""
description: Client secret password.
private_metadata:
type: object
example: { "model": "example" }
description: Arbitrary, object-encoded client's data private to the client.
metadata:
type: object
example: { "model": "example" }
description: Arbitrary, object-encoded client's data.
description: Arbitrary, object-encoded client's data visible to other clients.
status:
type: string
description: Client Status
@@ -1113,9 +1127,14 @@ components:
metadata:
type: object
example: { "role": "general" }
description: Arbitrary, object-encoded client's data.
description: Arbitrary, object-encoded client's data visible to other clients.
private_metadata:
type: object
example: { "role": "general" }
description: Arbitrary, object-encoded client's data private to the client.
required:
- name
- private_metadata
- metadata
ClientTags:
@@ -1130,6 +1149,63 @@ components:
items:
type: string
DisabledClient:
type: object
properties:
id:
type: string
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: Client unique identifier.
name:
type: string
example: clientName
description: Client name.
tags:
type: array
minItems: 0
items:
type: string
example: ["tag1", "tag2"]
description: Client tags.
domain_id:
type: string
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: ID of the domain to which client belongs.
credentials:
type: object
properties:
identity:
type: string
example: clientIDentity
description: Client Identity for example email address.
secret:
type: string
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: Client secret password.
metadata:
type: object
example: { "model": "example" }
description: Arbitrary, object-encoded client's data visible to other clients.
status:
type: string
description: Client Status
format: string
example: disabled
created_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the channel was created.
updated_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the channel was created.
xml:
name: client
ClientSecret:
type: object
properties:
@@ -1208,16 +1284,12 @@ components:
Tags:
name: tags
description: Client tags.
description: Clients tags. Multiple tags can be specified separated by comma for OR condition and plus for AND condition.
in: query
schema:
type: array
minItems: 0
uniqueItems: true
items:
type: string
type: string
required: false
example: ["yello", "orange"]
example: "orange,yellow"
Metadata:
name: metadata
@@ -1225,7 +1297,6 @@ components:
in: query
schema:
type: string
minimum: 0
required: false
Limit:
@@ -1238,7 +1309,7 @@ components:
maximum: 100
minimum: 1
required: false
example: "100"
example: 100
Offset:
name: offset
@@ -1249,7 +1320,7 @@ components:
default: 0
minimum: 0
required: false
example: "0"
example: 0
Order:
name: order
@@ -1349,6 +1420,26 @@ components:
minLength: 36
required: false
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
CreatedFrom:
name: created_from
description: Filter clients created from this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-01-01T00:00:00Z"
CreatedTo:
name: created_to
description: Filter clients created up to this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-12-31T23:59:59Z"
requestBodies:
ClientCreateReq:
@@ -1370,7 +1461,7 @@ components:
$ref: "#/components/schemas/ClientReqObj"
ClientUpdateReq:
description: JSON-formated document describing the metadata and name of client to be update
description: JSON-formated document describing the metadata, private metadata and name of client to be update
required: true
content:
application/json:
@@ -1447,6 +1538,13 @@ components:
schema:
$ref: "#/components/schemas/Client"
DisabledClientRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/DisabledClient"
ClientPageRes:
description: Data retrieved.
content:
+36 -5
View File
@@ -9,7 +9,7 @@ info:
Some useful links:
- [The SuperMQ repository](https://github.com/absmach/supermq)
contact:
email: info@abstractmachines.fr
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/supermq/blob/main/LICENSE
@@ -24,22 +24,22 @@ tags:
description: CRUD operations for your domains
externalDocs:
description: Find out more about domains
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
- name: Roles
description: All operations involving roles for domains
externalDocs:
description: Find out more about roles
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
- name: Invitations
description: All operations involving invitations for domains
externalDocs:
description: Find out more about Invitations
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
- name: Health
description: Service health check endpoint.
externalDocs:
description: Find out more about health check
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
paths:
/domains:
@@ -76,6 +76,7 @@ paths:
- $ref: "#/components/parameters/Order"
- $ref: "#/components/parameters/Direction"
- $ref: "#/components/parameters/Metadata"
- $ref: "#/components/parameters/Tags"
- $ref: "#/components/parameters/Status"
- $ref: "#/components/parameters/DomainName"
- $ref: "./schemas/roles.yaml#/components/parameters/ActionsQuery"
@@ -83,6 +84,8 @@ paths:
- $ref: "./schemas/roles.yaml#/components/parameters/RoleNameQuery"
- $ref: "#/components/parameters/AccessType"
- $ref: "#/components/parameters/OnlyTotal"
- $ref: "#/components/parameters/CreatedFrom"
- $ref: "#/components/parameters/CreatedTo"
tags:
- Domains
security:
@@ -1179,6 +1182,14 @@ components:
schema:
type: object
additionalProperties: {}
Tags:
name: tags
description: Domain tags. Multiple tags can be specified separated by comma for OR condition and plus for AND condition.
in: query
schema:
type: string
required: false
example: "orange,yellow"
Type:
name: type
description: The type of the API Key.
@@ -1282,6 +1293,26 @@ components:
default: false
required: false
CreatedFrom:
name: created_from
description: Filter domains created from this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-01-01T00:00:00Z"
CreatedTo:
name: created_to
description: Filter domains created up to this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-12-31T23:59:59Z"
requestBodies:
DomainCreateReq:
description: JSON-formatted document describing the new domain to be registered
+63 -30
View File
@@ -9,7 +9,7 @@ info:
Some useful links:
- [The SuperMQ repository](https://github.com/absmach/supermq)
contact:
email: info@abstractmachines.fr
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/supermq/blob/main/LICENSE
@@ -24,17 +24,17 @@ tags:
description: CRUD operations for your groups
externalDocs:
description: Find out more about users groups
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
- name: Roles
description: All operations involving roles for groups
externalDocs:
description: Find out more about roles
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
- name: Health
description: Health check operations
externalDocs:
description: Find out more about health checks
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
paths:
/{domainID}/groups:
@@ -92,16 +92,19 @@ paths:
- $ref: "#/components/parameters/DirectionOrder"
- $ref: "#/components/parameters/Level"
- $ref: "#/components/parameters/Tree"
- $ref: "#/components/parameters/Tags"
- $ref: "#/components/parameters/Metadata"
- $ref: "#/components/parameters/GroupName"
- $ref: "#/components/parameters/RootGroup"
- $ref: "#/components/parameters/Status"
- $ref: "#/components/parameters/ID"
- $ref: "#/components/parameters/ID"
- $ref: "./schemas/roles.yaml#/components/parameters/ActionsQuery"
- $ref: "./schemas/roles.yaml#/components/parameters/RoleIDQuery"
- $ref: "./schemas/roles.yaml#/components/parameters/RoleNameQuery"
- $ref: "#/components/parameters/AccessType"
- $ref: "#/components/parameters/OnlyTotal"
- $ref: "#/components/parameters/OnlyTotal"
- $ref: "#/components/parameters/CreatedFrom"
- $ref: "#/components/parameters/CreatedTo"
responses:
"200":
$ref: "#/components/responses/GroupPageRes"
@@ -328,6 +331,8 @@ paths:
- $ref: "#/components/parameters/Level"
- $ref: "#/components/parameters/Tree"
- $ref: "#/components/parameters/Direction"
- $ref: "#/components/parameters/CreatedFrom"
- $ref: "#/components/parameters/CreatedTo"
responses:
"200":
$ref: "#/components/responses/GroupsHierarchyPageRes"
@@ -362,6 +367,11 @@ paths:
responses:
"200":
description: Parent group set.
content:
application/json:
schema:
type: object
links: {}
"400":
description: Failed due to malformed group's ID.
"401":
@@ -392,6 +402,11 @@ paths:
responses:
"200":
description: Parent group removed.
content:
application/json:
schema:
type: object
links: {}
"400":
description: Failed due to malformed group's ID.
"401":
@@ -1377,16 +1392,12 @@ components:
Tags:
name: tags
description: User tags.
description: Group tags. Multiple tags can be specified separated by comma for OR condition and plus for AND condition.
in: query
schema:
type: array
minItems: 0
uniqueItems: true
items:
type: string
type: string
required: false
example: ["yello", "orange"]
example: "orange,yellow"
GroupName:
name: name
@@ -1398,7 +1409,7 @@ components:
example: "groupName"
GroupDescription:
name: name
name: description
description: Group's description.
in: query
schema:
@@ -1513,7 +1524,6 @@ components:
in: query
schema:
type: string
minimum: 0
required: false
Limit:
@@ -1526,7 +1536,7 @@ components:
maximum: 100
minimum: 1
required: false
example: "100"
example: 100
Offset:
name: offset
@@ -1537,9 +1547,9 @@ components:
default: 0
minimum: 0
required: false
example: "0"
Order:
example: 0
Order:
name: order
description: Field by which to order the results
in: query
@@ -1547,8 +1557,8 @@ components:
type: string
required: false
example: created_at
DirectionOrder:
DirectionOrder:
name: order
description: Direction of ordering the results.
in: query
@@ -1560,7 +1570,7 @@ components:
required: false
example: desc
ID:
ID:
name: id
description: List groups with the given ID.
in: query
@@ -1569,7 +1579,7 @@ components:
format: uuid
required: false
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
AccessType:
name: access_type
description: Type of access the user has on the group.
@@ -1583,8 +1593,8 @@ components:
- indirect_group
required: false
example: direct
OnlyTotal:
OnlyTotal:
name: only_total
description: If true, the response will contain only the total number of groups that match the query parameters.
in: query
@@ -1592,7 +1602,27 @@ components:
type: boolean
default: false
required: false
CreatedFrom:
name: created_from
description: Filter groups created from this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-01-01T00:00:00Z"
CreatedTo:
name: created_to
description: Filter groups created up to this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-12-31T23:59:59Z"
User:
name: user
description: If provided lists groups associated with a user with the provided ID. Only available for admin users.
@@ -1663,10 +1693,10 @@ components:
operationId: getGroup
parameters:
groupID: $response.body#/id
get_children:
operationId: listChildrenGroups
parameters:
groupID: $response.body#/id
# get_children:
# operationId: listChildrenGroups
# parameters:
# groupID: $response.body#/id
update:
operationId: updateGroup
parameters:
@@ -1710,6 +1740,7 @@ components:
application/json:
schema:
$ref: "#/components/schemas/GroupsPage"
links: {}
GroupsHierarchyPageRes:
description: Group hierarchy retrieved.
@@ -1717,6 +1748,7 @@ components:
application/json:
schema:
$ref: "#/components/schemas/GroupsHierarchyPage"
links: {}
MembersPageRes:
description: Group members retrieved.
@@ -1724,6 +1756,7 @@ components:
application/json:
schema:
$ref: "#/components/schemas/MembersPage"
links: {}
HealthRes:
description: Service Health Check.
+4 -2
View File
@@ -9,7 +9,7 @@ info:
Some useful links:
- [The SuperMQ repository](https://github.com/absmach/supermq)
contact:
email: info@abstractmachines.fr
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/supermq/blob/main/LICENSE
@@ -24,7 +24,7 @@ tags:
description: Everything about your Messages
externalDocs:
description: Find out more about messages
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
paths:
/m/{domainPrefix}/c/{channelPrefix}:
@@ -47,6 +47,8 @@ paths:
description: Message discarded due to its malformed content.
"401":
description: Missing or invalid access token provided.
"403":
description: Access denied to the requested resource.
"404":
description: Message discarded due to invalid channel id.
"415":
+6 -6
View File
@@ -75,7 +75,7 @@ paths:
This includes connection status, messages sent/received, and other metrics.
parameters:
- $ref: "#/components/parameters/domain_id"
- $ref: '#/components/parameters/client_id'
- $ref: "#/components/parameters/client_id"
security:
- bearerAuth: []
responses:
@@ -319,7 +319,7 @@ components:
default: 0
minimum: 0
required: false
example: "0"
example: 0
limit:
name: limit
@@ -328,10 +328,10 @@ components:
schema:
type: integer
default: 10
maximum: 10
maximum: 100
minimum: 1
required: false
example: "10"
example: 10
operation:
name: operation
@@ -365,7 +365,7 @@ components:
description: Start date in unix time.
in: query
schema:
type: string
type: integer
format: int64
required: false
example: 1966777289
@@ -375,7 +375,7 @@ components:
description: End date in unix time.
in: query
schema:
type: string
type: integer
format: int64
required: false
example: 1966777289
-431
View File
@@ -1,431 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.1
info:
title: SuperMQ twins service
description: |
HTTP API for managing digital twins and their states.
Some useful links:
- [The SuperMQ repository](https://github.com/absmach/supermq)
contact:
email: info@abstractmachines.fr
license:
name: Apache 2.0
url: https://github.com/absmach/supermq/blob/main/LICENSE
version: 0.18.0
servers:
- url: http://localhost:9018
- url: https://localhost:9018
tags:
- name: twins
description: Everything about your Twins
externalDocs:
description: Find out more about twins
url: https://docs.supermq.abstractmachines.fr/
paths:
/twins:
post:
operationId: createTwin
summary: Adds new twin
description: |
Adds new twin to the list of twins owned by user identified using
the provided access token.
tags:
- twins
requestBody:
$ref: "#/components/requestBodies/TwinReq"
responses:
"201":
$ref: "#/components/responses/TwinCreateRes"
"400":
description: Failed due to malformed JSON.
"401":
description: Missing or invalid access token provided.
"415":
description: Missing or invalid content type.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
get:
operationId: getTwins
summary: Retrieves twins
description: |
Retrieves a list of twins. Due to performance concerns, data
is retrieved in subsets.
tags:
- twins
parameters:
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/Name"
- $ref: "#/components/parameters/Metadata"
responses:
"200":
$ref: "#/components/responses/TwinsPageRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/twins/{twinID}:
get:
operationId: getTwin
summary: Retrieves twin info
tags:
- twins
parameters:
- $ref: "#/components/parameters/TwinID"
responses:
"200":
$ref: "#/components/responses/TwinRes"
"400":
description: Failed due to malformed twin's ID.
"401":
description: Missing or invalid access token provided.
"404":
description: Twin does not exist.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
put:
operationId: updateTwin
summary: Updates twin info
description: |
Update is performed by replacing the current resource data with values
provided in a request payload. Note that the twin's ID cannot be changed.
tags:
- twins
parameters:
- $ref: "#/components/parameters/TwinID"
requestBody:
$ref: "#/components/requestBodies/TwinReq"
responses:
"200":
description: Twin updated.
"400":
description: Failed due to malformed twin's ID or malformed JSON.
"401":
description: Missing or invalid access token provided.
"404":
description: Twin does not exist.
"415":
description: Missing or invalid content type.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: removeTwin
summary: Removes a twin
description: Removes a twin.
tags:
- twins
parameters:
- $ref: "#/components/parameters/TwinID"
responses:
"204":
description: Twin removed.
"400":
description: Failed due to malformed twin's ID.
"401":
description: Missing or invalid access token provided
"404":
description: Twin does not exist.
"415":
description: Missing or invalid content type.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/states/{twinID}:
get:
operationId: getStates
summary: Retrieves states of twin with id twinID
description: |
Retrieves a list of states. Due to performance concerns, data
is retrieved in subsets.
tags:
- states
parameters:
- $ref: "#/components/parameters/TwinID"
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
responses:
"200":
$ref: "#/components/responses/StatesPageRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"404":
description: Twin does not exist.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/health:
get:
summary: Retrieves service health check info.
tags:
- health
security: []
responses:
"200":
$ref: "#/components/responses/HealthRes"
"500":
$ref: "#/components/responses/ServiceError"
components:
parameters:
Limit:
name: limit
description: Size of the subset to retrieve.
in: query
schema:
type: integer
default: 10
maximum: 100
minimum: 1
required: false
Offset:
name: offset
description: Number of items to skip during retrieval.
in: query
schema:
type: integer
default: 0
minimum: 0
required: false
Name:
name: name
description: Twin name
in: query
schema:
type: string
required: false
Metadata:
name: metadata
description: |
Metadata filter. Filtering is performed matching the parameter with
metadata on top level. Parameter is json.
in: query
schema:
type: string
minimum: 0
required: false
TwinID:
name: twinID
description: Unique twin identifier.
in: path
schema:
type: string
format: uuid
minimum: 1
required: true
schemas:
Attribute:
type: object
properties:
name:
type: string
description: Name of the attribute.
channel:
type: string
description: SuperMQ channel used by attribute.
subtopic:
type: string
description: Subtopic used by attribute.
persist_state:
type: boolean
description: Trigger state creation based on the attribute.
Definition:
type: object
properties:
delta:
type: number
description: Minimal time delay before new state creation.
attributes:
type: array
minItems: 0
items:
$ref: "#/components/schemas/Attribute"
TwinReqObj:
type: object
properties:
name:
type: string
description: Free-form twin name.
metadata:
type: object
description: Arbitrary, object-encoded twin's data.
definition:
$ref: "#/components/schemas/Definition"
TwinResObj:
type: object
properties:
owner:
type: string
description: Email address of SuperMQ user that owns twin.
id:
type: string
format: uuid
description: Unique twin identifier generated by the service.
name:
type: string
description: Free-form twin name.
revision:
type: number
description: Oridnal revision number of twin.
created:
type: string
format: date
description: Twin creation date and time.
updated:
type: string
format: date
description: Twin update date and time.
definitions:
type: array
minItems: 0
items:
$ref: "#/components/schemas/Definition"
metadata:
type: object
description: Arbitrary, object-encoded twin's data.
TwinsPage:
type: object
properties:
twins:
type: array
minItems: 0
items:
$ref: "#/components/schemas/TwinResObj"
total:
type: integer
description: Total number of items.
offset:
type: integer
description: Number of items to skip during retrieval.
limit:
type: integer
description: Maximum number of items to return in one page.
required:
- twins
State:
type: object
properties:
twin_id:
type: string
format: uuid
description: ID of twin state belongs to.
id:
type: number
description: State position in a time row of states.
created:
type: string
format: date
description: State creation date.
payload:
type: object
description: Object-encoded states's payload.
StatesPage:
type: object
properties:
states:
type: array
minItems: 0
items:
$ref: "#/components/schemas/State"
total:
type: integer
description: Total number of items.
offset:
type: integer
description: Number of items to skip during retrieval.
limit:
type: integer
description: Maximum number of items to return in one page.
required:
- states
requestBodies:
TwinReq:
description: JSON-formatted document describing the twin to create or update.
content:
application/json:
schema:
$ref: "#/components/schemas/TwinReqObj"
required: true
responses:
TwinCreateRes:
description: Created twin's relative URL (i.e. /twins/{twinID}).
headers:
Location:
content:
text/plain:
schema:
type: string
TwinRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/TwinResObj"
links:
update:
operationId: updateTwin
parameters:
twinID: $response.body#/id
delete:
operationId: removeTwin
parameters:
twinID: $response.body#/id
states:
operationId: getStates
parameters:
twinID: $response.body#/id
TwinsPageRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/TwinsPage"
StatesPageRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/StatesPage"
ServiceError:
description: Unexpected server-side error occurred.
HealthRes:
description: Service Health Check.
content:
application/health+json:
schema:
$ref: "./schemas/health_info.yaml"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
* Users access: "Authorization: Bearer <user_token>"
security:
- bearerAuth: []
+169 -35
View File
@@ -9,7 +9,7 @@ info:
Some useful links:
- [The SuperMQ repository](https://github.com/absmach/supermq)
contact:
email: info@abstractmachines.fr
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/supermq/blob/main/LICENSE
@@ -24,12 +24,12 @@ tags:
description: Everything about your Users
externalDocs:
description: Find out more about users
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
- name: Health
description: Health check operations
externalDocs:
description: Find out more about health checks
url: https://docs.supermq.abstractmachines.fr/
url: https://docs.supermq.absmach.eu/
paths:
/users:
@@ -83,8 +83,10 @@ paths:
- $ref: "#/components/parameters/LastName"
- $ref: "#/components/parameters/Username"
- $ref: "#/components/parameters/Email"
- $ref: "#/components/parameters/Tag"
- $ref: "#/components/parameters/Tags"
- $ref: "#/components/parameters/OnlyTotal"
- $ref: "#/components/parameters/CreatedFrom"
- $ref: "#/components/parameters/CreatedTo"
security:
- bearerAuth: []
responses:
@@ -116,7 +118,7 @@ paths:
- bearerAuth: []
responses:
"200":
$ref: "#/components/responses/UserRes"
$ref: "#/components/responses/UserProfileRes"
"400":
description: Failed due to malformed query parameters.
"401":
@@ -154,10 +156,10 @@ paths:
patch:
operationId: updateUser
summary: Updates first, last name and metadata of the user.
summary: Updates first, last name, metadata, and private metadata of the user.
description: |
Updates name and metadata of the user with provided ID. Name and metadata
is updated using authorization token and the new received info.
Updates name, metadata and private metadata of the user with provided ID. Name, metadata
and private metadata are updated using authorization token and the new received info.
tags:
- Users
parameters:
@@ -461,7 +463,7 @@ paths:
- bearerAuth: []
responses:
"200":
$ref: "#/components/responses/UserRes"
$ref: "#/components/responses/UserProfileRes"
"400":
description: Failed due to malformed JSON.
"401":
@@ -569,7 +571,7 @@ paths:
requestBody:
$ref: "#/components/requestBodies/IssueTokenReq"
responses:
"200":
"201":
$ref: "#/components/responses/TokenRes"
"400":
description: Failed due to malformed JSON.
@@ -610,6 +612,57 @@ paths:
"500":
$ref: "#/components/responses/ServiceError"
/users/tokens/revoke:
post:
operationId: revokeRefreshToken
summary: Revoke Refresh Token
description: |
Revokes a specific refresh token by its ID. This invalidates the
refresh token so it can no longer be used to obtain new access tokens.
tags:
- Users
security:
- bearerAuth: []
requestBody:
$ref: "#/components/requestBodies/RevokeRefreshTokenReq"
responses:
"204":
description: Refresh token revoked successfully.
"400":
description: Failed due to malformed JSON.
"401":
description: Missing or invalid access token provided.
"404":
description: A non-existent entity request.
"415":
description: Missing or invalid content type.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/users/tokens/refresh-tokens:
get:
operationId: listActiveRefreshTokens
summary: List Active Refresh Tokens
description: |
Lists all active refresh token sessions for the currently authenticated user.
tags:
- Users
security:
- bearerAuth: []
responses:
"200":
$ref: "#/components/responses/RefreshTokensPageRes"
"400":
description: Failed due to malformed JSON.
"401":
description: Missing or invalid access token provided.
"404":
description: A non-existent entity request.
"500":
$ref: "#/components/responses/ServiceError"
/users/send-verification:
post:
operationId: sendVerification
@@ -706,7 +759,7 @@ components:
type: string
format: password
example: password
minimum: 8
minLength: 8
description: Free-form account secret used for acquiring auth token(s).
required:
- username
@@ -714,7 +767,11 @@ components:
metadata:
type: object
example: { "domain": "example.com" }
description: Arbitrary, object-encoded user's data.
description: Arbitrary, object-encoded user's data visible to other users.
private_metadata:
type: object
example: { "domain": "example.com" }
description: Arbitrary, object-encoded user's data private to the user.
profile_picture:
type: string
example: "https://example.com/profile.jpg"
@@ -770,7 +827,11 @@ components:
metadata:
type: object
example: { "address": "example" }
description: Arbitrary, object-encoded user's data.
description: Arbitrary, object-encoded user's data visible to other users.
private_metadata:
type: object
example: { "address": "example" }
description: Arbitrary, object-encoded user's data private to the user.
profile_picture:
type: string
example: "https://example.com/profile.jpg"
@@ -830,12 +891,12 @@ components:
secret:
type: string
example: password
minimum: 8
minLength: 8
description: User secret password.
metadata:
type: object
example: { "role": "general" }
description: Arbitrary, object-encoded user's data.
description: Arbitrary, object-encoded user's data visible to other users.
status:
type: string
description: User Status
@@ -894,11 +955,11 @@ components:
metadata:
type: object
example: { "role": "general" }
description: Arbitrary, object-encoded user's data.
required:
- first_name
- last_name
- metadata
description: Arbitrary, object-encoded user's data visible to other users.
private_metadata:
type: object
example: { "role": "general" }
description: Arbitrary, object-encoded user's data private to the user.
UserTags:
type: object
@@ -908,7 +969,6 @@ components:
example: ["yello", "orange"]
description: User tags.
minItems: 0
uniqueItems: true
items:
type: string
@@ -938,12 +998,12 @@ components:
old_secret:
type: string
example: oldpassword
minimum: 8
minLength: 8
description: Old user secret password.
new_secret:
type: string
example: newpassword
minimum: 8
minLength: 8
description: New user secret password.
required:
- old_secret
@@ -1029,12 +1089,36 @@ components:
password:
type: string
example: password
minimum: 8
minLength: 8
description: User secret password.
required:
- username
- password
RefreshToken:
type: object
properties:
id:
type: string
format: uuid
example: "bb7edb32-2eac-4aad-aebe-ed96fe073879"
description: Unique identifier of the refresh token.
description:
type: string
example: "Chrome browser session"
description: Description of the refresh token session.
RefreshTokensPage:
type: object
properties:
refresh_tokens:
type: array
items:
$ref: "#/components/schemas/RefreshToken"
description: List of active refresh tokens.
required:
- refresh_tokens
Error:
type: object
properties:
@@ -1138,14 +1222,14 @@ components:
required: false
example: enabled
Tag:
name: tag
description: User tag.
Tags:
name: tags
description: User tags. Multiple tags can be specified separated by comma for OR condition and plus for AND condition.
in: query
schema:
type: string
required: false
example: "orange"
example: "orange,yellow"
GroupName:
name: name
@@ -1278,7 +1362,7 @@ components:
maximum: 100
minimum: 1
required: false
example: "100"
example: 100
Offset:
name: offset
@@ -1289,7 +1373,7 @@ components:
default: 0
minimum: 0
required: false
example: "0"
example: 0
Order:
name: order
@@ -1321,6 +1405,26 @@ components:
default: false
required: false
CreatedFrom:
name: created_from
description: Filter users created from this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-01-01T00:00:00Z"
CreatedTo:
name: created_to
description: Filter users created up to this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-12-31T23:59:59Z"
VerificationToken:
name: token
description: Verification token.
@@ -1339,7 +1443,7 @@ components:
$ref: "#/components/schemas/UserReqObj"
UserUpdateReq:
description: JSON-formated document describing the metadata and name of user to be update
description: JSON-formated document describing the name, metadata and private_metadata of user to be update
required: true
content:
application/json:
@@ -1444,18 +1548,34 @@ components:
format: password
description: New password.
example: "12345678"
minimum: 8
minLength: 8
confirm_password:
type: string
format: password
description: New confirmation password.
example: "12345678"
minimum: 8
minLength: 8
token:
type: string
format: jwt
description: Reset token generated and sent in email.
RevokeRefreshTokenReq:
description: JSON-formatted document describing the refresh token to revoke.
required: true
content:
application/json:
schema:
type: object
properties:
token_id:
type: string
format: uuid
example: "bb7edb32-2eac-4aad-aebe-ed96fe073879"
description: The unique identifier of the refresh token to revoke.
required:
- token_id
PasswordChange:
description: Password change data. User can change its password.
required: true
@@ -1467,11 +1587,11 @@ components:
password:
type: string
format: password
minimum: 8
minLength: 8
description: New password.
old_password:
type: string
minimum: 8
minLength: 8
format: password
description: Old password.
@@ -1533,6 +1653,13 @@ components:
schema:
$ref: "#/components/schemas/User"
UserProfileRes:
description: Data retrieved for current user profile.
content:
application/json:
schema:
$ref: "#/components/schemas/User"
UserPageRes:
description: Data retrieved.
content:
@@ -1560,6 +1687,13 @@ components:
example: access
description: User access token type.
RefreshTokensPageRes:
description: List of active refresh tokens for the authenticated user.
content:
application/json:
schema:
$ref: "#/components/schemas/RefreshTokensPage"
HealthRes:
description: Service Health Check.
content:
+49 -49
View File
@@ -40,7 +40,7 @@ The following actions are supported:
## Domains
Domains are used to group users and clients. Each domain has a unique route that is associated with the domain. Domains are used to group users and their entities.
Domains are used to group users and clients. Each domain has a unique `route` that is associated with the domain. Domains are used to group users and their entities.
Domain consists of the following fields:
@@ -59,50 +59,50 @@ Domain consists of the following fields:
The service is configured using the environment variables presented in the following table. Note that any unset variables will be replaced with their default values.
| Variable | Description | Default |
| ----------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------ |
| SMQ_AUTH_LOG_LEVEL | Log level for the Auth service (debug, info, warn, error) | info |
| SMQ_AUTH_DB_HOST | Database host address | localhost |
| SMQ_AUTH_DB_PORT | Database host port | 5432 |
| SMQ_AUTH_DB_USER | Database user | supermq |
| SMQ_AUTH_DB_PASSWORD | Database password | supermq |
| SMQ_AUTH_DB_NAME | Name of the database used by the service | auth |
| SMQ_AUTH_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
| SMQ_AUTH_DB_SSL_CERT | Path to the PEM encoded certificate file | "" |
| SMQ_AUTH_DB_SSL_KEY | Path to the PEM encoded key file | "" |
| SMQ_AUTH_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | "" |
| SMQ_AUTH_HTTP_HOST | Auth service HTTP host | "" |
| SMQ_AUTH_HTTP_PORT | Auth service HTTP port | 8189 |
| SMQ_AUTH_HTTP_SERVER_CERT | Path to the PEM encoded HTTP server certificate file | "" |
| SMQ_AUTH_HTTP_SERVER_KEY | Path to the PEM encoded HTTP server key file | "" |
| SMQ_AUTH_GRPC_HOST | Auth service gRPC host | "" |
| SMQ_AUTH_GRPC_PORT | Auth service gRPC port | 8181 |
| SMQ_AUTH_GRPC_SERVER_CERT | Path to the PEM encoded gRPC server certificate file | "" |
| SMQ_AUTH_GRPC_SERVER_KEY | Path to the PEM encoded gRPC server key file | "" |
| SMQ_AUTH_GRPC_SERVER_CA_CERTS | Path to the PEM encoded gRPC server CA certificate file | "" |
| SMQ_AUTH_GRPC_CLIENT_CA_CERTS | Path to the PEM encoded gRPC client CA certificate file | "" |
| SMQ_AUTH_SECRET_KEY | String used for signing tokens | secret |
| SMQ_AUTH_ACCESS_TOKEN_DURATION | The access token expiration period | 1h |
| SMQ_AUTH_REFRESH_TOKEN_DURATION | The refresh token expiration period | 24h |
| SMQ_AUTH_INVITATION_DURATION | The invitation token expiration period | 168h |
| SMQ_AUTH_CACHE_URL | Redis URL for caching PAT scopes | redis://localhost:6379/0 |
| SMQ_AUTH_CACHE_KEY_DURATION | Duration for which PAT scope cache keys are valid | 10m |
| SMQ_SPICEDB_HOST | SpiceDB host address | localhost |
| SMQ_SPICEDB_PORT | SpiceDB host port | 50051 |
| SMQ_SPICEDB_PRE_SHARED_KEY | SpiceDB pre-shared key | 12345678 |
| SMQ_SPICEDB_SCHEMA_FILE | Path to SpiceDB schema file | ./docker/spicedb/schema.zed |
| SMQ_JAEGER_URL | Jaeger server URL | <http://jaeger:4318/v1/traces> |
| SMQ_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 |
| SMQ_SEND_TELEMETRY | Send telemetry to supermq call home server | true |
| SMQ_ADAPTER_INSTANCE_ID | Adapter instance ID | "" |
| SMQ_CALLOUT_URLS | Comma-separated list of callout URLs | "" |
| SMQ_CALLOUT_METHOD | Callout method | POST |
| SMQ_CALLOUT_TLS_VERIFICATION | Enable TLS verification for callouts | true |
| SMQ_CALLOUT_TIMEOUT | Callout timeout | 10s |
| SMQ_CALLOUT_CA_CERT | Path to CA certificate file | "" |
| SMQ_CALLOUT_CERT | Path to client certificate file | "" |
| SMQ_CALLOUT_KEY | Path to client key file | "" |
| SMQ_CALLOUT_OPERATIONS | Invoke callout if the authorization permission matches any of the given permissions. | "" |
| Variable | Description | Default |
| :--- | :--- | :--- |
| `SMQ_AUTH_LOG_LEVEL` | Log level for the Auth service (debug, info, warn, error) | info |
| `SMQ_AUTH_DB_HOST` | Database host address | localhost |
| `SMQ_AUTH_DB_PORT` | Database host port | 5432 |
| `SMQ_AUTH_DB_USER` | Database user | supermq |
| `SMQ_AUTH_DB_PASSWORD` | Database password | supermq |
| `SMQ_AUTH_DB_NAME` | Name of the database used by the service | auth |
| `SMQ_AUTH_DB_SSL_MODE` | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
| `SMQ_AUTH_DB_SSL_CERT` | Path to the PEM encoded certificate file | "" |
| `SMQ_AUTH_DB_SSL_KEY` | Path to the PEM encoded key file | "" |
| `SMQ_AUTH_DB_SSL_ROOT_CERT` | Path to the PEM encoded root certificate file | "" |
| `SMQ_AUTH_HTTP_HOST` | Auth service HTTP host | "" |
| `SMQ_AUTH_HTTP_PORT` | Auth service HTTP port | 8189 |
| `SMQ_AUTH_HTTP_SERVER_CERT` | Path to the PEM encoded HTTP server certificate file | "" |
| `SMQ_AUTH_HTTP_SERVER_KEY` | Path to the PEM encoded HTTP server key file | "" |
| `SMQ_AUTH_GRPC_HOST` | Auth service gRPC host | "" |
| `SMQ_AUTH_GRPC_PORT` | Auth service gRPC port | 8181 |
| `SMQ_AUTH_GRPC_SERVER_CERT` | Path to the PEM encoded gRPC server certificate file | "" |
| `SMQ_AUTH_GRPC_SERVER_KEY` | Path to the PEM encoded gRPC server key file | "" |
| `SMQ_AUTH_GRPC_SERVER_CA_CERTS` | Path to the PEM encoded gRPC server CA certificate file | "" |
| `SMQ_AUTH_GRPC_CLIENT_CA_CERTS` | Path to the PEM encoded gRPC client CA certificate file | "" |
| `SMQ_AUTH_SECRET_KEY` | String used for signing tokens | secret |
| `SMQ_AUTH_ACCESS_TOKEN_DURATION` | The access token expiration period | 1h |
| `SMQ_AUTH_REFRESH_TOKEN_DURATION` | The refresh token expiration period | 24h |
| `SMQ_AUTH_INVITATION_DURATION` | The invitation token expiration period | 168h |
| `SMQ_AUTH_CACHE_URL` | Redis URL for caching PAT scopes | redis://localhost:6379/0 |
| `SMQ_AUTH_CACHE_KEY_DURATION` | Duration for which PAT scope cache keys are valid | 10m |
| `SMQ_SPICEDB_HOST` | SpiceDB host address | localhost |
| `SMQ_SPICEDB_PORT` | SpiceDB host port | 50051 |
| `SMQ_SPICEDB_PRE_SHARED_KEY` | SpiceDB pre-shared key | 12345678 |
| `SMQ_SPICEDB_SCHEMA_FILE` | Path to SpiceDB schema file | ./docker/spicedb/schema.zed |
| `SMQ_JAEGER_URL` | Jaeger server URL | <http://jaeger:4318/v1/traces> |
| `SMQ_JAEGER_TRACE_RATIO` | Jaeger sampling ratio | 1.0 |
| `SMQ_SEND_TELEMETRY` | Send telemetry to supermq call home server | true |
| `SMQ_ADAPTER_INSTANCE_ID` | Adapter instance ID | "" |
| `SMQ_CALLOUT_URLS` | Comma-separated list of callout URLs | "" |
| `SMQ_CALLOUT_METHOD` | Callout method | POST |
| `SMQ_CALLOUT_TLS_VERIFICATION` | Enable TLS verification for callouts | true |
| `SMQ_CALLOUT_TIMEOUT` | Callout timeout | 10s |
| `SMQ_CALLOUT_CA_CERT` | Path to CA certificate file | "" |
| `SMQ_CALLOUT_CERT` | Path to client certificate file | "" |
| `SMQ_CALLOUT_KEY` | Path to client key file | "" |
| `SMQ_CALLOUT_OPERATIONS` | Invoke callout if the authorization permission matches any of the given permissions. | "" |
## Deployment
@@ -183,7 +183,7 @@ PATs in SuperMQ are designed with the following features:
A PAT consists of three parts separated by underscores:
```
```bash
pat_<encoded-user-and-pat-id>_<random-string>
```
@@ -318,7 +318,7 @@ curl --location --request PATCH 'http://localhost:9001/pats/a2500226-95dc-4285-8
When making API requests, include the PAT in the Authorization header:
```
```bash
Authorization: Bearer pat_<encoded-user-and-pat-id>_<random-string>
```
@@ -444,6 +444,6 @@ When a PAT is used for authentication:
## Usage
For more information about service capabilities and its usage, please check out the [API documentation](https://docs.api.supermq.abstractmachines.fr/?urls.primaryName=auth.yaml).
For more information about service capabilities and its usage, please check out the [API documentation](https://docs.api.supermq.absmach.eu/?urls.primaryName=auth.yaml).
[doc]: https://docs.supermq.abstractmachines.fr
[doc]: https://docs.supermq.absmach.eu/
+54 -86
View File
@@ -18,11 +18,9 @@ import (
const authSvcName = "auth.v1.AuthService"
type authGrpcClient struct {
authenticate endpoint.Endpoint
authenticatePAT endpoint.Endpoint
authorize endpoint.Endpoint
authorizePAT endpoint.Endpoint
timeout time.Duration
authenticate endpoint.Endpoint
authorize endpoint.Endpoint
timeout time.Duration
}
var _ grpcAuthV1.AuthServiceClient = (*authGrpcClient)(nil)
@@ -38,14 +36,6 @@ func NewAuthClient(conn *grpc.ClientConn, timeout time.Duration) grpcAuthV1.Auth
decodeIdentifyResponse,
grpcAuthV1.AuthNRes{},
).Endpoint(),
authenticatePAT: kitgrpc.NewClient(
conn,
authSvcName,
"AuthenticatePAT",
encodeIdentifyRequest,
decodeIdentifyPATResponse,
grpcAuthV1.AuthNRes{},
).Endpoint(),
authorize: kitgrpc.NewClient(
conn,
authSvcName,
@@ -54,14 +44,6 @@ func NewAuthClient(conn *grpc.ClientConn, timeout time.Duration) grpcAuthV1.Auth
decodeAuthorizeResponse,
grpcAuthV1.AuthZRes{},
).Endpoint(),
authorizePAT: kitgrpc.NewClient(
conn,
authSvcName,
"AuthorizePAT",
encodeAuthorizePATRequest,
decodeAuthorizeResponse,
grpcAuthV1.AuthZRes{},
).Endpoint(),
timeout: timeout,
}
}
@@ -88,37 +70,39 @@ func decodeIdentifyResponse(_ context.Context, grpcRes any) (any, error) {
return authenticateRes{id: res.GetId(), userID: res.GetUserId(), userRole: auth.Role(res.UserRole), verified: res.GetVerified()}, nil
}
func (client authGrpcClient) AuthenticatePAT(ctx context.Context, token *grpcAuthV1.AuthNReq, _ ...grpc.CallOption) (*grpcAuthV1.AuthNRes, error) {
ctx, cancel := context.WithTimeout(ctx, client.timeout)
defer cancel()
res, err := client.authenticatePAT(ctx, authenticateReq{token: token.GetToken()})
if err != nil {
return &grpcAuthV1.AuthNRes{}, grpcapi.DecodeError(err)
}
ir := res.(authenticateRes)
return &grpcAuthV1.AuthNRes{Id: ir.id, UserId: ir.userID, UserRole: uint32(ir.userRole)}, nil
}
func decodeIdentifyPATResponse(_ context.Context, grpcRes any) (any, error) {
res := grpcRes.(*grpcAuthV1.AuthNRes)
return authenticateRes{id: res.GetId(), userID: res.GetUserId(), userRole: auth.Role(res.UserRole)}, nil
}
func (client authGrpcClient) Authorize(ctx context.Context, req *grpcAuthV1.AuthZReq, _ ...grpc.CallOption) (r *grpcAuthV1.AuthZRes, err error) {
ctx, cancel := context.WithTimeout(ctx, client.timeout)
defer cancel()
res, err := client.authorize(ctx, authReq{
Domain: req.GetDomain(),
SubjectType: req.GetSubjectType(),
Subject: req.GetSubject(),
SubjectKind: req.GetSubjectKind(),
Relation: req.GetRelation(),
Permission: req.GetPermission(),
ObjectType: req.GetObjectType(),
Object: req.GetObject(),
})
var authReqData authReq
if req != nil {
policyReq := req.GetPolicyReq()
patReq := req.GetPatReq()
if policyReq != nil {
authReqData = authReq{
Domain: policyReq.GetDomain(),
SubjectType: policyReq.GetSubjectType(),
Subject: policyReq.GetSubject(),
SubjectKind: policyReq.GetSubjectKind(),
Relation: policyReq.GetRelation(),
Permission: policyReq.GetPermission(),
ObjectType: policyReq.GetObjectType(),
Object: policyReq.GetObject(),
}
}
if patReq != nil {
authReqData.UserID = patReq.GetUserId()
authReqData.PatID = patReq.GetPatId()
authReqData.EntityType = patReq.GetEntityType()
authReqData.Operation = patReq.GetOperation()
authReqData.EntityID = patReq.GetEntityId()
}
}
res, err := client.authorize(ctx, authReqData)
if err != nil {
return &grpcAuthV1.AuthZRes{}, grpcapi.DecodeError(err)
}
@@ -134,46 +118,30 @@ func decodeAuthorizeResponse(_ context.Context, grpcRes any) (any, error) {
func encodeAuthorizeRequest(_ context.Context, grpcReq any) (any, error) {
req := grpcReq.(authReq)
return &grpcAuthV1.AuthZReq{
Domain: req.Domain,
SubjectType: req.SubjectType,
Subject: req.Subject,
SubjectKind: req.SubjectKind,
Relation: req.Relation,
Permission: req.Permission,
ObjectType: req.ObjectType,
Object: req.Object,
}, nil
}
func (client authGrpcClient) AuthorizePAT(ctx context.Context, req *grpcAuthV1.AuthZPatReq, _ ...grpc.CallOption) (r *grpcAuthV1.AuthZRes, err error) {
ctx, cancel := context.WithTimeout(ctx, client.timeout)
defer cancel()
res, err := client.authorizePAT(ctx, authPATReq{
userID: req.GetUserId(),
patID: req.GetPatId(),
entityType: auth.EntityType(req.GetEntityType()),
optionalDomainID: req.GetOptionalDomainId(),
operation: auth.Operation(req.GetOperation()),
entityID: req.GetEntityId(),
})
if err != nil {
return &grpcAuthV1.AuthZRes{}, grpcapi.DecodeError(err)
authZReq := &grpcAuthV1.AuthZReq{
PolicyReq: &grpcAuthV1.PolicyReq{
Domain: req.Domain,
SubjectType: req.SubjectType,
Subject: req.Subject,
SubjectKind: req.SubjectKind,
Relation: req.Relation,
Permission: req.Permission,
ObjectType: req.ObjectType,
Object: req.Object,
},
}
ar := res.(authorizeRes)
return &grpcAuthV1.AuthZRes{Authorized: ar.authorized, Id: ar.id}, nil
}
if req.PatID != "" {
authZReq.PatReq = &grpcAuthV1.PATReq{
PatId: req.PatID,
Domain: req.Domain,
Operation: req.Operation,
UserId: req.UserID,
EntityId: req.EntityID,
EntityType: req.EntityType,
}
}
func encodeAuthorizePATRequest(_ context.Context, grpcReq any) (any, error) {
req := grpcReq.(authPATReq)
return &grpcAuthV1.AuthZPatReq{
UserId: req.userID,
PatId: req.patID,
EntityType: uint32(req.entityType),
OptionalDomainId: req.optionalDomainID,
Operation: uint32(req.operation),
EntityId: req.entityID,
}, nil
return authZReq, nil
}
+19 -33
View File
@@ -23,23 +23,7 @@ func authenticateEndpoint(svc auth.Service) endpoint.Endpoint {
return authenticateRes{}, err
}
return authenticateRes{userID: key.Subject, userRole: key.Role, verified: key.Verified}, nil
}
}
func authenticatePATEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(authenticateReq)
if err := req.validate(); err != nil {
return authenticateRes{}, err
}
pat, err := svc.IdentifyPAT(ctx, req.token)
if err != nil {
return authenticateRes{}, err
}
return authenticateRes{id: pat.ID, userID: pat.User, userRole: pat.Role}, nil
return authenticateRes{id: key.ID, userID: key.Subject, userRole: key.Role, verified: key.Verified}, nil
}
}
@@ -50,6 +34,23 @@ func authorizeEndpoint(svc auth.Service) endpoint.Endpoint {
if err := req.validate(); err != nil {
return authorizeRes{}, err
}
var pat *auth.PATAuthz
if req.PatID != "" {
entityType, err := auth.ParseEntityType(req.EntityType)
if err != nil {
return authorizeRes{authorized: false}, err
}
pat = &auth.PATAuthz{
PatID: req.PatID,
UserID: req.UserID,
EntityType: entityType,
EntityID: req.EntityID,
Operation: req.Operation,
Domain: req.Domain,
}
}
err := svc.Authorize(ctx, policies.Policy{
Domain: req.Domain,
SubjectType: req.SubjectType,
@@ -59,22 +60,7 @@ func authorizeEndpoint(svc auth.Service) endpoint.Endpoint {
Permission: req.Permission,
ObjectType: req.ObjectType,
Object: req.Object,
})
if err != nil {
return authorizeRes{authorized: false}, err
}
return authorizeRes{authorized: true}, nil
}
}
func authorizePATEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(authPATReq)
if err := req.validate(); err != nil {
return authorizeRes{}, err
}
err := svc.AuthorizePAT(ctx, req.userID, req.patID, req.entityType, req.optionalDomainID, req.operation, req.entityID)
}, pat)
if err != nil {
return authorizeRes{authorized: false}, err
}
+175 -191
View File
@@ -17,6 +17,7 @@ import (
"github.com/absmach/supermq/internal/testsutil"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/absmach/supermq/pkg/policies"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"google.golang.org/grpc"
@@ -25,25 +26,14 @@ import (
const (
port = 8081
secret = "secret"
email = "test@example.com"
id = "testID"
clientsType = "clients"
usersType = "users"
description = "Description"
groupName = "smqx"
adminPermission = "admin"
authoritiesObj = "authorities"
memberRelation = "member"
loginDuration = 30 * time.Minute
refreshDuration = 24 * time.Hour
invalidDuration = 7 * 24 * time.Hour
validToken = "valid"
inValidToken = "invalid"
validPATToken = "valid"
inValidPATToken = "invalid"
validPolicy = "valid"
)
var (
@@ -73,6 +63,7 @@ func TestIdentify(t *testing.T) {
cases := []struct {
desc string
token string
key auth.Key
idt *grpcAuthV1.AuthNRes
svcErr error
err error
@@ -80,12 +71,14 @@ func TestIdentify(t *testing.T) {
{
desc: "authenticate user with valid user token",
token: validToken,
key: auth.Key{ID: "", Subject: id, Role: auth.UserRole},
idt: &grpcAuthV1.AuthNRes{UserId: id, UserRole: uint32(auth.UserRole)},
err: nil,
},
{
desc: "authenticate user with invalid user token",
token: "invalid",
key: auth.Key{},
idt: &grpcAuthV1.AuthNRes{},
svcErr: svcerr.ErrAuthentication,
err: svcerr.ErrAuthentication,
@@ -96,11 +89,26 @@ func TestIdentify(t *testing.T) {
idt: &grpcAuthV1.AuthNRes{},
err: apiutil.ErrBearerToken,
},
{
desc: "authenticate user with valid PAT token",
token: "pat_" + validPATToken,
key: auth.Key{ID: id, Type: auth.PersonalAccessToken, Subject: clientID, Role: auth.UserRole},
idt: &grpcAuthV1.AuthNRes{Id: id, UserId: clientID, UserRole: uint32(auth.UserRole)},
err: nil,
},
{
desc: "authenticate user with invalid PAT token",
token: "pat_invalid",
key: auth.Key{},
idt: &grpcAuthV1.AuthNRes{},
svcErr: svcerr.ErrAuthentication,
err: svcerr.ErrAuthentication,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
svcCall := svc.On("Identify", mock.Anything, mock.Anything).Return(auth.Key{Subject: id, Role: auth.UserRole}, tc.svcErr)
svcCall := svc.On("Identify", mock.Anything, tc.token).Return(tc.key, tc.svcErr)
idt, err := grpcClient.Authenticate(context.Background(), &grpcAuthV1.AuthNReq{Token: tc.token})
if idt != nil {
assert.Equal(t, tc.idt, idt, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.idt, idt))
@@ -129,12 +137,14 @@ func TestAuthorize(t *testing.T) {
desc: "authorize user with authorized token",
token: validToken,
authRequest: &grpcAuthV1.AuthZReq{
Subject: id,
SubjectType: usersType,
Object: authoritiesObj,
ObjectType: usersType,
Relation: memberRelation,
Permission: adminPermission,
PolicyReq: &grpcAuthV1.PolicyReq{
Subject: id,
SubjectType: usersType,
Object: authoritiesObj,
ObjectType: usersType,
Relation: memberRelation,
Permission: adminPermission,
},
},
authResponse: &grpcAuthV1.AuthZRes{Authorized: true},
err: nil,
@@ -143,12 +153,14 @@ func TestAuthorize(t *testing.T) {
desc: "authorize user with unauthorized token",
token: inValidToken,
authRequest: &grpcAuthV1.AuthZReq{
Subject: id,
SubjectType: usersType,
Object: authoritiesObj,
ObjectType: usersType,
Relation: memberRelation,
Permission: adminPermission,
PolicyReq: &grpcAuthV1.PolicyReq{
Subject: id,
SubjectType: usersType,
Object: authoritiesObj,
ObjectType: usersType,
Relation: memberRelation,
Permission: adminPermission,
},
},
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
err: svcerr.ErrAuthorization,
@@ -157,12 +169,14 @@ func TestAuthorize(t *testing.T) {
desc: "authorize user with empty subject",
token: validToken,
authRequest: &grpcAuthV1.AuthZReq{
Subject: "",
SubjectType: usersType,
Object: authoritiesObj,
ObjectType: usersType,
Relation: memberRelation,
Permission: adminPermission,
PolicyReq: &grpcAuthV1.PolicyReq{
Subject: "",
SubjectType: usersType,
Object: authoritiesObj,
ObjectType: usersType,
Relation: memberRelation,
Permission: adminPermission,
},
},
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
err: apiutil.ErrMissingPolicySub,
@@ -171,12 +185,14 @@ func TestAuthorize(t *testing.T) {
desc: "authorize user with empty subject type",
token: validToken,
authRequest: &grpcAuthV1.AuthZReq{
Subject: id,
SubjectType: "",
Object: authoritiesObj,
ObjectType: usersType,
Relation: memberRelation,
Permission: adminPermission,
PolicyReq: &grpcAuthV1.PolicyReq{
Subject: id,
SubjectType: "",
Object: authoritiesObj,
ObjectType: usersType,
Relation: memberRelation,
Permission: adminPermission,
},
},
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
err: apiutil.ErrMissingPolicySub,
@@ -185,12 +201,14 @@ func TestAuthorize(t *testing.T) {
desc: "authorize user with empty object",
token: validToken,
authRequest: &grpcAuthV1.AuthZReq{
Subject: id,
SubjectType: usersType,
Object: "",
ObjectType: usersType,
Relation: memberRelation,
Permission: adminPermission,
PolicyReq: &grpcAuthV1.PolicyReq{
Subject: id,
SubjectType: usersType,
Object: "",
ObjectType: usersType,
Relation: memberRelation,
Permission: adminPermission,
},
},
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
err: apiutil.ErrMissingPolicyObj,
@@ -199,12 +217,14 @@ func TestAuthorize(t *testing.T) {
desc: "authorize user with empty object type",
token: validToken,
authRequest: &grpcAuthV1.AuthZReq{
Subject: id,
SubjectType: usersType,
Object: authoritiesObj,
ObjectType: "",
Relation: memberRelation,
Permission: adminPermission,
PolicyReq: &grpcAuthV1.PolicyReq{
Subject: id,
SubjectType: usersType,
Object: authoritiesObj,
ObjectType: "",
Relation: memberRelation,
Permission: adminPermission,
},
},
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
err: apiutil.ErrMissingPolicyObj,
@@ -213,20 +233,120 @@ func TestAuthorize(t *testing.T) {
desc: "authorize user with empty permission",
token: validToken,
authRequest: &grpcAuthV1.AuthZReq{
Subject: id,
SubjectType: usersType,
Object: authoritiesObj,
ObjectType: usersType,
Relation: memberRelation,
Permission: "",
PolicyReq: &grpcAuthV1.PolicyReq{
Subject: id,
SubjectType: usersType,
Object: authoritiesObj,
ObjectType: usersType,
Relation: memberRelation,
Permission: "",
},
},
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
err: apiutil.ErrMalformedPolicyPer,
},
{
desc: "authorize user with valid PAT token",
token: validPATToken,
authRequest: &grpcAuthV1.AuthZReq{
PolicyReq: &grpcAuthV1.PolicyReq{
Subject: id,
SubjectType: policies.UserType,
SubjectKind: policies.UsersKind,
Permission: policies.ViewPermission,
ObjectType: policies.ClientType,
Domain: domainID,
Object: clientID,
},
PatReq: &grpcAuthV1.PATReq{
PatId: id,
Domain: domainID,
Operation: "view",
UserId: id,
EntityId: clientID,
EntityType: auth.ClientsScopeStr,
},
},
authResponse: &grpcAuthV1.AuthZRes{Authorized: true},
err: nil,
},
{
desc: "authorize user with unauthorized PAT token",
token: inValidToken,
authRequest: &grpcAuthV1.AuthZReq{
PolicyReq: &grpcAuthV1.PolicyReq{
Subject: id,
SubjectType: policies.UserType,
SubjectKind: policies.UsersKind,
Permission: policies.ViewPermission,
ObjectType: policies.ClientType,
Domain: domainID,
Object: clientID,
},
PatReq: &grpcAuthV1.PATReq{
PatId: id,
Domain: domainID,
Operation: "view",
UserId: id,
EntityId: clientID,
EntityType: auth.ClientsScopeStr,
},
},
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
err: svcerr.ErrAuthorization,
},
{
desc: "authorize PAT with missing user id",
token: validPATToken,
authRequest: &grpcAuthV1.AuthZReq{
PolicyReq: &grpcAuthV1.PolicyReq{
Subject: id,
SubjectType: policies.UserType,
SubjectKind: policies.UsersKind,
Permission: policies.ViewPermission,
ObjectType: policies.ClientType,
Domain: domainID,
Object: clientID,
},
PatReq: &grpcAuthV1.PATReq{
PatId: id,
Domain: domainID,
Operation: "view",
EntityId: clientID,
EntityType: auth.ClientsScopeStr,
},
},
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
err: apiutil.ErrMissingUserID,
},
{
desc: "authorize PAT with missing entity id",
token: validPATToken,
authRequest: &grpcAuthV1.AuthZReq{
PolicyReq: &grpcAuthV1.PolicyReq{
Subject: id,
SubjectType: policies.UserType,
SubjectKind: policies.UsersKind,
Permission: policies.ViewPermission,
ObjectType: policies.ClientType,
Domain: domainID,
Object: clientID,
},
PatReq: &grpcAuthV1.PATReq{
PatId: id,
Domain: domainID,
Operation: "view",
UserId: id,
EntityType: auth.ClientsScopeStr,
},
},
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
err: apiutil.ErrMissingID,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
svcCall := svc.On("Authorize", mock.Anything, mock.Anything).Return(tc.err)
svcCall := svc.On("Authorize", mock.Anything, mock.Anything, mock.Anything).Return(tc.err)
ar, err := grpcClient.Authorize(context.Background(), tc.authRequest)
if ar != nil {
assert.Equal(t, tc.authResponse, ar, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.authResponse, ar))
@@ -236,139 +356,3 @@ func TestAuthorize(t *testing.T) {
})
}
}
func TestIdentifyPAT(t *testing.T) {
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
defer conn.Close()
grpcClient := grpcapi.NewAuthClient(conn, time.Second)
cases := []struct {
desc string
token string
idt *grpcAuthV1.AuthNRes
svcErr error
err error
}{
{
desc: "authenticate user with valid user token",
token: validToken,
idt: &grpcAuthV1.AuthNRes{Id: id, UserId: clientID},
err: nil,
},
{
desc: "authenticate user with invalid user token",
token: "invalid",
idt: &grpcAuthV1.AuthNRes{},
svcErr: svcerr.ErrAuthentication,
err: svcerr.ErrAuthentication,
},
{
desc: "authenticate user with empty token",
token: "",
idt: &grpcAuthV1.AuthNRes{},
err: apiutil.ErrBearerToken,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
svcCall := svc.On("IdentifyPAT", mock.Anything, tc.token).Return(auth.PAT{ID: id, User: clientID, IssuedAt: time.Now()}, tc.svcErr)
idt, err := grpcClient.AuthenticatePAT(context.Background(), &grpcAuthV1.AuthNReq{Token: tc.token})
if idt != nil {
assert.Equal(t, tc.idt, idt, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.idt, idt))
}
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
svcCall.Unset()
})
}
}
func TestAuthorizePAT(t *testing.T) {
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
defer conn.Close()
grpcClient := grpcapi.NewAuthClient(conn, time.Second)
cases := []struct {
desc string
token string
authRequest *grpcAuthV1.AuthZPatReq
authResponse *grpcAuthV1.AuthZRes
err error
}{
{
desc: "authorize user with authorized token",
token: validPATToken,
authRequest: &grpcAuthV1.AuthZPatReq{
UserId: id,
PatId: id,
EntityType: uint32(auth.ClientsType),
OptionalDomainId: domainID,
Operation: uint32(auth.CreateOp),
EntityId: clientID,
},
authResponse: &grpcAuthV1.AuthZRes{Authorized: true},
err: nil,
},
{
desc: "authorize user with unauthorized token",
token: inValidPATToken,
authRequest: &grpcAuthV1.AuthZPatReq{
UserId: id,
PatId: id,
EntityType: uint32(auth.ClientsType),
OptionalDomainId: domainID,
Operation: uint32(auth.CreateOp),
EntityId: clientID,
},
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
err: svcerr.ErrAuthorization,
},
{
desc: "authorize user with missing user id",
token: validPATToken,
authRequest: &grpcAuthV1.AuthZPatReq{
PatId: id,
EntityType: uint32(auth.ClientsType),
OptionalDomainId: domainID,
Operation: uint32(auth.CreateOp),
EntityId: clientID,
},
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
err: apiutil.ErrMissingUserID,
},
{
desc: "authorize user with missing pat id",
token: validPATToken,
authRequest: &grpcAuthV1.AuthZPatReq{
UserId: id,
EntityType: uint32(auth.ClientsType),
OptionalDomainId: domainID,
Operation: uint32(auth.CreateOp),
EntityId: clientID,
},
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
err: apiutil.ErrMissingPATID,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
svcCall := svc.On("AuthorizePAT",
mock.Anything,
tc.authRequest.UserId,
tc.authRequest.PatId,
mock.Anything,
tc.authRequest.OptionalDomainId,
mock.Anything,
mock.Anything,
mock.Anything).Return(tc.err)
ar, err := grpcClient.AuthorizePAT(context.Background(), tc.authRequest)
if ar != nil {
assert.Equal(t, tc.authResponse, ar, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.authResponse, ar))
}
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
svcCall.Unset()
})
}
}
+19 -20
View File
@@ -5,7 +5,6 @@ package auth
import (
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/auth"
)
type authenticateReq struct {
@@ -33,9 +32,28 @@ type authReq struct {
Permission string
ObjectType string
Object string
// PAT authorization fields
UserID string
PatID string
EntityType string
Operation string
EntityID string
}
func (req authReq) validate() error {
if req.PatID != "" {
if req.UserID == "" {
return apiutil.ErrMissingUserID
}
if req.EntityID == "" {
return apiutil.ErrMissingID
}
if req.EntityType == "" {
return apiutil.ErrMissingPolicyObj
}
}
if req.Subject == "" || req.SubjectType == "" {
return apiutil.ErrMissingPolicySub
}
@@ -50,22 +68,3 @@ func (req authReq) validate() error {
return nil
}
type authPATReq struct {
userID string
patID string
entityType auth.EntityType
optionalDomainID string
operation auth.Operation
entityID string
}
func (req authPATReq) validate() error {
if req.userID == "" {
return apiutil.ErrMissingUserID
}
if req.patID == "" {
return apiutil.ErrMissingPATID
}
return nil
}
+33 -59
View File
@@ -16,10 +16,8 @@ var _ grpcAuthV1.AuthServiceServer = (*authGrpcServer)(nil)
type authGrpcServer struct {
grpcAuthV1.UnimplementedAuthServiceServer
authorize kitgrpc.Handler
authenticate kitgrpc.Handler
authenticatePAT kitgrpc.Handler
authorizePAT kitgrpc.Handler
authorize kitgrpc.Handler
authenticate kitgrpc.Handler
}
// NewAuthServer returns new AuthnServiceServer instance.
@@ -36,18 +34,6 @@ func NewAuthServer(svc auth.Service) grpcAuthV1.AuthServiceServer {
decodeAuthenticateRequest,
encodeAuthenticateResponse,
),
authenticatePAT: kitgrpc.NewServer(
(authenticatePATEndpoint(svc)),
decodeAuthenticateRequest,
encodeAuthenticatePATResponse,
),
authorizePAT: kitgrpc.NewServer(
(authorizePATEndpoint(svc)),
decodeAuthorizePATRequest,
encodeAuthorizeResponse,
),
}
}
@@ -59,14 +45,6 @@ func (s *authGrpcServer) Authenticate(ctx context.Context, req *grpcAuthV1.AuthN
return res.(*grpcAuthV1.AuthNRes), nil
}
func (s *authGrpcServer) AuthenticatePAT(ctx context.Context, req *grpcAuthV1.AuthNReq) (*grpcAuthV1.AuthNRes, error) {
_, res, err := s.authenticatePAT.ServeGRPC(ctx, req)
if err != nil {
return nil, grpcapi.EncodeError(err)
}
return res.(*grpcAuthV1.AuthNRes), nil
}
func (s *authGrpcServer) Authorize(ctx context.Context, req *grpcAuthV1.AuthZReq) (*grpcAuthV1.AuthZRes, error) {
_, res, err := s.authorize.ServeGRPC(ctx, req)
if err != nil {
@@ -85,46 +63,42 @@ func encodeAuthenticateResponse(_ context.Context, grpcRes any) (any, error) {
return &grpcAuthV1.AuthNRes{Id: res.id, UserId: res.userID, UserRole: uint32(res.userRole), Verified: res.verified}, nil
}
func encodeAuthenticatePATResponse(_ context.Context, grpcRes any) (any, error) {
res := grpcRes.(authenticateRes)
return &grpcAuthV1.AuthNRes{Id: res.id, UserId: res.userID, UserRole: uint32(res.userRole)}, nil
}
func decodeAuthorizeRequest(_ context.Context, grpcReq any) (any, error) {
req := grpcReq.(*grpcAuthV1.AuthZReq)
return authReq{
Domain: req.GetDomain(),
SubjectType: req.GetSubjectType(),
SubjectKind: req.GetSubjectKind(),
Subject: req.GetSubject(),
Relation: req.GetRelation(),
Permission: req.GetPermission(),
ObjectType: req.GetObjectType(),
Object: req.GetObject(),
}, nil
if req == nil {
return authReq{}, nil
}
policyReq := req.GetPolicyReq()
patReq := req.GetPatReq()
if policyReq == nil {
return authReq{}, nil
}
authRequest := authReq{
Domain: policyReq.GetDomain(),
SubjectType: policyReq.GetSubjectType(),
SubjectKind: policyReq.GetSubjectKind(),
Subject: policyReq.GetSubject(),
Relation: policyReq.GetRelation(),
Permission: policyReq.GetPermission(),
ObjectType: policyReq.GetObjectType(),
Object: policyReq.GetObject(),
}
if patReq != nil {
authRequest.UserID = patReq.GetUserId()
authRequest.PatID = patReq.GetPatId()
authRequest.EntityType = patReq.GetEntityType()
authRequest.Operation = patReq.GetOperation()
authRequest.EntityID = patReq.GetEntityId()
}
return authRequest, nil
}
func encodeAuthorizeResponse(_ context.Context, grpcRes any) (any, error) {
res := grpcRes.(authorizeRes)
return &grpcAuthV1.AuthZRes{Authorized: res.authorized, Id: res.id}, nil
}
func decodeAuthorizePATRequest(_ context.Context, grpcReq any) (any, error) {
req := grpcReq.(*grpcAuthV1.AuthZPatReq)
return authPATReq{
userID: req.GetUserId(),
patID: req.GetPatId(),
entityType: auth.EntityType(req.GetEntityType()),
optionalDomainID: req.GetOptionalDomainId(),
operation: auth.Operation(req.GetOperation()),
entityID: req.GetEntityId(),
}, nil
}
func (s *authGrpcServer) AuthorizePAT(ctx context.Context, req *grpcAuthV1.AuthZPatReq) (*grpcAuthV1.AuthZRes, error) {
_, res, err := s.authorizePAT.ServeGRPC(ctx, req)
if err != nil {
return nil, grpcapi.EncodeError(err)
}
return res.(*grpcAuthV1.AuthZRes), nil
}
+72 -12
View File
@@ -18,14 +18,16 @@ import (
const tokenSvcName = "token.v1.TokenService"
type tokenGrpcClient struct {
issue endpoint.Endpoint
refresh endpoint.Endpoint
timeout time.Duration
issue endpoint.Endpoint
refresh endpoint.Endpoint
revoke endpoint.Endpoint
listUserRefreshTokens endpoint.Endpoint
timeout time.Duration
}
var _ grpcTokenV1.TokenServiceClient = (*tokenGrpcClient)(nil)
// NewAuthClient returns new auth gRPC client instance.
// NewTokenClient returns new token gRPC client instance.
func NewTokenClient(conn *grpc.ClientConn, timeout time.Duration) grpcTokenV1.TokenServiceClient {
return &tokenGrpcClient{
issue: kitgrpc.NewClient(
@@ -44,6 +46,22 @@ func NewTokenClient(conn *grpc.ClientConn, timeout time.Duration) grpcTokenV1.To
decodeRefreshResponse,
grpcTokenV1.Token{},
).Endpoint(),
revoke: kitgrpc.NewClient(
conn,
tokenSvcName,
"Revoke",
encodeRevokeRequest,
decodeRevokeResponse,
grpcTokenV1.RevokeRes{},
).Endpoint(),
listUserRefreshTokens: kitgrpc.NewClient(
conn,
tokenSvcName,
"ListUserRefreshTokens",
encodeListUserRefreshTokensRequest,
decodeListUserRefreshTokensResponse,
grpcTokenV1.ListUserRefreshTokensRes{},
).Endpoint(),
timeout: timeout,
}
}
@@ -53,10 +71,11 @@ func (client tokenGrpcClient) Issue(ctx context.Context, req *grpcTokenV1.IssueR
defer cancel()
res, err := client.issue(ctx, issueReq{
userID: req.GetUserId(),
userRole: auth.Role(req.GetUserRole()),
keyType: auth.KeyType(req.GetType()),
verified: req.GetVerified(),
userID: req.GetUserId(),
userRole: auth.Role(req.GetUserRole()),
keyType: auth.KeyType(req.GetType()),
verified: req.GetVerified(),
description: req.GetDescription(),
})
if err != nil {
return &grpcTokenV1.Token{}, grpcapi.DecodeError(err)
@@ -67,10 +86,11 @@ func (client tokenGrpcClient) Issue(ctx context.Context, req *grpcTokenV1.IssueR
func encodeIssueRequest(_ context.Context, grpcReq any) (any, error) {
req := grpcReq.(issueReq)
return &grpcTokenV1.IssueReq{
UserId: req.userID,
UserRole: uint32(req.userRole),
Type: uint32(req.keyType),
Verified: req.verified,
UserId: req.userID,
UserRole: uint32(req.userRole),
Type: uint32(req.keyType),
Verified: req.verified,
Description: req.description,
}, nil
}
@@ -97,3 +117,43 @@ func encodeRefreshRequest(_ context.Context, grpcReq any) (any, error) {
func decodeRefreshResponse(_ context.Context, grpcRes any) (any, error) {
return grpcRes, nil
}
func (client tokenGrpcClient) Revoke(ctx context.Context, req *grpcTokenV1.RevokeReq, _ ...grpc.CallOption) (*grpcTokenV1.RevokeRes, error) {
ctx, cancel := context.WithTimeout(ctx, client.timeout)
defer cancel()
res, err := client.revoke(ctx, revokeReq{userID: req.GetUserId(), tokenID: req.GetTokenId()})
if err != nil {
return &grpcTokenV1.RevokeRes{}, grpcapi.DecodeError(err)
}
return res.(*grpcTokenV1.RevokeRes), nil
}
func encodeRevokeRequest(_ context.Context, grpcReq any) (any, error) {
req := grpcReq.(revokeReq)
return &grpcTokenV1.RevokeReq{UserId: req.userID, TokenId: req.tokenID}, nil
}
func decodeRevokeResponse(_ context.Context, grpcRes any) (any, error) {
return grpcRes, nil
}
func (client tokenGrpcClient) ListUserRefreshTokens(ctx context.Context, req *grpcTokenV1.ListUserRefreshTokensReq, _ ...grpc.CallOption) (*grpcTokenV1.ListUserRefreshTokensRes, error) {
ctx, cancel := context.WithTimeout(ctx, client.timeout)
defer cancel()
res, err := client.listUserRefreshTokens(ctx, listUserRefreshTokensReq{userID: req.GetUserId()})
if err != nil {
return &grpcTokenV1.ListUserRefreshTokensRes{}, grpcapi.DecodeError(err)
}
return res.(*grpcTokenV1.ListUserRefreshTokensRes), nil
}
func encodeListUserRefreshTokensRequest(_ context.Context, grpcReq any) (any, error) {
req := grpcReq.(listUserRefreshTokensReq)
return &grpcTokenV1.ListUserRefreshTokensReq{UserId: req.userID}, nil
}
func decodeListUserRefreshTokensResponse(_ context.Context, grpcRes any) (any, error) {
return grpcRes, nil
}
+36 -4
View File
@@ -18,10 +18,11 @@ func issueEndpoint(svc auth.Service) endpoint.Endpoint {
}
key := auth.Key{
Type: req.keyType,
Subject: req.userID,
Role: req.userRole,
Verified: req.verified,
Type: req.keyType,
Subject: req.userID,
Role: req.userRole,
Verified: req.verified,
Description: req.description,
}
tkn, err := svc.Issue(ctx, "", key)
if err != nil {
@@ -56,3 +57,34 @@ func refreshEndpoint(svc auth.Service) endpoint.Endpoint {
return ret, nil
}
}
func revokeEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(revokeReq)
if err := req.validate(); err != nil {
return nil, err
}
err := svc.RevokeToken(ctx, req.userID, req.tokenID)
if err != nil {
return nil, err
}
return nil, nil
}
}
func listUserRefreshTokensEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(listUserRefreshTokensReq)
if err := req.validate(); err != nil {
return listUserRefreshTokensRes{}, err
}
refreshTokens, err := svc.ListUserRefreshTokens(ctx, req.userID)
if err != nil {
return listUserRefreshTokensRes{}, err
}
return listUserRefreshTokensRes{refreshTokens: refreshTokens}, nil
}
}
+97 -21
View File
@@ -24,24 +24,10 @@ import (
)
const (
port = 8082
secret = "secret"
email = "test@example.com"
id = "testID"
clientsType = "clients"
usersType = "users"
description = "Description"
groupName = "smqx"
adminPermission = "admin"
authoritiesObj = "authorities"
memberRelation = "member"
loginDuration = 30 * time.Minute
refreshDuration = 24 * time.Hour
invalidDuration = 7 * 24 * time.Hour
validToken = "valid"
inValidToken = "invalid"
validPolicy = "valid"
port = 8082
validToken = "valid"
inValidToken = "invalid"
invalidID = "invalid"
)
var (
@@ -63,9 +49,9 @@ func startGRPCServer(svc auth.Service, port int) *grpc.Server {
func TestIssue(t *testing.T) {
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
defer conn.Close()
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
defer conn.Close()
cases := []struct {
desc string
@@ -127,9 +113,9 @@ func TestIssue(t *testing.T) {
func TestRefresh(t *testing.T) {
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
defer conn.Close()
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
defer conn.Close()
cases := []struct {
desc string
@@ -161,9 +147,99 @@ func TestRefresh(t *testing.T) {
}
for _, tc := range cases {
svcCall := svc.On("Issue", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.issueResponse, tc.err)
svcCall := svc.On("Issue", mock.Anything, mock.Anything, mock.Anything).Return(tc.issueResponse, tc.err)
_, err := grpcClient.Refresh(context.Background(), &grpcTokenV1.RefreshReq{RefreshToken: tc.token})
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
svcCall.Unset()
}
}
func TestRevoke(t *testing.T) {
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
defer conn.Close()
cases := []struct {
desc string
id string
err error
}{
{
desc: "revoke token with valid id",
id: validID,
err: nil,
},
{
desc: "revoke token with invalid id",
id: invalidID,
err: svcerr.ErrAuthentication,
},
{
desc: "revoke token with empty id",
id: "",
err: apiutil.ErrMissingID,
},
{
desc: "revoke already revoked token",
id: validID,
err: svcerr.ErrConflict,
},
}
for _, tc := range cases {
svcCall := svc.On("RevokeToken", mock.Anything, mock.Anything, tc.id).Return(tc.err)
_, err := grpcClient.Revoke(context.Background(), &grpcTokenV1.RevokeReq{TokenId: tc.id})
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
svcCall.Unset()
}
}
func TestListUserRefreshTokens(t *testing.T) {
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
defer conn.Close()
cases := []struct {
desc string
userID string
listResponse []auth.TokenInfo
err error
}{
{
desc: "list tokens for user with valid id",
userID: validID,
listResponse: []auth.TokenInfo{
{ID: testsutil.GenerateUUID(&testing.T{}), Description: "Token 1"},
{ID: testsutil.GenerateUUID(&testing.T{}), Description: "Token 2"},
},
err: nil,
},
{
desc: "list tokens for user with empty list",
userID: validID,
listResponse: []auth.TokenInfo{},
err: nil,
},
{
desc: "list tokens with invalid user id",
userID: invalidID,
listResponse: nil,
err: svcerr.ErrAuthentication,
},
{
desc: "list tokens with empty user id",
userID: "",
listResponse: nil,
err: apiutil.ErrMissingID,
},
}
for _, tc := range cases {
svcCall := svc.On("ListUserRefreshTokens", mock.Anything, tc.userID).Return(tc.listResponse, tc.err)
_, err := grpcClient.ListUserRefreshTokens(context.Background(), &grpcTokenV1.ListUserRefreshTokensReq{UserId: tc.userID})
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
svcCall.Unset()
}
}
+30 -4
View File
@@ -9,10 +9,11 @@ import (
)
type issueReq struct {
userID string
userRole auth.Role
keyType auth.KeyType
verified bool
userID string
userRole auth.Role
keyType auth.KeyType
verified bool
description string
}
func (req issueReq) validate() error {
@@ -38,3 +39,28 @@ func (req refreshReq) validate() error {
return nil
}
type revokeReq struct {
userID string
tokenID string
}
func (req revokeReq) validate() error {
if req.tokenID == "" {
return apiutil.ErrMissingID
}
return nil
}
type listUserRefreshTokensReq struct {
userID string
}
func (req listUserRefreshTokensReq) validate() error {
if req.userID == "" {
return apiutil.ErrMissingID
}
return nil
}
+6
View File
@@ -3,8 +3,14 @@
package token
import "github.com/absmach/supermq/auth"
type issueRes struct {
accessToken string
refreshToken string
accessType string
}
type listUserRefreshTokensRes struct {
refreshTokens []auth.TokenInfo
}
+66 -7
View File
@@ -16,11 +16,13 @@ var _ grpcTokenV1.TokenServiceServer = (*tokenGrpcServer)(nil)
type tokenGrpcServer struct {
grpcTokenV1.UnimplementedTokenServiceServer
issue kitgrpc.Handler
refresh kitgrpc.Handler
issue kitgrpc.Handler
refresh kitgrpc.Handler
revoke kitgrpc.Handler
listUserRefreshTokens kitgrpc.Handler
}
// NewAuthServer returns new AuthnServiceServer instance.
// NewTokenServer returns new TokenServiceServer instance.
func NewTokenServer(svc auth.Service) grpcTokenV1.TokenServiceServer {
return &tokenGrpcServer{
issue: kitgrpc.NewServer(
@@ -33,6 +35,16 @@ func NewTokenServer(svc auth.Service) grpcTokenV1.TokenServiceServer {
decodeRefreshRequest,
encodeIssueResponse,
),
revoke: kitgrpc.NewServer(
(revokeEndpoint(svc)),
decodeRevokeRequest,
encodeRevokeResponse,
),
listUserRefreshTokens: kitgrpc.NewServer(
(listUserRefreshTokensEndpoint(svc)),
decodeListUserRefreshTokensRequest,
encodeListUserRefreshTokensResponse,
),
}
}
@@ -55,10 +67,11 @@ func (s *tokenGrpcServer) Refresh(ctx context.Context, req *grpcTokenV1.RefreshR
func decodeIssueRequest(_ context.Context, grpcReq any) (any, error) {
req := grpcReq.(*grpcTokenV1.IssueReq)
return issueReq{
userID: req.GetUserId(),
userRole: auth.Role(req.GetUserRole()),
keyType: auth.KeyType(req.GetType()),
verified: req.Verified,
userID: req.GetUserId(),
userRole: auth.Role(req.GetUserRole()),
keyType: auth.KeyType(req.GetType()),
verified: req.Verified,
description: req.GetDescription(),
}, nil
}
@@ -76,3 +89,49 @@ func encodeIssueResponse(_ context.Context, grpcRes any) (any, error) {
AccessType: res.accessType,
}, nil
}
func (s *tokenGrpcServer) Revoke(ctx context.Context, req *grpcTokenV1.RevokeReq) (*grpcTokenV1.RevokeRes, error) {
_, res, err := s.revoke.ServeGRPC(ctx, req)
if err != nil {
return nil, grpcapi.EncodeError(err)
}
return res.(*grpcTokenV1.RevokeRes), nil
}
func decodeRevokeRequest(_ context.Context, grpcReq any) (any, error) {
req := grpcReq.(*grpcTokenV1.RevokeReq)
return revokeReq{userID: req.GetUserId(), tokenID: req.GetTokenId()}, nil
}
func encodeRevokeResponse(_ context.Context, grpcRes any) (any, error) {
return &grpcTokenV1.RevokeRes{}, nil
}
func (s *tokenGrpcServer) ListUserRefreshTokens(ctx context.Context, req *grpcTokenV1.ListUserRefreshTokensReq) (*grpcTokenV1.ListUserRefreshTokensRes, error) {
_, res, err := s.listUserRefreshTokens.ServeGRPC(ctx, req)
if err != nil {
return nil, grpcapi.EncodeError(err)
}
return res.(*grpcTokenV1.ListUserRefreshTokensRes), nil
}
func decodeListUserRefreshTokensRequest(_ context.Context, grpcReq any) (any, error) {
req := grpcReq.(*grpcTokenV1.ListUserRefreshTokensReq)
return listUserRefreshTokensReq{userID: req.GetUserId()}, nil
}
func encodeListUserRefreshTokensResponse(_ context.Context, grpcRes any) (any, error) {
res := grpcRes.(listUserRefreshTokensRes)
refreshTokens := make([]*grpcTokenV1.RefreshToken, len(res.refreshTokens))
for i, refreshToken := range res.refreshTokens {
refreshTokens[i] = &grpcTokenV1.RefreshToken{
Id: refreshToken.ID,
Description: refreshToken.Description,
}
}
return &grpcTokenV1.ListUserRefreshTokensRes{
RefreshTokens: refreshTokens,
}, nil
}
+12
View File
@@ -85,3 +85,15 @@ func revokeEndpoint(svc auth.Service) endpoint.Endpoint {
return revokeKeyRes{}, nil
}
}
func retrieveJWKSEndpoint(svc auth.Service, jwksCacheMaxAge, jwksCacheStaleWhileRevalidate int) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
jwks := svc.RetrieveJWKS()
return retrieveJWKSRes{
Keys: jwks,
CacheMaxAge: jwksCacheMaxAge,
CacheStaleWhileRevalidate: jwksCacheStaleWhileRevalidate,
}, nil
}
}
+99 -94
View File
@@ -4,7 +4,6 @@
package keys_test
import (
"context"
"encoding/json"
"fmt"
"io"
@@ -17,30 +16,23 @@ import (
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/auth"
httpapi "github.com/absmach/supermq/auth/api/http"
"github.com/absmach/supermq/auth/jwt"
"github.com/absmach/supermq/auth/mocks"
smqlog "github.com/absmach/supermq/logger"
svcerr "github.com/absmach/supermq/pkg/errors/service"
policymocks "github.com/absmach/supermq/pkg/policies/mocks"
"github.com/absmach/supermq/pkg/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
const (
secret = "secret"
contentType = "application/json"
id = "123e4567-e89b-12d3-a456-000000000001"
email = "user@example.com"
loginDuration = 30 * time.Minute
refreshDuration = 24 * time.Hour
invalidDuration = 7 * 24 * time.Hour
accessToken = "valid token"
)
var (
krepo *mocks.KeyRepository
pEvaluator *policymocks.Evaluator
)
var Token = auth.Token{
AccessToken: accessToken,
}
type issueRequest struct {
Duration time.Duration `json:"duration,omitempty"`
@@ -72,22 +64,11 @@ func (tr testRequest) make() (*http.Response, error) {
return tr.client.Do(req)
}
func newService() auth.Service {
krepo = new(mocks.KeyRepository)
pRepo := new(mocks.PATSRepository)
cache := new(mocks.Cache)
hash := new(mocks.Hasher)
idProvider := uuid.NewMock()
pService := new(policymocks.Service)
pEvaluator = new(policymocks.Evaluator)
t := jwt.New([]byte(secret))
func newServer() (*httptest.Server, *mocks.Service) {
svc := new(mocks.Service)
mux := httpapi.MakeHandler(svc, smqlog.NewMock(), "", 900, 60)
return auth.New(krepo, pRepo, cache, hash, idProvider, t, pEvaluator, pService, loginDuration, refreshDuration, invalidDuration)
}
func newServer(svc auth.Service) *httptest.Server {
mux := httpapi.MakeHandler(svc, smqlog.NewMock(), "")
return httptest.NewServer(mux)
return httptest.NewServer(mux), svc
}
func toJSON(data any) string {
@@ -99,13 +80,7 @@ func toJSON(data any) string {
}
func TestIssue(t *testing.T) {
svc := newService()
policyCall := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(nil)
token, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, Role: auth.UserRole, IssuedAt: time.Now(), Subject: id})
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
policyCall.Unset()
ts := newServer(svc)
ts, svc := newServer()
defer ts.Close()
client := ts.Client()
@@ -119,6 +94,8 @@ func TestIssue(t *testing.T) {
ct string
token string
status int
svcRes auth.Token
svcErr error
}{
{
desc: "issue login key with empty token",
@@ -131,28 +108,30 @@ func TestIssue(t *testing.T) {
desc: "issue API key",
req: toJSON(ak),
ct: contentType,
token: token.AccessToken,
token: accessToken,
status: http.StatusCreated,
svcRes: Token,
},
{
desc: "issue recovery key",
req: toJSON(rk),
ct: contentType,
token: token.AccessToken,
token: accessToken,
status: http.StatusCreated,
svcRes: Token,
},
{
desc: "issue login key wrong content type",
req: toJSON(lk),
ct: "",
token: token.AccessToken,
token: accessToken,
status: http.StatusUnsupportedMediaType,
},
{
desc: "issue recovery key wrong content type",
req: toJSON(rk),
ct: "",
token: token.AccessToken,
token: accessToken,
status: http.StatusUnsupportedMediaType,
},
{
@@ -160,6 +139,7 @@ func TestIssue(t *testing.T) {
req: toJSON(ak),
ct: contentType,
token: "wrong",
svcErr: svcerr.ErrAuthentication,
status: http.StatusUnauthorized,
},
{
@@ -167,27 +147,28 @@ func TestIssue(t *testing.T) {
req: toJSON(rk),
ct: contentType,
token: "",
svcErr: svcerr.ErrAuthentication,
status: http.StatusUnauthorized,
},
{
desc: "issue key with invalid request",
req: "{",
ct: contentType,
token: token.AccessToken,
token: accessToken,
status: http.StatusBadRequest,
},
{
desc: "issue key with invalid JSON",
req: "{invalid}",
ct: contentType,
token: token.AccessToken,
token: accessToken,
status: http.StatusBadRequest,
},
{
desc: "issue key with invalid JSON content",
req: `{"Type":{"key":"AccessToken"}}`,
ct: contentType,
token: token.AccessToken,
token: accessToken,
status: http.StatusBadRequest,
},
}
@@ -201,30 +182,16 @@ func TestIssue(t *testing.T) {
token: tc.token,
body: strings.NewReader(tc.req),
}
repoCall := krepo.On("Save", mock.Anything, mock.Anything).Return("", nil)
policyCall := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(nil)
svcCall := svc.On("Issue", mock.Anything, tc.token, mock.Anything).Return(tc.svcRes, tc.svcErr)
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
repoCall.Unset()
policyCall.Unset()
svcCall.Unset()
}
}
func TestRetrieve(t *testing.T) {
svc := newService()
policyCall := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(nil)
token, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, Role: auth.UserRole, IssuedAt: time.Now(), Subject: id})
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
key := auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), Subject: id}
repoCall := krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, nil)
k, err := svc.Issue(context.Background(), token.AccessToken, key)
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
repoCall.Unset()
policyCall.Unset()
ts := newServer(svc)
ts, svc := newServer()
defer ts.Close()
client := ts.Client()
@@ -234,12 +201,13 @@ func TestRetrieve(t *testing.T) {
token string
key auth.Key
status int
err error
svcRes auth.Key
svcErr error
}{
{
desc: "retrieve an existing key",
id: k.AccessToken,
token: token.AccessToken,
id: id,
token: accessToken,
key: auth.Key{
Subject: id,
Type: auth.AccessKey,
@@ -247,28 +215,28 @@ func TestRetrieve(t *testing.T) {
ExpiresAt: time.Now().Add(refreshDuration),
},
status: http.StatusOK,
err: nil,
svcErr: nil,
},
{
desc: "retrieve a non-existing key",
id: "non-existing",
token: token.AccessToken,
status: http.StatusBadRequest,
err: svcerr.ErrNotFound,
token: accessToken,
status: http.StatusNotFound,
svcErr: svcerr.ErrNotFound,
},
{
desc: "retrieve a key with an invalid token",
id: k.AccessToken,
id: accessToken,
token: "wrong",
status: http.StatusUnauthorized,
err: svcerr.ErrAuthentication,
svcErr: svcerr.ErrAuthentication,
},
{
desc: "retrieve a key with an empty token",
token: "",
id: k.AccessToken,
id: accessToken,
status: http.StatusUnauthorized,
err: svcerr.ErrAuthentication,
svcErr: svcerr.ErrAuthentication,
},
}
@@ -279,30 +247,16 @@ func TestRetrieve(t *testing.T) {
url: fmt.Sprintf("%s/keys/%s", ts.URL, tc.id),
token: tc.token,
}
policyCall := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(nil)
repoCall := krepo.On("Retrieve", mock.Anything, mock.Anything, mock.Anything).Return(tc.key, tc.err)
svcCall := svc.On("RetrieveKey", mock.Anything, tc.token, tc.id).Return(tc.svcRes, tc.svcErr)
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
repoCall.Unset()
policyCall.Unset()
svcCall.Unset()
}
}
func TestRevoke(t *testing.T) {
svc := newService()
policyCall := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(nil)
token, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, Role: auth.UserRole, IssuedAt: time.Now(), Subject: id})
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
key := auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), Subject: id}
repoCall := krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, nil)
k, err := svc.Issue(context.Background(), token.AccessToken, key)
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
repoCall.Unset()
policyCall.Unset()
ts := newServer(svc)
ts, svc := newServer()
defer ts.Close()
client := ts.Client()
@@ -311,29 +265,33 @@ func TestRevoke(t *testing.T) {
id string
token string
status int
svcErr error
}{
{
desc: "revoke an existing key",
id: k.AccessToken,
token: token.AccessToken,
id: id,
token: accessToken,
status: http.StatusNoContent,
},
{
desc: "revoke a non-existing key",
id: "non-existing",
token: token.AccessToken,
status: http.StatusNoContent,
token: accessToken,
svcErr: svcerr.ErrNotFound,
status: http.StatusNotFound,
},
{
desc: "revoke key with invalid token",
id: k.AccessToken,
id: id,
token: "wrong",
svcErr: svcerr.ErrAuthentication,
status: http.StatusUnauthorized,
},
{
desc: "revoke key with empty token",
id: k.AccessToken,
id: id,
token: "",
svcErr: svcerr.ErrAuthentication,
status: http.StatusUnauthorized,
},
}
@@ -345,10 +303,57 @@ func TestRevoke(t *testing.T) {
url: fmt.Sprintf("%s/keys/%s", ts.URL, tc.id),
token: tc.token,
}
repoCall := krepo.On("Remove", mock.Anything, mock.Anything, mock.Anything).Return(nil)
svcCall := svc.On("Revoke", mock.Anything, tc.token, tc.id).Return(tc.svcErr)
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
repoCall.Unset()
svcCall.Unset()
}
}
func TestRetrieveJWKS(t *testing.T) {
ts, svc := newServer()
defer ts.Close()
client := ts.Client()
cases := []struct {
desc string
svcRes []auth.PublicKeyInfo
status int
}{
{
desc: "retrieve JWKS with keys",
svcRes: []auth.PublicKeyInfo{newPublicKeyInfo(), newPublicKeyInfo()},
status: http.StatusOK,
},
{
desc: "retrieve empty JWKS",
svcRes: []auth.PublicKeyInfo{},
status: http.StatusOK,
},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodGet,
url: fmt.Sprintf("%s/keys/.well-known/jwks.json", ts.URL),
}
svcCall := svc.On("RetrieveJWKS").Return(tc.svcRes)
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
svcCall.Unset()
}
}
func newPublicKeyInfo() auth.PublicKeyInfo {
return auth.PublicKeyInfo{
KeyID: "test-key-id",
KeyType: "OKP",
Algorithm: "EdDSA",
Use: "sig",
Curve: "Ed25519",
X: "base64url-encoded-public-key",
}
}
+2
View File
@@ -46,3 +46,5 @@ func (req keyReq) validate() error {
}
return nil
}
type jwksReq struct{}
+35
View File
@@ -4,6 +4,8 @@
package keys
import (
"encoding/json"
"fmt"
"net/http"
"time"
@@ -14,6 +16,8 @@ import (
var (
_ supermq.Response = (*issueKeyRes)(nil)
_ supermq.Response = (*revokeKeyRes)(nil)
_ supermq.Response = (*retrieveKeyRes)(nil)
_ supermq.Response = (*retrieveJWKSRes)(nil)
)
type issueKeyRes struct {
@@ -69,3 +73,34 @@ func (res revokeKeyRes) Headers() map[string]string {
func (res revokeKeyRes) Empty() bool {
return true
}
type retrieveJWKSRes struct {
Keys []auth.PublicKeyInfo `json:"-"`
CacheMaxAge int `json:"-"`
CacheStaleWhileRevalidate int `json:"-"`
}
func (res retrieveJWKSRes) MarshalJSON() ([]byte, error) {
type jwksResponse struct {
Keys []auth.PublicKeyInfo `json:"keys"`
}
return json.Marshal(jwksResponse{Keys: res.Keys})
}
func (res retrieveJWKSRes) Code() int {
return http.StatusOK
}
func (res retrieveJWKSRes) Headers() map[string]string {
cacheControl := fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d", res.CacheMaxAge, res.CacheStaleWhileRevalidate)
headers := map[string]string{
"Cache-Control": cacheControl,
}
return headers
}
func (res retrieveJWKSRes) Empty() bool {
return false
}
+14 -2
View File
@@ -21,7 +21,7 @@ import (
const contentType = "application/json"
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc auth.Service, mux *chi.Mux, logger *slog.Logger) *chi.Mux {
func MakeHandler(svc auth.Service, mux *chi.Mux, logger *slog.Logger, jwksCacheMaxAge, jwksCacheStaleWhileRevalidate int) *chi.Mux {
opts := []kithttp.ServerOption{
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
}
@@ -46,6 +46,13 @@ func MakeHandler(svc auth.Service, mux *chi.Mux, logger *slog.Logger) *chi.Mux {
api.EncodeResponse,
opts...,
).ServeHTTP)
r.Get("/.well-known/jwks.json", kithttp.NewServer(
retrieveJWKSEndpoint(svc, jwksCacheMaxAge, jwksCacheStaleWhileRevalidate),
decodeJWKSReq,
api.EncodeResponse,
opts...,
).ServeHTTP)
})
return mux
}
@@ -57,7 +64,7 @@ func decodeIssue(_ context.Context, r *http.Request) (any, error) {
req := issueKeyReq{token: apiutil.ExtractBearerToken(r)}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
}
return req, nil
@@ -70,3 +77,8 @@ func decodeKeyReq(_ context.Context, r *http.Request) (any, error) {
}
return req, nil
}
func decodeJWKSReq(_ context.Context, _ *http.Request) (any, error) {
req := jwksReq{}
return req, nil
}
+724
View File
@@ -0,0 +1,724 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package pats
import (
"encoding/json"
"testing"
"time"
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/auth"
"github.com/stretchr/testify/assert"
)
var valid = "valid"
func TestCreatePatReqValidate(t *testing.T) {
cases := []struct {
desc string
req createPatReq
err error
}{
{
desc: "valid request",
req: createPatReq{
token: valid,
Name: "test-pat",
Description: "test description",
Duration: 24 * time.Hour,
},
err: nil,
},
{
desc: "empty token",
req: createPatReq{
token: "",
Name: "test-pat",
Description: "test description",
Duration: 24 * time.Hour,
},
err: apiutil.ErrBearerToken,
},
{
desc: "empty name",
req: createPatReq{
token: valid,
Name: "",
Description: "test description",
Duration: 24 * time.Hour,
},
err: apiutil.ErrMissingName,
},
{
desc: "whitespace only name",
req: createPatReq{
token: valid,
Name: " ",
Description: "test description",
Duration: 24 * time.Hour,
},
err: apiutil.ErrMissingName,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.req.validate()
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
})
}
}
func TestCreatePatReqUnmarshalJSON(t *testing.T) {
cases := []struct {
desc string
data string
expected createPatReq
err bool
}{
{
desc: "valid JSON with duration",
data: `{"name":"test-pat","description":"test desc","duration":"24h"}`,
expected: createPatReq{
Name: "test-pat",
Description: "test desc",
Duration: 24 * time.Hour,
},
err: false,
},
{
desc: "invalid duration format",
data: `{"name":"test-pat","description":"test desc","duration":"invalid"}`,
err: true,
},
{
desc: "invalid JSON",
data: `{invalid json}`,
err: true,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
var req createPatReq
err := json.Unmarshal([]byte(tc.data), &req)
if tc.err {
assert.Error(t, err, "UnmarshalJSON() should return error")
} else {
assert.NoError(t, err, "UnmarshalJSON() should not return error")
assert.Equal(t, tc.expected.Name, req.Name)
assert.Equal(t, tc.expected.Description, req.Description)
assert.Equal(t, tc.expected.Duration, req.Duration)
}
})
}
}
func TestRetrievePatReqValidate(t *testing.T) {
cases := []struct {
desc string
req retrievePatReq
err error
}{
{
desc: "valid request",
req: retrievePatReq{
token: valid,
id: "pat-id",
},
err: nil,
},
{
desc: "empty token",
req: retrievePatReq{
token: "",
id: "pat-id",
},
err: apiutil.ErrBearerToken,
},
{
desc: "empty id",
req: retrievePatReq{
token: valid,
id: "",
},
err: apiutil.ErrMissingPATID,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.req.validate()
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
})
}
}
func TestUpdatePatNameReqValidate(t *testing.T) {
cases := []struct {
desc string
req updatePatNameReq
err error
}{
{
desc: "valid request",
req: updatePatNameReq{
token: valid,
id: "pat-id",
Name: "new-name",
},
err: nil,
},
{
desc: "empty token",
req: updatePatNameReq{
token: "",
id: "pat-id",
Name: "new-name",
},
err: apiutil.ErrBearerToken,
},
{
desc: "empty id",
req: updatePatNameReq{
token: valid,
id: "",
Name: "new-name",
},
err: apiutil.ErrMissingPATID,
},
{
desc: "empty name",
req: updatePatNameReq{
token: valid,
id: "pat-id",
Name: "",
},
err: apiutil.ErrMissingName,
},
{
desc: "whitespace only name",
req: updatePatNameReq{
token: valid,
id: "pat-id",
Name: " ",
},
err: apiutil.ErrMissingName,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.req.validate()
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
})
}
}
func TestUpdatePatDescriptionReqValidate(t *testing.T) {
cases := []struct {
desc string
req updatePatDescriptionReq
err error
}{
{
desc: "valid request",
req: updatePatDescriptionReq{
token: valid,
id: "pat-id",
Description: "new description",
},
err: nil,
},
{
desc: "empty token",
req: updatePatDescriptionReq{
token: "",
id: "pat-id",
Description: "new description",
},
err: apiutil.ErrBearerToken,
},
{
desc: "empty id",
req: updatePatDescriptionReq{
token: valid,
id: "",
Description: "new description",
},
err: apiutil.ErrMissingPATID,
},
{
desc: "empty description",
req: updatePatDescriptionReq{
token: valid,
id: "pat-id",
Description: "",
},
err: apiutil.ErrMissingDescription,
},
{
desc: "whitespace only description",
req: updatePatDescriptionReq{
token: valid,
id: "pat-id",
Description: " ",
},
err: apiutil.ErrMissingDescription,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.req.validate()
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
})
}
}
func TestListPatsReqValidate(t *testing.T) {
cases := []struct {
desc string
req listPatsReq
err error
}{
{
desc: "valid request",
req: listPatsReq{
token: valid,
offset: 0,
limit: 10,
},
err: nil,
},
{
desc: "empty token",
req: listPatsReq{
token: "",
offset: 0,
limit: 10,
},
err: apiutil.ErrBearerToken,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.req.validate()
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
})
}
}
func TestDeletePatReqValidate(t *testing.T) {
cases := []struct {
desc string
req deletePatReq
err error
}{
{
desc: "valid request",
req: deletePatReq{
token: valid,
id: "pat-id",
},
err: nil,
},
{
desc: "empty token",
req: deletePatReq{
token: "",
id: "pat-id",
},
err: apiutil.ErrBearerToken,
},
{
desc: "empty id",
req: deletePatReq{
token: valid,
id: "",
},
err: apiutil.ErrMissingPATID,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.req.validate()
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
})
}
}
func TestResetPatSecretReqValidate(t *testing.T) {
cases := []struct {
desc string
req resetPatSecretReq
err error
}{
{
desc: "valid request",
req: resetPatSecretReq{
token: valid,
id: "pat-id",
Duration: 24 * time.Hour,
},
err: nil,
},
{
desc: "empty token",
req: resetPatSecretReq{
token: "",
id: "pat-id",
Duration: 24 * time.Hour,
},
err: apiutil.ErrBearerToken,
},
{
desc: "empty id",
req: resetPatSecretReq{
token: valid,
id: "",
Duration: 24 * time.Hour,
},
err: apiutil.ErrMissingPATID,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.req.validate()
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
})
}
}
func TestResetPatSecretReqUnmarshalJSON(t *testing.T) {
cases := []struct {
desc string
data string
expected resetPatSecretReq
err bool
}{
{
desc: "valid JSON with duration",
data: `{"duration":"48h"}`,
expected: resetPatSecretReq{
Duration: 48 * time.Hour,
},
err: false,
},
{
desc: "invalid duration format",
data: `{"duration":"invalid"}`,
err: true,
},
{
desc: "invalid JSON",
data: `{invalid}`,
err: true,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
var req resetPatSecretReq
err := json.Unmarshal([]byte(tc.data), &req)
if tc.err {
assert.Error(t, err, "UnmarshalJSON() should return error")
} else {
assert.NoError(t, err, "UnmarshalJSON() should not return error")
assert.Equal(t, tc.expected.Duration, req.Duration)
}
})
}
}
func TestRevokePatSecretReqValidate(t *testing.T) {
cases := []struct {
desc string
req revokePatSecretReq
err error
}{
{
desc: "valid request",
req: revokePatSecretReq{
token: valid,
id: "pat-id",
},
err: nil,
},
{
desc: "empty token",
req: revokePatSecretReq{
token: "",
id: "pat-id",
},
err: apiutil.ErrBearerToken,
},
{
desc: "empty id",
req: revokePatSecretReq{
token: valid,
id: "",
},
err: apiutil.ErrMissingPATID,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.req.validate()
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
})
}
}
func TestClearAllPATReqValidate(t *testing.T) {
cases := []struct {
desc string
req clearAllPATReq
err error
}{
{
desc: "valid request",
req: clearAllPATReq{
token: valid,
},
err: nil,
},
{
desc: "empty token",
req: clearAllPATReq{
token: "",
},
err: apiutil.ErrBearerToken,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.req.validate()
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
})
}
}
func TestAddScopeReqValidate(t *testing.T) {
validScope := auth.Scope{
DomainID: "domain1",
EntityType: auth.GroupsType,
EntityID: "entity1",
Operation: "create",
}
invalidScope := auth.Scope{
DomainID: "",
EntityType: auth.GroupsType,
EntityID: "",
Operation: "view",
}
cases := []struct {
desc string
req addScopeReq
err error
}{
{
desc: "valid request",
req: addScopeReq{
token: valid,
id: "pat-id",
Scopes: []auth.Scope{validScope},
},
err: nil,
},
{
desc: "empty token",
req: addScopeReq{
token: "",
id: "pat-id",
Scopes: []auth.Scope{validScope},
},
err: apiutil.ErrBearerToken,
},
{
desc: "empty id",
req: addScopeReq{
token: valid,
id: "",
Scopes: []auth.Scope{validScope},
},
err: apiutil.ErrMissingPATID,
},
{
desc: "empty scopes",
req: addScopeReq{
token: valid,
id: "pat-id",
Scopes: []auth.Scope{},
},
err: apiutil.ErrValidation,
},
{
desc: "invalid scope",
req: addScopeReq{
token: valid,
id: "pat-id",
Scopes: []auth.Scope{invalidScope},
},
err: apiutil.ErrValidation,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.req.validate()
if tc.err != nil {
assert.Error(t, err, "validate() should return error")
} else {
assert.NoError(t, err, "validate() should not return error")
}
})
}
}
func TestRemoveScopeReqValidate(t *testing.T) {
cases := []struct {
desc string
req removeScopeReq
err error
}{
{
desc: "valid request",
req: removeScopeReq{
token: valid,
id: "pat-id",
ScopesID: []string{"scope1", "scope2"},
},
err: nil,
},
{
desc: "empty token",
req: removeScopeReq{
token: "",
id: "pat-id",
ScopesID: []string{"scope1"},
},
err: apiutil.ErrBearerToken,
},
{
desc: "empty id",
req: removeScopeReq{
token: valid,
id: "",
ScopesID: []string{"scope1"},
},
err: apiutil.ErrMissingPATID,
},
{
desc: "empty scopes list",
req: removeScopeReq{
token: valid,
id: "pat-id",
ScopesID: []string{},
},
err: apiutil.ErrValidation,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.req.validate()
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
})
}
}
func TestClearAllScopeReqValidate(t *testing.T) {
cases := []struct {
desc string
req clearAllScopeReq
err error
}{
{
desc: "valid request",
req: clearAllScopeReq{
token: valid,
id: "pat-id",
},
err: nil,
},
{
desc: "empty token",
req: clearAllScopeReq{
token: "",
id: "pat-id",
},
err: apiutil.ErrBearerToken,
},
{
desc: "empty id",
req: clearAllScopeReq{
token: valid,
id: "",
},
err: apiutil.ErrMissingPATID,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.req.validate()
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
})
}
}
func TestListScopesReqValidate(t *testing.T) {
cases := []struct {
desc string
req listScopesReq
err error
}{
{
desc: "valid request",
req: listScopesReq{
token: valid,
offset: 0,
limit: 10,
patID: "pat-id",
},
err: nil,
},
{
desc: "empty token",
req: listScopesReq{
token: "",
offset: 0,
limit: 10,
patID: "pat-id",
},
err: apiutil.ErrBearerToken,
},
{
desc: "empty patID",
req: listScopesReq{
token: valid,
offset: 0,
limit: 10,
patID: "",
},
err: apiutil.ErrMissingPATID,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.req.validate()
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
})
}
}
+6 -6
View File
@@ -140,7 +140,7 @@ func decodeCreatePATRequest(_ context.Context, r *http.Request) (any, error) {
}
req := createPatReq{token: token}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
}
return req, nil
}
@@ -171,7 +171,7 @@ func decodeUpdatePATNameRequest(_ context.Context, r *http.Request) (any, error)
id: chi.URLParam(r, "id"),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
}
return req, nil
}
@@ -190,7 +190,7 @@ func decodeUpdatePATDescriptionRequest(_ context.Context, r *http.Request) (any,
id: chi.URLParam(r, "id"),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
}
return req, nil
}
@@ -262,7 +262,7 @@ func decodeResetPATSecretRequest(_ context.Context, r *http.Request) (any, error
id: chi.URLParam(r, "id"),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
}
return req, nil
}
@@ -305,7 +305,7 @@ func decodeAddScopeRequest(_ context.Context, r *http.Request) (any, error) {
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
}
return req, nil
@@ -348,7 +348,7 @@ func decodeRemoveScopeRequest(_ context.Context, r *http.Request) (any, error) {
id: chi.URLParam(r, "id"),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
}
return req, nil
}
+2 -2
View File
@@ -15,10 +15,10 @@ import (
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc auth.Service, logger *slog.Logger, instanceID string) http.Handler {
func MakeHandler(svc auth.Service, logger *slog.Logger, instanceID string, jwksCacheMaxAge, jwksCacheStaleWhileRevalidate int) http.Handler {
mux := chi.NewRouter()
mux = keys.MakeHandler(svc, mux, logger)
mux = keys.MakeHandler(svc, mux, logger, jwksCacheMaxAge, jwksCacheStaleWhileRevalidate)
mux = pats.MakeHandler(svc, mux, logger)
mux.Get("/health", supermq.Health("auth", instanceID))
+2
View File
@@ -1,4 +1,6 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package cache contains the domain concept definitions needed to
// support SuperMQ auth cache service functionality.
package cache
+6 -6
View File
@@ -28,7 +28,7 @@ func NewPatsCache(client *redis.Client, duration time.Duration) auth.Cache {
func (pc *patCache) Save(ctx context.Context, userID string, scopes []auth.Scope) error {
for _, sc := range scopes {
key := generateKey(userID, sc.PatID, sc.OptionalDomainID, sc.EntityType, sc.Operation, sc.EntityID)
key := generateKey(userID, sc.PatID, sc.DomainID, sc.EntityType, sc.Operation, sc.EntityID)
if err := pc.client.Set(ctx, key, sc.ID, pc.duration).Err(); err != nil {
return errors.Wrap(repoerr.ErrCreateEntity, err)
}
@@ -37,9 +37,9 @@ func (pc *patCache) Save(ctx context.Context, userID string, scopes []auth.Scope
return nil
}
func (pc *patCache) CheckScope(ctx context.Context, userID, patID, optionalDomainID string, entityType auth.EntityType, operation auth.Operation, entityID string) bool {
exactKey := fmt.Sprintf("pat:%s:%s:%s:%s:%s:%s", userID, patID, entityType, optionalDomainID, operation, entityID)
wildcardKey := fmt.Sprintf("pat:%s:%s:%s:%s:%s:*", userID, patID, entityType, operation, operation)
func (pc *patCache) CheckScope(ctx context.Context, userID, patID, domainID string, entityType auth.EntityType, operation string, entityID string) bool {
exactKey := fmt.Sprintf("pat:%s:%s:%s:%s:%s:%s", userID, patID, entityType, domainID, operation, entityID)
wildcardKey := fmt.Sprintf("pat:%s:%s:%s:%s:%s:*", userID, patID, entityType, domainID, operation)
res, err := pc.client.Exists(ctx, exactKey, wildcardKey).Result()
if err != nil {
@@ -115,6 +115,6 @@ func (pc *patCache) RemoveAllScope(ctx context.Context, userID, patID string) er
return nil
}
func generateKey(userID, patID, optionalDomainId string, entityType auth.EntityType, operation auth.Operation, entityID string) string {
return fmt.Sprintf("pat:%s:%s:%s:%s:%s:%s", userID, patID, entityType, optionalDomainId, operation, entityID)
func generateKey(userID, patID, domainId string, entityType auth.EntityType, operation string, entityID string) string {
return fmt.Sprintf("pat:%s:%s:%s:%s:%s:%s", userID, patID, entityType, domainId, operation, entityID)
}
+132
View File
@@ -0,0 +1,132 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cache
import (
"context"
"strconv"
"time"
"github.com/absmach/supermq/auth"
"github.com/absmach/supermq/pkg/errors"
"github.com/redis/go-redis/v9"
)
const (
refreshPrefix = "refresh_tokens:"
scoreNegInf = "-inf"
scorePosInf = "+inf"
)
var _ auth.UserActiveTokensCache = (*tokensCache)(nil)
type tokensCache struct {
client *redis.Client
keyDuration time.Duration
}
// NewUserActiveTokensCache returns redis auth cache implementation.
func NewUserActiveTokensCache(client *redis.Client, duration time.Duration) (auth.UserActiveTokensCache, error) {
if duration == 0 {
return nil, errors.New("token cache duration must not be zero")
}
return &tokensCache{
client: client,
keyDuration: duration,
}, nil
}
// SaveActive saves an active refresh token ID for a user with optional description.
func (tc *tokensCache) SaveActive(ctx context.Context, userID, tokenID, description string, expiry time.Time) error {
ttl := min(tc.keyDuration, time.Until(expiry))
pipe := tc.client.TxPipeline()
pipe.Set(ctx, tokenKey(tokenID), description, ttl)
pipe.ZAdd(ctx, userTokensKey(userID), redis.Z{
Score: float64(time.Now().Add(ttl).Unix()),
Member: tokenID,
})
_, err := pipe.Exec(ctx)
return err
}
// IsActive checks if the token ID is active for the given user.
func (tc *tokensCache) IsActive(ctx context.Context, tokenID string) (bool, error) {
count, err := tc.client.Exists(ctx, tokenKey(tokenID)).Result()
if err != nil {
return false, err
}
return count > 0, nil
}
// ListUserTokens lists all active refresh token IDs with descriptions for a user.
func (tc *tokensCache) ListUserTokens(ctx context.Context, userID string) ([]auth.TokenInfo, error) {
key := userTokensKey(userID)
now := strconv.FormatInt(time.Now().Unix(), 10)
pipe := tc.client.TxPipeline()
pipe.ZRemRangeByScore(ctx, key, scoreNegInf, now)
zrangeCmd := pipe.ZRangeByScore(ctx, key, &redis.ZRangeBy{Min: "(" + now, Max: scorePosInf})
if _, err := pipe.Exec(ctx); err != nil && err != redis.Nil {
return nil, err
}
tokenIDs, err := zrangeCmd.Result()
if err != nil {
return nil, err
}
if len(tokenIDs) == 0 {
return nil, nil
}
getPipe := tc.client.Pipeline()
getCmds := make([]*redis.StringCmd, len(tokenIDs))
for i, tokenID := range tokenIDs {
getCmds[i] = getPipe.Get(ctx, tokenKey(tokenID))
}
if _, err = getPipe.Exec(ctx); err != nil && err != redis.Nil {
return nil, err
}
valid := make([]auth.TokenInfo, 0, len(tokenIDs))
for i, cmd := range getCmds {
description, err := cmd.Result()
if err == redis.Nil {
continue
}
if err != nil {
return nil, err
}
valid = append(valid, auth.TokenInfo{
ID: tokenIDs[i],
Description: description,
})
}
return valid, nil
}
// RemoveActive removes an active refresh token ID for a user.
func (tc *tokensCache) RemoveActive(ctx context.Context, userID, tokenID string) error {
pipe := tc.client.TxPipeline()
pipe.Del(ctx, tokenKey(tokenID))
pipe.ZRem(ctx, userTokensKey(userID), tokenID)
_, err := pipe.Exec(ctx)
return err
}
func tokenKey(tokenID string) string {
return refreshPrefix + "token:" + tokenID
}
func userTokensKey(userID string) string {
return refreshPrefix + "user_tokens:" + userID
}
+288
View File
@@ -0,0 +1,288 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cache_test
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/absmach/supermq/auth"
"github.com/absmach/supermq/auth/cache"
"github.com/absmach/supermq/internal/testsutil"
"github.com/absmach/supermq/pkg/errors"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
)
var (
storeClient *redis.Client
storeURL string
)
func TestMain(m *testing.M) {
code := testsutil.RunRedisTest(m, &storeClient, &storeURL)
os.Exit(code)
}
func setupRedisTokensClient() auth.UserActiveTokensCache {
tc, err := cache.NewUserActiveTokensCache(storeClient, 10*time.Minute)
if err != nil {
panic(err)
}
return tc
}
func TestTokenSave(t *testing.T) {
storeClient.FlushAll(context.Background())
tokensCache := setupRedisTokensClient()
userID := testsutil.GenerateUUID(t)
tokenID := testsutil.GenerateUUID(t)
cases := []struct {
desc string
userID string
tokenID string
description string
expiry time.Time
err error
}{
{
desc: "Save active token",
userID: userID,
tokenID: tokenID,
description: "Test token",
expiry: time.Now().Add(10 * time.Minute),
err: nil,
},
{
desc: "Save already cached token",
userID: userID,
tokenID: tokenID,
description: "Updated token",
expiry: time.Now().Add(10 * time.Minute),
err: nil,
},
{
desc: "Save another token for same user",
userID: userID,
tokenID: testsutil.GenerateUUID(t),
description: "Another token",
expiry: time.Now().Add(10 * time.Minute),
err: nil,
},
{
desc: "Save token with empty id",
userID: userID,
tokenID: "",
description: "Empty ID token",
expiry: time.Now().Add(10 * time.Minute),
err: nil,
},
{
desc: "Save token with empty description",
userID: userID,
tokenID: testsutil.GenerateUUID(t),
description: "",
expiry: time.Now().Add(10 * time.Minute),
err: nil,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tokensCache.SaveActive(context.Background(), tc.userID, tc.tokenID, tc.description, tc.expiry)
if err == nil {
ok, err := tokensCache.IsActive(context.Background(), tc.tokenID)
assert.NoError(t, err)
assert.True(t, ok)
}
assert.True(t, errors.Contains(err, tc.err))
})
}
}
func TestTokenContains(t *testing.T) {
storeClient.FlushAll(context.Background())
tokensCache := setupRedisTokensClient()
userID := testsutil.GenerateUUID(t)
tokenID := testsutil.GenerateUUID(t)
err := tokensCache.SaveActive(context.Background(), userID, tokenID, "Test token", time.Now().Add(10*time.Minute))
assert.Nil(t, err, fmt.Sprintf("Unexpected error while trying to save: %s", err))
cases := []struct {
desc string
userID string
tokenID string
ok bool
}{
{
desc: "IsActive for existing token",
userID: userID,
tokenID: tokenID,
ok: true,
},
{
desc: "IsActive for non existing token",
userID: userID,
tokenID: testsutil.GenerateUUID(t),
},
{
desc: "IsActive with empty token id",
userID: userID,
tokenID: "",
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
ok, err := tokensCache.IsActive(context.Background(), tc.tokenID)
if tc.ok {
assert.NoError(t, err)
}
assert.Equal(t, tc.ok, ok)
})
}
}
func TestTokenRemove(t *testing.T) {
storeClient.FlushAll(context.Background())
tokensCache := setupRedisTokensClient()
userID := testsutil.GenerateUUID(t)
num := 10
var tokenIDs []string
for i := range num {
tokenID := testsutil.GenerateUUID(t)
err := tokensCache.SaveActive(context.Background(), userID, tokenID, fmt.Sprintf("Token %d", i), time.Now().Add(10*time.Minute))
assert.Nil(t, err, fmt.Sprintf("Unexpected error while trying to save: %s", err))
tokenIDs = append(tokenIDs, tokenID)
}
cases := []struct {
desc string
userID string
tokenID string
err error
}{
{
desc: "Remove an existing token from cache",
userID: userID,
tokenID: tokenIDs[0],
err: nil,
},
{
desc: "Remove token with empty id from cache",
userID: userID,
tokenID: "",
err: nil,
},
{
desc: "Remove non existing id from cache",
userID: userID,
tokenID: testsutil.GenerateUUID(t),
err: nil,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tokensCache.RemoveActive(context.Background(), tc.userID, tc.tokenID)
assert.True(t, errors.Contains(err, tc.err))
if err == nil {
ok, err := tokensCache.IsActive(context.Background(), tc.tokenID)
assert.NoError(t, err)
assert.False(t, ok)
}
})
}
}
func TestListUserTokens(t *testing.T) {
storeClient.FlushAll(context.Background())
tokensCache := setupRedisTokensClient()
userID := testsutil.GenerateUUID(t)
userID2 := testsutil.GenerateUUID(t)
num := 5
var expectedTokens []auth.TokenInfo
for i := range num {
tokenID := testsutil.GenerateUUID(t)
description := fmt.Sprintf("Token %d", i)
err := tokensCache.SaveActive(context.Background(), userID, tokenID, description, time.Now().Add(10*time.Minute))
assert.Nil(t, err, fmt.Sprintf("Unexpected error while trying to save: %s", err))
expectedTokens = append(expectedTokens, auth.TokenInfo{
ID: tokenID,
Description: description,
})
}
tokenID2 := testsutil.GenerateUUID(t)
desc2 := "User 2 token"
err := tokensCache.SaveActive(context.Background(), userID2, tokenID2, desc2, time.Now().Add(10*time.Minute))
assert.Nil(t, err, fmt.Sprintf("Unexpected error while trying to save: %s", err))
cases := []struct {
desc string
userID string
expectedCount int
expectedTokens []auth.TokenInfo
err error
}{
{
desc: "List all tokens for user with multiple tokens",
userID: userID,
expectedCount: num,
expectedTokens: expectedTokens,
err: nil,
},
{
desc: "List tokens for user with single token",
userID: userID2,
expectedCount: 1,
expectedTokens: []auth.TokenInfo{{ID: tokenID2, Description: desc2}},
err: nil,
},
{
desc: "List tokens for user with no tokens",
userID: testsutil.GenerateUUID(t),
expectedCount: 0,
expectedTokens: nil,
err: nil,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
tokens, err := tokensCache.ListUserTokens(context.Background(), tc.userID)
assert.True(t, errors.Contains(err, tc.err))
assert.Equal(t, tc.expectedCount, len(tokens))
if tc.expectedTokens != nil {
assert.ElementsMatch(t, tc.expectedTokens, tokens)
}
})
}
t.Run("Cleanup expired tokens from list", func(t *testing.T) {
// Remove one token directly from Redis to simulate expiration
err := tokensCache.RemoveActive(context.Background(), userID, expectedTokens[0].ID)
assert.NoError(t, err)
// List should now return only valid tokens
tokens, err := tokensCache.ListUserTokens(context.Background(), userID)
assert.NoError(t, err)
assert.Equal(t, num-1, len(tokens))
// Check that the removed token is not in the list
for _, token := range tokens {
assert.NotEqual(t, expectedTokens[0].ID, token.ID)
}
})
}
+1 -3
View File
@@ -4,11 +4,10 @@
package hasher
import (
"crypto/rand"
"encoding/base64"
"fmt"
"math/rand"
"strings"
"time"
"github.com/absmach/supermq/auth"
"github.com/absmach/supermq/pkg/errors"
@@ -76,7 +75,6 @@ func (bh *bcryptHasher) Compare(plain, hashed string) error {
}
func generateSalt(length int) ([]byte, error) {
rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
salt := make([]byte, length)
_, err := rand.Read(salt)
if err != nil {
-241
View File
@@ -1,241 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package jwt_test
import (
"fmt"
"testing"
"time"
"github.com/absmach/supermq/auth"
authjwt "github.com/absmach/supermq/auth/jwt"
"github.com/absmach/supermq/internal/testsutil"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
tokenType = "type"
roleField = "role"
issuerName = "supermq.auth"
secret = "test"
)
var (
errInvalidIssuer = errors.New("invalid token issuer value")
reposecret = []byte("test")
)
func newToken(issuerName string, key auth.Key) string {
builder := jwt.NewBuilder()
builder.
Issuer(issuerName).
IssuedAt(key.IssuedAt).
Claim(tokenType, "r").
Expiration(key.ExpiresAt)
builder.Claim(roleField, key.Role)
if key.Subject != "" {
builder.Subject(key.Subject)
}
if key.ID != "" {
builder.JwtID(key.ID)
}
tkn, _ := builder.Build()
tokn, _ := jwt.Sign(tkn, jwt.WithKey(jwa.HS512, reposecret))
return string(tokn)
}
func TestIssue(t *testing.T) {
tokenizer := authjwt.New([]byte(secret))
cases := []struct {
desc string
key auth.Key
err error
}{
{
desc: "issue new token",
key: key(),
err: nil,
},
{
desc: "issue token with OAuth token",
key: auth.Key{
ID: testsutil.GenerateUUID(t),
Type: auth.AccessKey,
Subject: testsutil.GenerateUUID(t),
IssuedAt: time.Now().Add(-10 * time.Second).Round(time.Second),
ExpiresAt: time.Now().Add(10 * time.Minute).Round(time.Second),
},
err: nil,
},
{
desc: "issue token without a domain",
key: auth.Key{
ID: testsutil.GenerateUUID(t),
Type: auth.AccessKey,
Subject: testsutil.GenerateUUID(t),
IssuedAt: time.Now().Add(-10 * time.Second).Round(time.Second),
},
err: nil,
},
{
desc: "issue token without a subject",
key: auth.Key{
ID: testsutil.GenerateUUID(t),
Type: auth.AccessKey,
Subject: "",
IssuedAt: time.Now().Add(-10 * time.Second).Round(time.Second),
},
err: nil,
},
{
desc: "issue token without type",
key: auth.Key{
ID: testsutil.GenerateUUID(t),
Type: auth.KeyType(auth.InvitationKey + 1),
Subject: testsutil.GenerateUUID(t),
IssuedAt: time.Now().Add(-10 * time.Second).Round(time.Second),
},
err: nil,
},
{
desc: "issue token without a domain and subject",
key: auth.Key{
ID: testsutil.GenerateUUID(t),
Type: auth.AccessKey,
Subject: "",
IssuedAt: time.Now().Add(-10 * time.Second).Round(time.Second),
ExpiresAt: time.Now().Add(10 * time.Minute).Round(time.Second),
},
err: nil,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
tkn, err := tokenizer.Issue(tc.key)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s, got %s", tc.desc, tc.err, err))
if err != nil {
assert.NotEmpty(t, tkn, fmt.Sprintf("%s expected token, got empty string", tc.desc))
}
})
}
}
func TestParse(t *testing.T) {
tokenizer := authjwt.New([]byte(secret))
token, err := tokenizer.Issue(key())
require.Nil(t, err, fmt.Sprintf("issuing key expected to succeed: %s", err))
apiKey := key()
apiKey.Type = auth.APIKey
apiKey.ExpiresAt = time.Now().UTC().Add(-1 * time.Minute).Round(time.Second)
apiToken, err := tokenizer.Issue(apiKey)
require.Nil(t, err, fmt.Sprintf("issuing user key expected to succeed: %s", err))
expKey := key()
expKey.ExpiresAt = time.Now().UTC().Add(-1 * time.Minute).Round(time.Second)
expToken, err := tokenizer.Issue(expKey)
require.Nil(t, err, fmt.Sprintf("issuing expired key expected to succeed: %s", err))
emptySubjectKey := key()
emptySubjectKey.Subject = ""
emptySubjectToken, err := tokenizer.Issue(emptySubjectKey)
require.Nil(t, err, fmt.Sprintf("issuing user key expected to succeed: %s", err))
emptyTypeKey := key()
emptyTypeKey.Type = auth.KeyType(auth.InvitationKey + 1)
emptyTypeToken, err := tokenizer.Issue(emptyTypeKey)
require.Nil(t, err, fmt.Sprintf("issuing user key expected to succeed: %s", err))
emptyKey := key()
emptyKey.Subject = ""
require.Nil(t, err, fmt.Sprintf("issuing user key expected to succeed: %s", err))
inValidToken := newToken("invalid", key())
cases := []struct {
desc string
key auth.Key
token string
err error
}{
{
desc: "parse valid key",
key: key(),
token: token,
err: nil,
},
{
desc: "parse invalid key",
key: auth.Key{},
token: "invalid",
err: svcerr.ErrAuthentication,
},
{
desc: "parse expired key",
key: auth.Key{},
token: expToken,
err: auth.ErrExpiry,
},
{
desc: "parse expired API key",
key: apiKey,
token: apiToken,
err: auth.ErrExpiry,
},
{
desc: "parse token with invalid issuer",
key: auth.Key{},
token: inValidToken,
err: errInvalidIssuer,
},
{
desc: "parse token with invalid content",
key: auth.Key{},
token: newToken(issuerName, key()),
err: authjwt.ErrJSONHandle,
},
{
desc: "parse token with empty subject",
key: emptySubjectKey,
token: emptySubjectToken,
err: nil,
},
{
desc: "parse token with empty type",
key: emptyTypeKey,
token: emptyTypeToken,
err: errors.ErrAuthentication,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
key, err := tokenizer.Parse(tc.token)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s, got %s", tc.desc, tc.err, err))
if err == nil {
assert.Equal(t, tc.key, key, fmt.Sprintf("%s expected %v, got %v", tc.desc, tc.key, key))
}
})
}
}
func key() auth.Key {
exp := time.Now().UTC().Add(10 * time.Minute).Round(time.Second)
return auth.Key{
ID: "66af4a67-3823-438a-abd7-efdb613eaef6",
Type: auth.AccessKey,
Issuer: "supermq.auth",
Role: auth.UserRole,
Subject: "66af4a67-3823-438a-abd7-efdb613eaef6",
IssuedAt: time.Now().UTC().Add(-10 * time.Second).Round(time.Second),
ExpiresAt: exp,
}
}
-182
View File
@@ -1,182 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package jwt
import (
"context"
"encoding/json"
"github.com/absmach/supermq/auth"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwt"
)
var (
// errInvalidIssuer is returned when the issuer is not supermq.auth.
errInvalidIssuer = errors.New("invalid token issuer value")
// errInvalidType is returned when there is no type field.
errInvalidType = errors.New("invalid token type")
// errInvalidRole is returned when the role is invalid.
errInvalidRole = errors.New("invalid role")
// errInvalidVerified is returned when the verified is invalid.
errInvalidVerified = errors.New("invalid verified")
// errJWTExpiryKey is used to check if the token is expired.
errJWTExpiryKey = errors.New(`"exp" not satisfied`)
// ErrSignJWT indicates an error in signing jwt token.
ErrSignJWT = errors.New("failed to sign jwt token")
// ErrValidateJWTToken indicates a failure to validate JWT token.
ErrValidateJWTToken = errors.New("failed to validate jwt token")
// ErrJSONHandle indicates an error in handling JSON.
ErrJSONHandle = errors.New("failed to perform operation JSON")
)
const (
issuerName = "supermq.auth"
tokenType = "type"
userField = "user"
RoleField = "role"
VerifiedField = "verified"
oauthProviderField = "oauth_provider"
oauthAccessTokenField = "access_token"
oauthRefreshTokenField = "refresh_token"
)
type tokenizer struct {
secret []byte
}
var _ auth.Tokenizer = (*tokenizer)(nil)
// NewRepository instantiates an implementation of Token repository.
func New(secret []byte) auth.Tokenizer {
return &tokenizer{
secret: secret,
}
}
func (tok *tokenizer) Issue(key auth.Key) (string, error) {
builder := jwt.NewBuilder()
builder.
Issuer(issuerName).
IssuedAt(key.IssuedAt).
Claim(tokenType, key.Type).
Expiration(key.ExpiresAt)
builder.Claim(RoleField, key.Role)
builder.Claim(VerifiedField, key.Verified)
if key.Subject != "" {
builder.Subject(key.Subject)
}
if key.ID != "" {
builder.JwtID(key.ID)
}
tkn, err := builder.Build()
if err != nil {
return "", errors.Wrap(svcerr.ErrAuthentication, err)
}
signedTkn, err := jwt.Sign(tkn, jwt.WithKey(jwa.HS512, tok.secret))
if err != nil {
return "", errors.Wrap(ErrSignJWT, err)
}
return string(signedTkn), nil
}
func (tok *tokenizer) Parse(token string) (auth.Key, error) {
tkn, err := tok.validateToken(token)
if err != nil {
return auth.Key{}, errors.Wrap(svcerr.ErrAuthentication, err)
}
key, err := toKey(tkn)
if err != nil {
return auth.Key{}, errors.Wrap(svcerr.ErrAuthentication, err)
}
return key, nil
}
func (tok *tokenizer) validateToken(token string) (jwt.Token, error) {
tkn, err := jwt.Parse(
[]byte(token),
jwt.WithValidate(true),
jwt.WithKey(jwa.HS512, tok.secret),
)
if err != nil {
if errors.Contains(err, errJWTExpiryKey) {
return nil, auth.ErrExpiry
}
return nil, err
}
validator := jwt.ValidatorFunc(func(_ context.Context, t jwt.Token) jwt.ValidationError {
if t.Issuer() != issuerName {
return jwt.NewValidationError(errInvalidIssuer)
}
return nil
})
if err := jwt.Validate(tkn, jwt.WithValidator(validator)); err != nil {
return nil, errors.Wrap(ErrValidateJWTToken, err)
}
return tkn, nil
}
func toKey(tkn jwt.Token) (auth.Key, error) {
data, err := json.Marshal(tkn.PrivateClaims())
if err != nil {
return auth.Key{}, errors.Wrap(ErrJSONHandle, err)
}
var key auth.Key
if err := json.Unmarshal(data, &key); err != nil {
return auth.Key{}, errors.Wrap(ErrJSONHandle, err)
}
tType, ok := tkn.Get(tokenType)
if !ok {
return auth.Key{}, errInvalidType
}
kType, ok := tType.(float64)
if !ok {
return auth.Key{}, errInvalidType
}
kt := auth.KeyType(kType)
if !kt.Validate() {
return auth.Key{}, errInvalidType
}
tRole, ok := tkn.Get(RoleField)
if !ok {
return auth.Key{}, errInvalidRole
}
kRole, ok := tRole.(float64)
if !ok {
return auth.Key{}, errInvalidRole
}
tVerified, ok := tkn.Get(VerifiedField)
if !ok {
return auth.Key{}, errInvalidVerified
}
kVerified, ok := tVerified.(bool)
if !ok {
return auth.Key{}, errInvalidVerified
}
kr := auth.Role(kRole)
if !kr.Validate() {
return auth.Key{}, errInvalidRole
}
key.ID = tkn.JwtID()
key.Type = auth.KeyType(kType)
key.Role = auth.Role(kRole)
key.Issuer = tkn.Issuer()
key.Subject = tkn.Subject()
key.IssuedAt = tkn.IssuedAt()
key.ExpiresAt = tkn.Expiration()
key.Verified = kVerified
return key, nil
}
+86
View File
@@ -0,0 +1,86 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package auth
import (
"context"
"time"
"github.com/absmach/supermq/pkg/errors"
)
var (
ErrUnsupportedKeyAlgorithm = errors.New("unsupported key algorithm")
ErrInvalidSymmetricKey = errors.New("invalid symmetric key")
ErrPublicKeysNotSupported = errors.New("public keys not supported for symmetric algorithm")
ErrRevokedToken = errors.NewAuthNError("token is revoked")
)
// PublicKeyInfo represents a public key for external distribution via JWKS.
// This follows RFC 7517 (JSON Web Key) specification.
type PublicKeyInfo struct {
KeyID string `json:"kid"`
KeyType string `json:"kty"`
Algorithm string `json:"alg"`
Use string `json:"use,omitempty"`
// EdDSA (Ed25519) fields
Curve string `json:"crv,omitempty"`
X string `json:"x,omitempty"`
// Future: RSA fields (n, e), ECDSA fields (x, y, crv), etc.
}
// Tokenizer handles token creation and verification for authentication.
// Implementations manage underlying cryptographic operations and key distribution.
type Tokenizer interface {
// Issue creates a signed token string from the given key claims.
// For RefreshKey types, the token ID is stored as active in the cache.
Issue(key Key) (token string, err error)
// Parse verifies and parses a token string (JWT or PAT), returning the extracted claims.
// For PAT tokens (prefix "pat"), returns a Key with Type set to PersonalAccessToken.
// For JWT tokens, performs cryptographic verification and returns the parsed claims.
Parse(ctx context.Context, token string) (key Key, err error)
// RetrieveJWKS returns public keys for distribution via JWKS endpoint.
// Returns ErrPublicKeysNotSupported for symmetric tokenizers (HMAC).
RetrieveJWKS() ([]PublicKeyInfo, error)
}
// UserActiveTokensCache represents a cache repository for managing active refresh tokens per user.
type UserActiveTokensCache interface {
// SaveActive saves an active refresh token ID for a user with optional description.
SaveActive(ctx context.Context, userID, tokenID, description string, expiry time.Time) error
// IsActive checks if the token ID is active.
IsActive(ctx context.Context, tokenID string) (bool, error)
// ListUserTokens lists all active token IDs with descriptions for a given user.
ListUserTokens(ctx context.Context, userID string) ([]TokenInfo, error)
// RemoveActive removes an active refresh token ID.
RemoveActive(ctx context.Context, userID, tokenID string) error
}
// TokenInfo represents information about an active refresh token.
type TokenInfo struct {
ID string `json:"id"`
Description string `json:"description,omitempty"`
}
// IsSymmetricAlgorithm determines if the given algorithm is symmetric (HMAC-based).
// Returns true for HMAC algorithms (HS256, HS384, HS512).
// Returns false for asymmetric algorithms (EdDSA).
// Returns error for unsupported algorithms.
func IsSymmetricAlgorithm(alg string) (bool, error) {
switch alg {
case "EdDSA":
return false, nil
case "HS256", "HS384", "HS512":
return true, nil
default:
return false, ErrUnsupportedKeyAlgorithm
}
}
+9 -8
View File
@@ -81,14 +81,15 @@ func (r Role) Validate() bool {
// Key represents API key.
type Key struct {
ID string `json:"id,omitempty"`
Type KeyType `json:"type,omitempty"`
Issuer string `json:"issuer,omitempty"`
Subject string `json:"subject,omitempty"` // user ID
Role Role `json:"role,omitempty"`
IssuedAt time.Time `json:"issued_at,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
Verified bool `json:"verified,omitempty"`
ID string `json:"id,omitempty"`
Type KeyType `json:"type,omitempty"`
Issuer string `json:"issuer,omitempty"`
Subject string `json:"subject,omitempty"` // user ID
Role Role `json:"role,omitempty"`
IssuedAt time.Time `json:"issued_at,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
Verified bool `json:"verified,omitempty"`
Description string `json:"description,omitempty"` // Optional description for refresh tokens
}
func (key Key) String() string {
+193 -2
View File
@@ -12,6 +12,195 @@ import (
"github.com/stretchr/testify/assert"
)
func TestKeyTypeString(t *testing.T) {
cases := []struct {
desc string
keyType auth.KeyType
expected string
}{
{
desc: "Access key type",
keyType: auth.AccessKey,
expected: "access",
},
{
desc: "Refresh key type",
keyType: auth.RefreshKey,
expected: "refresh",
},
{
desc: "Recovery key type",
keyType: auth.RecoveryKey,
expected: "recovery",
},
{
desc: "API key type",
keyType: auth.APIKey,
expected: "API",
},
{
desc: "Personal access token type",
keyType: auth.PersonalAccessToken,
expected: "pat",
},
{
desc: "Invitation key type",
keyType: auth.InvitationKey,
expected: "unknown",
},
{
desc: "Unknown key type",
keyType: auth.KeyType(100),
expected: "unknown",
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got := tc.keyType.String()
assert.Equal(t, tc.expected, got, "String() = %v, expected %v", got, tc.expected)
})
}
}
func TestKeyTypeValidate(t *testing.T) {
cases := []struct {
desc string
keyType auth.KeyType
expected bool
}{
{
desc: "Valid access key",
keyType: auth.AccessKey,
expected: true,
},
{
desc: "Valid refresh key",
keyType: auth.RefreshKey,
expected: true,
},
{
desc: "Valid recovery key",
keyType: auth.RecoveryKey,
expected: true,
},
{
desc: "Valid API key",
keyType: auth.APIKey,
expected: true,
},
{
desc: "Valid personal access token",
keyType: auth.PersonalAccessToken,
expected: true,
},
{
desc: "Valid invitation key",
keyType: auth.InvitationKey,
expected: true,
},
{
desc: "Invalid key type (too large)",
keyType: auth.KeyType(100),
expected: false,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got := tc.keyType.Validate()
assert.Equal(t, tc.expected, got, "Validate() = %v, expected %v", got, tc.expected)
})
}
}
func TestRoleString(t *testing.T) {
cases := []struct {
desc string
role auth.Role
expected string
}{
{
desc: "User role",
role: auth.UserRole,
expected: "user",
},
{
desc: "Admin role",
role: auth.AdminRole,
expected: "admin",
},
{
desc: "Unknown role",
role: auth.Role(100),
expected: "unknown",
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got := tc.role.String()
assert.Equal(t, tc.expected, got, "String() = %v, expected %v", got, tc.expected)
})
}
}
func TestRoleValidate(t *testing.T) {
cases := []struct {
desc string
role auth.Role
expected bool
}{
{
desc: "Valid user role",
role: auth.UserRole,
expected: true,
},
{
desc: "Valid admin role",
role: auth.AdminRole,
expected: true,
},
{
desc: "Invalid role (zero)",
role: auth.Role(0),
expected: false,
},
{
desc: "Invalid role (too large)",
role: auth.Role(100),
expected: false,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got := tc.role.Validate()
assert.Equal(t, tc.expected, got, "Validate() = %v, expected %v", got, tc.expected)
})
}
}
func TestKeyString(t *testing.T) {
key := auth.Key{
ID: "test-id",
Type: auth.APIKey,
Issuer: "test-issuer",
Subject: "test-subject",
Role: auth.UserRole,
IssuedAt: time.Now().UTC().Round(time.Second),
ExpiresAt: time.Now().UTC().Add(24 * time.Hour).Round(time.Second),
}
str := key.String()
assert.NotEmpty(t, str, "String() should return non-empty string")
assert.Contains(t, str, "test-id", "String() should contain ID")
assert.Contains(t, str, "test-issuer", "String() should contain Issuer")
assert.Contains(t, str, "test-subject", "String() should contain Subject")
assert.Contains(t, str, "API", "String() should contain Type")
assert.Contains(t, str, "user", "String() should contain Role")
}
func TestExpired(t *testing.T) {
exp := time.Now().Add(5 * time.Minute)
exp1 := time.Now()
@@ -54,7 +243,9 @@ func TestExpired(t *testing.T) {
}
for _, tc := range cases {
res := tc.key.Expired()
assert.Equal(t, tc.expired, res, fmt.Sprintf("%s: expected %t got %t\n", tc.desc, tc.expired, res))
t.Run(tc.desc, func(t *testing.T) {
res := tc.key.Expired()
assert.Equal(t, tc.expired, res, fmt.Sprintf("%s: expected %t got %t\n", tc.desc, tc.expired, res))
})
}
}
+1 -1
View File
@@ -5,5 +5,5 @@
// for SuperMQ Auth service.
//
// For more details about tracing instrumentation for SuperMQ refer to the
// documentation at https://docs.supermq.abstractmachines.fr/tracing/.
// documentation at https://docs.supermq.absmach.eu/tracing/.
package middleware
+66 -8
View File
@@ -100,7 +100,53 @@ func (lm *loggingMiddleware) Identify(ctx context.Context, token string) (id aut
return lm.svc.Identify(ctx, token)
}
func (lm *loggingMiddleware) Authorize(ctx context.Context, pr policies.Policy) (err error) {
func (lm *loggingMiddleware) RetrieveJWKS() (jwks []auth.PublicKeyInfo) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
}
lm.logger.Info("Retrieve JWKS completed successfully", args...)
}(time.Now())
return lm.svc.RetrieveJWKS()
}
func (lm *loggingMiddleware) RevokeToken(ctx context.Context, userID, tokenID string) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("user_id", userID),
slog.String("token_id", tokenID),
}
if err != nil {
args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("Revoke token failed", args...)
return
}
lm.logger.Info("Revoke token completed successfully", args...)
}(time.Now())
return lm.svc.RevokeToken(ctx, userID, tokenID)
}
func (lm *loggingMiddleware) ListUserRefreshTokens(ctx context.Context, userID string) (tokens []auth.TokenInfo, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("user_id", userID),
slog.Int("tokens_count", len(tokens)),
}
if err != nil {
args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("List user refresh tokens failed", args...)
return
}
lm.logger.Info("List user refresh tokens completed successfully", args...)
}(time.Now())
return lm.svc.ListUserRefreshTokens(ctx, userID)
}
func (lm *loggingMiddleware) Authorize(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
@@ -115,6 +161,18 @@ func (lm *loggingMiddleware) Authorize(ctx context.Context, pr policies.Policy)
),
slog.String("permission", pr.Permission),
}
if patAuthz != nil {
args = append(args,
slog.Group("pat",
slog.String("pat_id", patAuthz.PatID),
slog.String("user_id", patAuthz.UserID),
slog.String("entity_type", patAuthz.EntityType.String()),
slog.String("entity_id", patAuthz.EntityID),
slog.String("operation", patAuthz.Operation),
slog.String("domain", patAuthz.Domain),
),
)
}
if err != nil {
args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("Authorize failed", args...)
@@ -122,7 +180,7 @@ func (lm *loggingMiddleware) Authorize(ctx context.Context, pr policies.Policy)
}
lm.logger.Info("Authorize completed successfully", args...)
}(time.Now())
return lm.svc.Authorize(ctx, pr)
return lm.svc.Authorize(ctx, pr, patAuthz)
}
func (lm *loggingMiddleware) CreatePAT(ctx context.Context, token, name, description string, duration time.Duration) (pa auth.PAT, err error) {
@@ -297,8 +355,8 @@ func (lm *loggingMiddleware) AddScope(ctx context.Context, token, patID string,
var groupArgs []any
for _, s := range scopes {
groupArgs = append(groupArgs, slog.String("entity_type", s.EntityType.String()))
groupArgs = append(groupArgs, slog.String("optional_domain_id", s.OptionalDomainID))
groupArgs = append(groupArgs, slog.String("operation", s.Operation.String()))
groupArgs = append(groupArgs, slog.String("domain_id", s.DomainID))
groupArgs = append(groupArgs, slog.String("operation", s.Operation))
groupArgs = append(groupArgs, slog.String("entity_id", s.EntityID))
}
@@ -369,13 +427,13 @@ func (lm *loggingMiddleware) IdentifyPAT(ctx context.Context, paToken string) (p
return lm.svc.IdentifyPAT(ctx, paToken)
}
func (lm *loggingMiddleware) AuthorizePAT(ctx context.Context, userID, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) (err error) {
func (lm *loggingMiddleware) AuthorizePAT(ctx context.Context, userID, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("entity_type", entityType.String()),
slog.String("optional_domain_id", optionalDomainID),
slog.String("operation", operation.String()),
slog.String("domain_id", domainID),
slog.String("operation", operation),
slog.String("entities", entityID),
}
if err != nil {
@@ -385,5 +443,5 @@ func (lm *loggingMiddleware) AuthorizePAT(ctx context.Context, userID, patID str
}
lm.logger.Info("Authorize PAT completed successfully", args...)
}(time.Now())
return lm.svc.AuthorizePAT(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
return lm.svc.AuthorizePAT(ctx, userID, patID, entityType, domainID, operation, entityID)
}
+30 -4
View File
@@ -40,6 +40,15 @@ func (ms *metricsMiddleware) Issue(ctx context.Context, token string, key auth.K
return ms.svc.Issue(ctx, token, key)
}
func (ms *metricsMiddleware) RevokeToken(ctx context.Context, userID, tokenID string) error {
defer func(begin time.Time) {
ms.counter.With("method", "revoke_token").Add(1)
ms.latency.With("method", "revoke_token").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.RevokeToken(ctx, userID, tokenID)
}
func (ms *metricsMiddleware) Revoke(ctx context.Context, token, id string) error {
defer func(begin time.Time) {
ms.counter.With("method", "revoke_key").Add(1)
@@ -67,12 +76,29 @@ func (ms *metricsMiddleware) Identify(ctx context.Context, token string) (auth.K
return ms.svc.Identify(ctx, token)
}
func (ms *metricsMiddleware) Authorize(ctx context.Context, pr policies.Policy) error {
func (ms *metricsMiddleware) RetrieveJWKS() []auth.PublicKeyInfo {
defer func(begin time.Time) {
ms.counter.With("method", "retrieve_jwks").Add(1)
ms.latency.With("method", "retrieve_jwks").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.RetrieveJWKS()
}
func (ms *metricsMiddleware) ListUserRefreshTokens(ctx context.Context, userID string) ([]auth.TokenInfo, error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_user_refresh_tokens").Add(1)
ms.latency.With("method", "list_user_refresh_tokens").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ListUserRefreshTokens(ctx, userID)
}
func (ms *metricsMiddleware) Authorize(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz) error {
defer func(begin time.Time) {
ms.counter.With("method", "authorize").Add(1)
ms.latency.With("method", "authorize").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Authorize(ctx, pr)
return ms.svc.Authorize(ctx, pr, patAuthz)
}
func (ms *metricsMiddleware) CreatePAT(ctx context.Context, token, name, description string, duration time.Duration) (auth.PAT, error) {
@@ -187,10 +213,10 @@ func (ms *metricsMiddleware) IdentifyPAT(ctx context.Context, paToken string) (a
return ms.svc.IdentifyPAT(ctx, paToken)
}
func (ms *metricsMiddleware) AuthorizePAT(ctx context.Context, userID, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error {
func (ms *metricsMiddleware) AuthorizePAT(ctx context.Context, userID, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error {
defer func(begin time.Time) {
ms.counter.With("method", "authorize_pat").Add(1)
ms.latency.With("method", "authorize_pat").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.AuthorizePAT(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
return ms.svc.AuthorizePAT(ctx, userID, patID, entityType, domainID, operation, entityID)
}
+46 -10
View File
@@ -61,8 +61,31 @@ func (tm *tracingMiddleware) Identify(ctx context.Context, token string) (auth.K
return tm.svc.Identify(ctx, token)
}
func (tm *tracingMiddleware) Authorize(ctx context.Context, pr policies.Policy) error {
ctx, span := tm.tracer.Start(ctx, "authorize", trace.WithAttributes(
func (tm *tracingMiddleware) RetrieveJWKS() []auth.PublicKeyInfo {
return tm.svc.RetrieveJWKS()
}
func (tm *tracingMiddleware) RevokeToken(ctx context.Context, userID, tokenID string) error {
ctx, span := tm.tracer.Start(ctx, "revoke_token", trace.WithAttributes(
attribute.String("user_id", userID),
attribute.String("token_id", tokenID),
))
defer span.End()
return tm.svc.RevokeToken(ctx, userID, tokenID)
}
func (tm *tracingMiddleware) ListUserRefreshTokens(ctx context.Context, userID string) ([]auth.TokenInfo, error) {
ctx, span := tm.tracer.Start(ctx, "list_user_refresh_tokens", trace.WithAttributes(
attribute.String("user_id", userID),
))
defer span.End()
return tm.svc.ListUserRefreshTokens(ctx, userID)
}
func (tm *tracingMiddleware) Authorize(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz) error {
attributes := []attribute.KeyValue{
attribute.String("subject", pr.Subject),
attribute.String("subject_type", pr.SubjectType),
attribute.String("subject_relation", pr.SubjectRelation),
@@ -70,10 +93,23 @@ func (tm *tracingMiddleware) Authorize(ctx context.Context, pr policies.Policy)
attribute.String("object_type", pr.ObjectType),
attribute.String("relation", pr.Relation),
attribute.String("permission", pr.Permission),
))
}
if patAuthz != nil {
attributes = append(attributes,
attribute.String("pat_id", patAuthz.PatID),
attribute.String("pat_user_id", patAuthz.UserID),
attribute.String("pat_entity_type", patAuthz.EntityType.String()),
attribute.String("pat_entity_id", patAuthz.EntityID),
attribute.String("pat_operation", patAuthz.Operation),
attribute.String("pat_domain", patAuthz.Domain),
)
}
ctx, span := tm.tracer.Start(ctx, "authorize", trace.WithAttributes(attributes...))
defer span.End()
return tm.svc.Authorize(ctx, pr)
return tm.svc.Authorize(ctx, pr, patAuthz)
}
func (tm *tracingMiddleware) CreatePAT(ctx context.Context, token, name, description string, duration time.Duration) (auth.PAT, error) {
@@ -165,8 +201,8 @@ func (tm *tracingMiddleware) AddScope(ctx context.Context, token, patID string,
var attributes []attribute.KeyValue
for _, s := range scopes {
attributes = append(attributes, attribute.String("entity_type", s.EntityType.String()))
attributes = append(attributes, attribute.String("optional_domain_id", s.OptionalDomainID))
attributes = append(attributes, attribute.String("operation", s.Operation.String()))
attributes = append(attributes, attribute.String("domain_id", s.DomainID))
attributes = append(attributes, attribute.String("operation", s.Operation))
attributes = append(attributes, attribute.String("entity_id", s.EntityID))
}
@@ -204,14 +240,14 @@ func (tm *tracingMiddleware) IdentifyPAT(ctx context.Context, paToken string) (a
return tm.svc.IdentifyPAT(ctx, paToken)
}
func (tm *tracingMiddleware) AuthorizePAT(ctx context.Context, userID, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error {
func (tm *tracingMiddleware) AuthorizePAT(ctx context.Context, userID, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error {
ctx, span := tm.tracer.Start(ctx, "authorize_pat", trace.WithAttributes(
attribute.String("pat_id", patID),
attribute.String("entity_type", entityType.String()),
attribute.String("optional_domain_id", optionalDomainID),
attribute.String("operation", operation.String()),
attribute.String("domain_id", domainID),
attribute.String("operation", operation),
attribute.String("entities", entityID),
))
defer span.End()
return tm.svc.AuthorizePAT(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
return tm.svc.AuthorizePAT(ctx, userID, patID, entityType, domainID, operation, entityID)
}
+15 -8
View File
@@ -11,6 +11,7 @@ package mocks
import (
"context"
"github.com/absmach/supermq/auth"
"github.com/absmach/supermq/pkg/policies"
mock "github.com/stretchr/testify/mock"
)
@@ -43,16 +44,16 @@ func (_m *Authz) EXPECT() *Authz_Expecter {
}
// Authorize provides a mock function for the type Authz
func (_mock *Authz) Authorize(ctx context.Context, pr policies.Policy) error {
ret := _mock.Called(ctx, pr)
func (_mock *Authz) Authorize(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz) error {
ret := _mock.Called(ctx, pr, patAuthz)
if len(ret) == 0 {
panic("no return value specified for Authorize")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, policies.Policy) error); ok {
r0 = returnFunc(ctx, pr)
if returnFunc, ok := ret.Get(0).(func(context.Context, policies.Policy, *auth.PATAuthz) error); ok {
r0 = returnFunc(ctx, pr, patAuthz)
} else {
r0 = ret.Error(0)
}
@@ -67,11 +68,12 @@ type Authz_Authorize_Call struct {
// Authorize is a helper method to define mock.On call
// - ctx context.Context
// - pr policies.Policy
func (_e *Authz_Expecter) Authorize(ctx interface{}, pr interface{}) *Authz_Authorize_Call {
return &Authz_Authorize_Call{Call: _e.mock.On("Authorize", ctx, pr)}
// - patAuthz *auth.PATAuthz
func (_e *Authz_Expecter) Authorize(ctx interface{}, pr interface{}, patAuthz interface{}) *Authz_Authorize_Call {
return &Authz_Authorize_Call{Call: _e.mock.On("Authorize", ctx, pr, patAuthz)}
}
func (_c *Authz_Authorize_Call) Run(run func(ctx context.Context, pr policies.Policy)) *Authz_Authorize_Call {
func (_c *Authz_Authorize_Call) Run(run func(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz)) *Authz_Authorize_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -81,9 +83,14 @@ func (_c *Authz_Authorize_Call) Run(run func(ctx context.Context, pr policies.Po
if args[1] != nil {
arg1 = args[1].(policies.Policy)
}
var arg2 *auth.PATAuthz
if args[2] != nil {
arg2 = args[2].(*auth.PATAuthz)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
@@ -94,7 +101,7 @@ func (_c *Authz_Authorize_Call) Return(err error) *Authz_Authorize_Call {
return _c
}
func (_c *Authz_Authorize_Call) RunAndReturn(run func(ctx context.Context, pr policies.Policy) error) *Authz_Authorize_Call {
func (_c *Authz_Authorize_Call) RunAndReturn(run func(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz) error) *Authz_Authorize_Call {
_c.Call.Return(run)
return _c
}
+7 -7
View File
@@ -43,7 +43,7 @@ func (_m *Cache) EXPECT() *Cache_Expecter {
}
// CheckScope provides a mock function for the type Cache
func (_mock *Cache) CheckScope(ctx context.Context, userID string, patID string, optionalDomainID string, entityType auth.EntityType, operation auth.Operation, entityID string) bool {
func (_mock *Cache) CheckScope(ctx context.Context, userID string, patID string, optionalDomainID string, entityType auth.EntityType, operation string, entityID string) bool {
ret := _mock.Called(ctx, userID, patID, optionalDomainID, entityType, operation, entityID)
if len(ret) == 0 {
@@ -51,7 +51,7 @@ func (_mock *Cache) CheckScope(ctx context.Context, userID string, patID string,
}
var r0 bool
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, auth.EntityType, auth.Operation, string) bool); ok {
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, auth.EntityType, string, string) bool); ok {
r0 = returnFunc(ctx, userID, patID, optionalDomainID, entityType, operation, entityID)
} else {
r0 = ret.Get(0).(bool)
@@ -70,13 +70,13 @@ type Cache_CheckScope_Call struct {
// - patID string
// - optionalDomainID string
// - entityType auth.EntityType
// - operation auth.Operation
// - operation string
// - entityID string
func (_e *Cache_Expecter) CheckScope(ctx interface{}, userID interface{}, patID interface{}, optionalDomainID interface{}, entityType interface{}, operation interface{}, entityID interface{}) *Cache_CheckScope_Call {
return &Cache_CheckScope_Call{Call: _e.mock.On("CheckScope", ctx, userID, patID, optionalDomainID, entityType, operation, entityID)}
}
func (_c *Cache_CheckScope_Call) Run(run func(ctx context.Context, userID string, patID string, optionalDomainID string, entityType auth.EntityType, operation auth.Operation, entityID string)) *Cache_CheckScope_Call {
func (_c *Cache_CheckScope_Call) Run(run func(ctx context.Context, userID string, patID string, optionalDomainID string, entityType auth.EntityType, operation string, entityID string)) *Cache_CheckScope_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -98,9 +98,9 @@ func (_c *Cache_CheckScope_Call) Run(run func(ctx context.Context, userID string
if args[4] != nil {
arg4 = args[4].(auth.EntityType)
}
var arg5 auth.Operation
var arg5 string
if args[5] != nil {
arg5 = args[5].(auth.Operation)
arg5 = args[5].(string)
}
var arg6 string
if args[6] != nil {
@@ -124,7 +124,7 @@ func (_c *Cache_CheckScope_Call) Return(b bool) *Cache_CheckScope_Call {
return _c
}
func (_c *Cache_CheckScope_Call) RunAndReturn(run func(ctx context.Context, userID string, patID string, optionalDomainID string, entityType auth.EntityType, operation auth.Operation, entityID string) bool) *Cache_CheckScope_Call {
func (_c *Cache_CheckScope_Call) RunAndReturn(run func(ctx context.Context, userID string, patID string, optionalDomainID string, entityType auth.EntityType, operation string, entityID string) bool) *Cache_CheckScope_Call {
_c.Call.Return(run)
return _c
}
+12 -12
View File
@@ -113,16 +113,16 @@ func (_c *PATS_AddScope_Call) RunAndReturn(run func(ctx context.Context, token s
}
// AuthorizePAT provides a mock function for the type PATS
func (_mock *PATS) AuthorizePAT(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error {
ret := _mock.Called(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
func (_mock *PATS) AuthorizePAT(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error {
ret := _mock.Called(ctx, userID, patID, entityType, domainID, operation, entityID)
if len(ret) == 0 {
panic("no return value specified for AuthorizePAT")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, auth.EntityType, string, auth.Operation, string) error); ok {
r0 = returnFunc(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, auth.EntityType, string, string, string) error); ok {
r0 = returnFunc(ctx, userID, patID, entityType, domainID, operation, entityID)
} else {
r0 = ret.Error(0)
}
@@ -139,14 +139,14 @@ type PATS_AuthorizePAT_Call struct {
// - userID string
// - patID string
// - entityType auth.EntityType
// - optionalDomainID string
// - operation auth.Operation
// - domainID string
// - operation string
// - entityID string
func (_e *PATS_Expecter) AuthorizePAT(ctx interface{}, userID interface{}, patID interface{}, entityType interface{}, optionalDomainID interface{}, operation interface{}, entityID interface{}) *PATS_AuthorizePAT_Call {
return &PATS_AuthorizePAT_Call{Call: _e.mock.On("AuthorizePAT", ctx, userID, patID, entityType, optionalDomainID, operation, entityID)}
func (_e *PATS_Expecter) AuthorizePAT(ctx interface{}, userID interface{}, patID interface{}, entityType interface{}, domainID interface{}, operation interface{}, entityID interface{}) *PATS_AuthorizePAT_Call {
return &PATS_AuthorizePAT_Call{Call: _e.mock.On("AuthorizePAT", ctx, userID, patID, entityType, domainID, operation, entityID)}
}
func (_c *PATS_AuthorizePAT_Call) Run(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string)) *PATS_AuthorizePAT_Call {
func (_c *PATS_AuthorizePAT_Call) Run(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string)) *PATS_AuthorizePAT_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -168,9 +168,9 @@ func (_c *PATS_AuthorizePAT_Call) Run(run func(ctx context.Context, userID strin
if args[4] != nil {
arg4 = args[4].(string)
}
var arg5 auth.Operation
var arg5 string
if args[5] != nil {
arg5 = args[5].(auth.Operation)
arg5 = args[5].(string)
}
var arg6 string
if args[6] != nil {
@@ -194,7 +194,7 @@ func (_c *PATS_AuthorizePAT_Call) Return(err error) *PATS_AuthorizePAT_Call {
return _c
}
func (_c *PATS_AuthorizePAT_Call) RunAndReturn(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error) *PATS_AuthorizePAT_Call {
func (_c *PATS_AuthorizePAT_Call) RunAndReturn(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error) *PATS_AuthorizePAT_Call {
_c.Call.Return(run)
return _c
}
+12 -12
View File
@@ -107,16 +107,16 @@ func (_c *PATSRepository_AddScope_Call) RunAndReturn(run func(ctx context.Contex
}
// CheckScope provides a mock function for the type PATSRepository
func (_mock *PATSRepository) CheckScope(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error {
ret := _mock.Called(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
func (_mock *PATSRepository) CheckScope(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error {
ret := _mock.Called(ctx, userID, patID, entityType, domainID, operation, entityID)
if len(ret) == 0 {
panic("no return value specified for CheckScope")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, auth.EntityType, string, auth.Operation, string) error); ok {
r0 = returnFunc(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, auth.EntityType, string, string, string) error); ok {
r0 = returnFunc(ctx, userID, patID, entityType, domainID, operation, entityID)
} else {
r0 = ret.Error(0)
}
@@ -133,14 +133,14 @@ type PATSRepository_CheckScope_Call struct {
// - userID string
// - patID string
// - entityType auth.EntityType
// - optionalDomainID string
// - operation auth.Operation
// - domainID string
// - operation string
// - entityID string
func (_e *PATSRepository_Expecter) CheckScope(ctx interface{}, userID interface{}, patID interface{}, entityType interface{}, optionalDomainID interface{}, operation interface{}, entityID interface{}) *PATSRepository_CheckScope_Call {
return &PATSRepository_CheckScope_Call{Call: _e.mock.On("CheckScope", ctx, userID, patID, entityType, optionalDomainID, operation, entityID)}
func (_e *PATSRepository_Expecter) CheckScope(ctx interface{}, userID interface{}, patID interface{}, entityType interface{}, domainID interface{}, operation interface{}, entityID interface{}) *PATSRepository_CheckScope_Call {
return &PATSRepository_CheckScope_Call{Call: _e.mock.On("CheckScope", ctx, userID, patID, entityType, domainID, operation, entityID)}
}
func (_c *PATSRepository_CheckScope_Call) Run(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string)) *PATSRepository_CheckScope_Call {
func (_c *PATSRepository_CheckScope_Call) Run(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string)) *PATSRepository_CheckScope_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -162,9 +162,9 @@ func (_c *PATSRepository_CheckScope_Call) Run(run func(ctx context.Context, user
if args[4] != nil {
arg4 = args[4].(string)
}
var arg5 auth.Operation
var arg5 string
if args[5] != nil {
arg5 = args[5].(auth.Operation)
arg5 = args[5].(string)
}
var arg6 string
if args[6] != nil {
@@ -188,7 +188,7 @@ func (_c *PATSRepository_CheckScope_Call) Return(err error) *PATSRepository_Chec
return _c
}
func (_c *PATSRepository_CheckScope_Call) RunAndReturn(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error) *PATSRepository_CheckScope_Call {
func (_c *PATSRepository_CheckScope_Call) RunAndReturn(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error) *PATSRepository_CheckScope_Call {
_c.Call.Return(run)
return _c
}
+203 -20
View File
@@ -114,16 +114,16 @@ func (_c *Service_AddScope_Call) RunAndReturn(run func(ctx context.Context, toke
}
// Authorize provides a mock function for the type Service
func (_mock *Service) Authorize(ctx context.Context, pr policies.Policy) error {
ret := _mock.Called(ctx, pr)
func (_mock *Service) Authorize(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz) error {
ret := _mock.Called(ctx, pr, patAuthz)
if len(ret) == 0 {
panic("no return value specified for Authorize")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, policies.Policy) error); ok {
r0 = returnFunc(ctx, pr)
if returnFunc, ok := ret.Get(0).(func(context.Context, policies.Policy, *auth.PATAuthz) error); ok {
r0 = returnFunc(ctx, pr, patAuthz)
} else {
r0 = ret.Error(0)
}
@@ -138,11 +138,12 @@ type Service_Authorize_Call struct {
// Authorize is a helper method to define mock.On call
// - ctx context.Context
// - pr policies.Policy
func (_e *Service_Expecter) Authorize(ctx interface{}, pr interface{}) *Service_Authorize_Call {
return &Service_Authorize_Call{Call: _e.mock.On("Authorize", ctx, pr)}
// - patAuthz *auth.PATAuthz
func (_e *Service_Expecter) Authorize(ctx interface{}, pr interface{}, patAuthz interface{}) *Service_Authorize_Call {
return &Service_Authorize_Call{Call: _e.mock.On("Authorize", ctx, pr, patAuthz)}
}
func (_c *Service_Authorize_Call) Run(run func(ctx context.Context, pr policies.Policy)) *Service_Authorize_Call {
func (_c *Service_Authorize_Call) Run(run func(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz)) *Service_Authorize_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -152,9 +153,14 @@ func (_c *Service_Authorize_Call) Run(run func(ctx context.Context, pr policies.
if args[1] != nil {
arg1 = args[1].(policies.Policy)
}
var arg2 *auth.PATAuthz
if args[2] != nil {
arg2 = args[2].(*auth.PATAuthz)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
@@ -165,22 +171,22 @@ func (_c *Service_Authorize_Call) Return(err error) *Service_Authorize_Call {
return _c
}
func (_c *Service_Authorize_Call) RunAndReturn(run func(ctx context.Context, pr policies.Policy) error) *Service_Authorize_Call {
func (_c *Service_Authorize_Call) RunAndReturn(run func(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz) error) *Service_Authorize_Call {
_c.Call.Return(run)
return _c
}
// AuthorizePAT provides a mock function for the type Service
func (_mock *Service) AuthorizePAT(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error {
ret := _mock.Called(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
func (_mock *Service) AuthorizePAT(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error {
ret := _mock.Called(ctx, userID, patID, entityType, domainID, operation, entityID)
if len(ret) == 0 {
panic("no return value specified for AuthorizePAT")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, auth.EntityType, string, auth.Operation, string) error); ok {
r0 = returnFunc(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, auth.EntityType, string, string, string) error); ok {
r0 = returnFunc(ctx, userID, patID, entityType, domainID, operation, entityID)
} else {
r0 = ret.Error(0)
}
@@ -197,14 +203,14 @@ type Service_AuthorizePAT_Call struct {
// - userID string
// - patID string
// - entityType auth.EntityType
// - optionalDomainID string
// - operation auth.Operation
// - domainID string
// - operation string
// - entityID string
func (_e *Service_Expecter) AuthorizePAT(ctx interface{}, userID interface{}, patID interface{}, entityType interface{}, optionalDomainID interface{}, operation interface{}, entityID interface{}) *Service_AuthorizePAT_Call {
return &Service_AuthorizePAT_Call{Call: _e.mock.On("AuthorizePAT", ctx, userID, patID, entityType, optionalDomainID, operation, entityID)}
func (_e *Service_Expecter) AuthorizePAT(ctx interface{}, userID interface{}, patID interface{}, entityType interface{}, domainID interface{}, operation interface{}, entityID interface{}) *Service_AuthorizePAT_Call {
return &Service_AuthorizePAT_Call{Call: _e.mock.On("AuthorizePAT", ctx, userID, patID, entityType, domainID, operation, entityID)}
}
func (_c *Service_AuthorizePAT_Call) Run(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string)) *Service_AuthorizePAT_Call {
func (_c *Service_AuthorizePAT_Call) Run(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string)) *Service_AuthorizePAT_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -226,9 +232,9 @@ func (_c *Service_AuthorizePAT_Call) Run(run func(ctx context.Context, userID st
if args[4] != nil {
arg4 = args[4].(string)
}
var arg5 auth.Operation
var arg5 string
if args[5] != nil {
arg5 = args[5].(auth.Operation)
arg5 = args[5].(string)
}
var arg6 string
if args[6] != nil {
@@ -252,7 +258,7 @@ func (_c *Service_AuthorizePAT_Call) Return(err error) *Service_AuthorizePAT_Cal
return _c
}
func (_c *Service_AuthorizePAT_Call) RunAndReturn(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error) *Service_AuthorizePAT_Call {
func (_c *Service_AuthorizePAT_Call) RunAndReturn(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error) *Service_AuthorizePAT_Call {
_c.Call.Return(run)
return _c
}
@@ -752,6 +758,74 @@ func (_c *Service_ListScopes_Call) RunAndReturn(run func(ctx context.Context, to
return _c
}
// ListUserRefreshTokens provides a mock function for the type Service
func (_mock *Service) ListUserRefreshTokens(ctx context.Context, userID string) ([]auth.TokenInfo, error) {
ret := _mock.Called(ctx, userID)
if len(ret) == 0 {
panic("no return value specified for ListUserRefreshTokens")
}
var r0 []auth.TokenInfo
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]auth.TokenInfo, error)); ok {
return returnFunc(ctx, userID)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string) []auth.TokenInfo); ok {
r0 = returnFunc(ctx, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]auth.TokenInfo)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = returnFunc(ctx, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Service_ListUserRefreshTokens_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListUserRefreshTokens'
type Service_ListUserRefreshTokens_Call struct {
*mock.Call
}
// ListUserRefreshTokens is a helper method to define mock.On call
// - ctx context.Context
// - userID string
func (_e *Service_Expecter) ListUserRefreshTokens(ctx interface{}, userID interface{}) *Service_ListUserRefreshTokens_Call {
return &Service_ListUserRefreshTokens_Call{Call: _e.mock.On("ListUserRefreshTokens", ctx, userID)}
}
func (_c *Service_ListUserRefreshTokens_Call) Run(run func(ctx context.Context, userID string)) *Service_ListUserRefreshTokens_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Service_ListUserRefreshTokens_Call) Return(tokenInfos []auth.TokenInfo, err error) *Service_ListUserRefreshTokens_Call {
_c.Call.Return(tokenInfos, err)
return _c
}
func (_c *Service_ListUserRefreshTokens_Call) RunAndReturn(run func(ctx context.Context, userID string) ([]auth.TokenInfo, error)) *Service_ListUserRefreshTokens_Call {
_c.Call.Return(run)
return _c
}
// RemoveAllPAT provides a mock function for the type Service
func (_mock *Service) RemoveAllPAT(ctx context.Context, token string) error {
ret := _mock.Called(ctx, token)
@@ -1028,6 +1102,52 @@ func (_c *Service_ResetPATSecret_Call) RunAndReturn(run func(ctx context.Context
return _c
}
// RetrieveJWKS provides a mock function for the type Service
func (_mock *Service) RetrieveJWKS() []auth.PublicKeyInfo {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for RetrieveJWKS")
}
var r0 []auth.PublicKeyInfo
if returnFunc, ok := ret.Get(0).(func() []auth.PublicKeyInfo); ok {
r0 = returnFunc()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]auth.PublicKeyInfo)
}
}
return r0
}
// Service_RetrieveJWKS_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveJWKS'
type Service_RetrieveJWKS_Call struct {
*mock.Call
}
// RetrieveJWKS is a helper method to define mock.On call
func (_e *Service_Expecter) RetrieveJWKS() *Service_RetrieveJWKS_Call {
return &Service_RetrieveJWKS_Call{Call: _e.mock.On("RetrieveJWKS")}
}
func (_c *Service_RetrieveJWKS_Call) Run(run func()) *Service_RetrieveJWKS_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Service_RetrieveJWKS_Call) Return(publicKeyInfos []auth.PublicKeyInfo) *Service_RetrieveJWKS_Call {
_c.Call.Return(publicKeyInfos)
return _c
}
func (_c *Service_RetrieveJWKS_Call) RunAndReturn(run func() []auth.PublicKeyInfo) *Service_RetrieveJWKS_Call {
_c.Call.Return(run)
return _c
}
// RetrieveKey provides a mock function for the type Service
func (_mock *Service) RetrieveKey(ctx context.Context, token string, id string) (auth.Key, error) {
ret := _mock.Called(ctx, token, id)
@@ -1298,6 +1418,69 @@ func (_c *Service_RevokePATSecret_Call) RunAndReturn(run func(ctx context.Contex
return _c
}
// RevokeToken provides a mock function for the type Service
func (_mock *Service) RevokeToken(ctx context.Context, userID string, tokenID string) error {
ret := _mock.Called(ctx, userID, tokenID)
if len(ret) == 0 {
panic("no return value specified for RevokeToken")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = returnFunc(ctx, userID, tokenID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Service_RevokeToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RevokeToken'
type Service_RevokeToken_Call struct {
*mock.Call
}
// RevokeToken is a helper method to define mock.On call
// - ctx context.Context
// - userID string
// - tokenID string
func (_e *Service_Expecter) RevokeToken(ctx interface{}, userID interface{}, tokenID interface{}) *Service_RevokeToken_Call {
return &Service_RevokeToken_Call{Call: _e.mock.On("RevokeToken", ctx, userID, tokenID)}
}
func (_c *Service_RevokeToken_Call) Run(run func(ctx context.Context, userID string, tokenID string)) *Service_RevokeToken_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *Service_RevokeToken_Call) Return(err error) *Service_RevokeToken_Call {
_c.Call.Return(err)
return _c
}
func (_c *Service_RevokeToken_Call) RunAndReturn(run func(ctx context.Context, userID string, tokenID string) error) *Service_RevokeToken_Call {
_c.Call.Return(run)
return _c
}
// UpdatePATDescription provides a mock function for the type Service
func (_mock *Service) UpdatePATDescription(ctx context.Context, token string, patID string, description string) (auth.PAT, error) {
ret := _mock.Called(ctx, token, patID, description)
+166
View File
@@ -126,6 +126,89 @@ func (_c *TokenServiceClient_Issue_Call) RunAndReturn(run func(ctx context.Conte
return _c
}
// ListUserRefreshTokens provides a mock function for the type TokenServiceClient
func (_mock *TokenServiceClient) ListUserRefreshTokens(ctx context.Context, in *v1.ListUserRefreshTokensReq, opts ...grpc.CallOption) (*v1.ListUserRefreshTokensRes, error) {
var tmpRet mock.Arguments
if len(opts) > 0 {
tmpRet = _mock.Called(ctx, in, opts)
} else {
tmpRet = _mock.Called(ctx, in)
}
ret := tmpRet
if len(ret) == 0 {
panic("no return value specified for ListUserRefreshTokens")
}
var r0 *v1.ListUserRefreshTokensRes
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *v1.ListUserRefreshTokensReq, ...grpc.CallOption) (*v1.ListUserRefreshTokensRes, error)); ok {
return returnFunc(ctx, in, opts...)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *v1.ListUserRefreshTokensReq, ...grpc.CallOption) *v1.ListUserRefreshTokensRes); ok {
r0 = returnFunc(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1.ListUserRefreshTokensRes)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *v1.ListUserRefreshTokensReq, ...grpc.CallOption) error); ok {
r1 = returnFunc(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// TokenServiceClient_ListUserRefreshTokens_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListUserRefreshTokens'
type TokenServiceClient_ListUserRefreshTokens_Call struct {
*mock.Call
}
// ListUserRefreshTokens is a helper method to define mock.On call
// - ctx context.Context
// - in *v1.ListUserRefreshTokensReq
// - opts ...grpc.CallOption
func (_e *TokenServiceClient_Expecter) ListUserRefreshTokens(ctx interface{}, in interface{}, opts ...interface{}) *TokenServiceClient_ListUserRefreshTokens_Call {
return &TokenServiceClient_ListUserRefreshTokens_Call{Call: _e.mock.On("ListUserRefreshTokens",
append([]interface{}{ctx, in}, opts...)...)}
}
func (_c *TokenServiceClient_ListUserRefreshTokens_Call) Run(run func(ctx context.Context, in *v1.ListUserRefreshTokensReq, opts ...grpc.CallOption)) *TokenServiceClient_ListUserRefreshTokens_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *v1.ListUserRefreshTokensReq
if args[1] != nil {
arg1 = args[1].(*v1.ListUserRefreshTokensReq)
}
var arg2 []grpc.CallOption
var variadicArgs []grpc.CallOption
if len(args) > 2 {
variadicArgs = args[2].([]grpc.CallOption)
}
arg2 = variadicArgs
run(
arg0,
arg1,
arg2...,
)
})
return _c
}
func (_c *TokenServiceClient_ListUserRefreshTokens_Call) Return(listUserRefreshTokensRes *v1.ListUserRefreshTokensRes, err error) *TokenServiceClient_ListUserRefreshTokens_Call {
_c.Call.Return(listUserRefreshTokensRes, err)
return _c
}
func (_c *TokenServiceClient_ListUserRefreshTokens_Call) RunAndReturn(run func(ctx context.Context, in *v1.ListUserRefreshTokensReq, opts ...grpc.CallOption) (*v1.ListUserRefreshTokensRes, error)) *TokenServiceClient_ListUserRefreshTokens_Call {
_c.Call.Return(run)
return _c
}
// Refresh provides a mock function for the type TokenServiceClient
func (_mock *TokenServiceClient) Refresh(ctx context.Context, in *v1.RefreshReq, opts ...grpc.CallOption) (*v1.Token, error) {
var tmpRet mock.Arguments
@@ -208,3 +291,86 @@ func (_c *TokenServiceClient_Refresh_Call) RunAndReturn(run func(ctx context.Con
_c.Call.Return(run)
return _c
}
// Revoke provides a mock function for the type TokenServiceClient
func (_mock *TokenServiceClient) Revoke(ctx context.Context, in *v1.RevokeReq, opts ...grpc.CallOption) (*v1.RevokeRes, error) {
var tmpRet mock.Arguments
if len(opts) > 0 {
tmpRet = _mock.Called(ctx, in, opts)
} else {
tmpRet = _mock.Called(ctx, in)
}
ret := tmpRet
if len(ret) == 0 {
panic("no return value specified for Revoke")
}
var r0 *v1.RevokeRes
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *v1.RevokeReq, ...grpc.CallOption) (*v1.RevokeRes, error)); ok {
return returnFunc(ctx, in, opts...)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *v1.RevokeReq, ...grpc.CallOption) *v1.RevokeRes); ok {
r0 = returnFunc(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1.RevokeRes)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *v1.RevokeReq, ...grpc.CallOption) error); ok {
r1 = returnFunc(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// TokenServiceClient_Revoke_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Revoke'
type TokenServiceClient_Revoke_Call struct {
*mock.Call
}
// Revoke is a helper method to define mock.On call
// - ctx context.Context
// - in *v1.RevokeReq
// - opts ...grpc.CallOption
func (_e *TokenServiceClient_Expecter) Revoke(ctx interface{}, in interface{}, opts ...interface{}) *TokenServiceClient_Revoke_Call {
return &TokenServiceClient_Revoke_Call{Call: _e.mock.On("Revoke",
append([]interface{}{ctx, in}, opts...)...)}
}
func (_c *TokenServiceClient_Revoke_Call) Run(run func(ctx context.Context, in *v1.RevokeReq, opts ...grpc.CallOption)) *TokenServiceClient_Revoke_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *v1.RevokeReq
if args[1] != nil {
arg1 = args[1].(*v1.RevokeReq)
}
var arg2 []grpc.CallOption
var variadicArgs []grpc.CallOption
if len(args) > 2 {
variadicArgs = args[2].([]grpc.CallOption)
}
arg2 = variadicArgs
run(
arg0,
arg1,
arg2...,
)
})
return _c
}
func (_c *TokenServiceClient_Revoke_Call) Return(revokeRes *v1.RevokeRes, err error) *TokenServiceClient_Revoke_Call {
_c.Call.Return(revokeRes, err)
return _c
}
func (_c *TokenServiceClient_Revoke_Call) RunAndReturn(run func(ctx context.Context, in *v1.RevokeReq, opts ...grpc.CallOption) (*v1.RevokeRes, error)) *TokenServiceClient_Revoke_Call {
_c.Call.Return(run)
return _c
}
+224
View File
@@ -0,0 +1,224 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"context"
"github.com/absmach/supermq/auth"
mock "github.com/stretchr/testify/mock"
)
// NewTokenizer creates a new instance of Tokenizer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewTokenizer(t interface {
mock.TestingT
Cleanup(func())
}) *Tokenizer {
mock := &Tokenizer{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// Tokenizer is an autogenerated mock type for the Tokenizer type
type Tokenizer struct {
mock.Mock
}
type Tokenizer_Expecter struct {
mock *mock.Mock
}
func (_m *Tokenizer) EXPECT() *Tokenizer_Expecter {
return &Tokenizer_Expecter{mock: &_m.Mock}
}
// Issue provides a mock function for the type Tokenizer
func (_mock *Tokenizer) Issue(key auth.Key) (string, error) {
ret := _mock.Called(key)
if len(ret) == 0 {
panic("no return value specified for Issue")
}
var r0 string
var r1 error
if returnFunc, ok := ret.Get(0).(func(auth.Key) (string, error)); ok {
return returnFunc(key)
}
if returnFunc, ok := ret.Get(0).(func(auth.Key) string); ok {
r0 = returnFunc(key)
} else {
r0 = ret.Get(0).(string)
}
if returnFunc, ok := ret.Get(1).(func(auth.Key) error); ok {
r1 = returnFunc(key)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Tokenizer_Issue_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Issue'
type Tokenizer_Issue_Call struct {
*mock.Call
}
// Issue is a helper method to define mock.On call
// - key auth.Key
func (_e *Tokenizer_Expecter) Issue(key interface{}) *Tokenizer_Issue_Call {
return &Tokenizer_Issue_Call{Call: _e.mock.On("Issue", key)}
}
func (_c *Tokenizer_Issue_Call) Run(run func(key auth.Key)) *Tokenizer_Issue_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 auth.Key
if args[0] != nil {
arg0 = args[0].(auth.Key)
}
run(
arg0,
)
})
return _c
}
func (_c *Tokenizer_Issue_Call) Return(token string, err error) *Tokenizer_Issue_Call {
_c.Call.Return(token, err)
return _c
}
func (_c *Tokenizer_Issue_Call) RunAndReturn(run func(key auth.Key) (string, error)) *Tokenizer_Issue_Call {
_c.Call.Return(run)
return _c
}
// Parse provides a mock function for the type Tokenizer
func (_mock *Tokenizer) Parse(ctx context.Context, token string) (auth.Key, error) {
ret := _mock.Called(ctx, token)
if len(ret) == 0 {
panic("no return value specified for Parse")
}
var r0 auth.Key
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) (auth.Key, error)); ok {
return returnFunc(ctx, token)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string) auth.Key); ok {
r0 = returnFunc(ctx, token)
} else {
r0 = ret.Get(0).(auth.Key)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = returnFunc(ctx, token)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Tokenizer_Parse_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Parse'
type Tokenizer_Parse_Call struct {
*mock.Call
}
// Parse is a helper method to define mock.On call
// - ctx context.Context
// - token string
func (_e *Tokenizer_Expecter) Parse(ctx interface{}, token interface{}) *Tokenizer_Parse_Call {
return &Tokenizer_Parse_Call{Call: _e.mock.On("Parse", ctx, token)}
}
func (_c *Tokenizer_Parse_Call) Run(run func(ctx context.Context, token string)) *Tokenizer_Parse_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Tokenizer_Parse_Call) Return(key auth.Key, err error) *Tokenizer_Parse_Call {
_c.Call.Return(key, err)
return _c
}
func (_c *Tokenizer_Parse_Call) RunAndReturn(run func(ctx context.Context, token string) (auth.Key, error)) *Tokenizer_Parse_Call {
_c.Call.Return(run)
return _c
}
// RetrieveJWKS provides a mock function for the type Tokenizer
func (_mock *Tokenizer) RetrieveJWKS() ([]auth.PublicKeyInfo, error) {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for RetrieveJWKS")
}
var r0 []auth.PublicKeyInfo
var r1 error
if returnFunc, ok := ret.Get(0).(func() ([]auth.PublicKeyInfo, error)); ok {
return returnFunc()
}
if returnFunc, ok := ret.Get(0).(func() []auth.PublicKeyInfo); ok {
r0 = returnFunc()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]auth.PublicKeyInfo)
}
}
if returnFunc, ok := ret.Get(1).(func() error); ok {
r1 = returnFunc()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Tokenizer_RetrieveJWKS_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveJWKS'
type Tokenizer_RetrieveJWKS_Call struct {
*mock.Call
}
// RetrieveJWKS is a helper method to define mock.On call
func (_e *Tokenizer_Expecter) RetrieveJWKS() *Tokenizer_RetrieveJWKS_Call {
return &Tokenizer_RetrieveJWKS_Call{Call: _e.mock.On("RetrieveJWKS")}
}
func (_c *Tokenizer_RetrieveJWKS_Call) Run(run func()) *Tokenizer_RetrieveJWKS_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Tokenizer_RetrieveJWKS_Call) Return(publicKeyInfos []auth.PublicKeyInfo, err error) *Tokenizer_RetrieveJWKS_Call {
_c.Call.Return(publicKeyInfos, err)
return _c
}
func (_c *Tokenizer_RetrieveJWKS_Call) RunAndReturn(run func() ([]auth.PublicKeyInfo, error)) *Tokenizer_RetrieveJWKS_Call {
_c.Call.Return(run)
return _c
}
+316
View File
@@ -0,0 +1,316 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"context"
"time"
"github.com/absmach/supermq/auth"
mock "github.com/stretchr/testify/mock"
)
// NewUserActiveTokensCache creates a new instance of UserActiveTokensCache. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewUserActiveTokensCache(t interface {
mock.TestingT
Cleanup(func())
}) *UserActiveTokensCache {
mock := &UserActiveTokensCache{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// UserActiveTokensCache is an autogenerated mock type for the UserActiveTokensCache type
type UserActiveTokensCache struct {
mock.Mock
}
type UserActiveTokensCache_Expecter struct {
mock *mock.Mock
}
func (_m *UserActiveTokensCache) EXPECT() *UserActiveTokensCache_Expecter {
return &UserActiveTokensCache_Expecter{mock: &_m.Mock}
}
// IsActive provides a mock function for the type UserActiveTokensCache
func (_mock *UserActiveTokensCache) IsActive(ctx context.Context, tokenID string) (bool, error) {
ret := _mock.Called(ctx, tokenID)
if len(ret) == 0 {
panic("no return value specified for IsActive")
}
var r0 bool
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok {
return returnFunc(ctx, tokenID)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string) bool); ok {
r0 = returnFunc(ctx, tokenID)
} else {
r0 = ret.Get(0).(bool)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = returnFunc(ctx, tokenID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UserActiveTokensCache_IsActive_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsActive'
type UserActiveTokensCache_IsActive_Call struct {
*mock.Call
}
// IsActive is a helper method to define mock.On call
// - ctx context.Context
// - tokenID string
func (_e *UserActiveTokensCache_Expecter) IsActive(ctx interface{}, tokenID interface{}) *UserActiveTokensCache_IsActive_Call {
return &UserActiveTokensCache_IsActive_Call{Call: _e.mock.On("IsActive", ctx, tokenID)}
}
func (_c *UserActiveTokensCache_IsActive_Call) Run(run func(ctx context.Context, tokenID string)) *UserActiveTokensCache_IsActive_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *UserActiveTokensCache_IsActive_Call) Return(b bool, err error) *UserActiveTokensCache_IsActive_Call {
_c.Call.Return(b, err)
return _c
}
func (_c *UserActiveTokensCache_IsActive_Call) RunAndReturn(run func(ctx context.Context, tokenID string) (bool, error)) *UserActiveTokensCache_IsActive_Call {
_c.Call.Return(run)
return _c
}
// ListUserTokens provides a mock function for the type UserActiveTokensCache
func (_mock *UserActiveTokensCache) ListUserTokens(ctx context.Context, userID string) ([]auth.TokenInfo, error) {
ret := _mock.Called(ctx, userID)
if len(ret) == 0 {
panic("no return value specified for ListUserTokens")
}
var r0 []auth.TokenInfo
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]auth.TokenInfo, error)); ok {
return returnFunc(ctx, userID)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string) []auth.TokenInfo); ok {
r0 = returnFunc(ctx, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]auth.TokenInfo)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = returnFunc(ctx, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UserActiveTokensCache_ListUserTokens_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListUserTokens'
type UserActiveTokensCache_ListUserTokens_Call struct {
*mock.Call
}
// ListUserTokens is a helper method to define mock.On call
// - ctx context.Context
// - userID string
func (_e *UserActiveTokensCache_Expecter) ListUserTokens(ctx interface{}, userID interface{}) *UserActiveTokensCache_ListUserTokens_Call {
return &UserActiveTokensCache_ListUserTokens_Call{Call: _e.mock.On("ListUserTokens", ctx, userID)}
}
func (_c *UserActiveTokensCache_ListUserTokens_Call) Run(run func(ctx context.Context, userID string)) *UserActiveTokensCache_ListUserTokens_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *UserActiveTokensCache_ListUserTokens_Call) Return(tokenInfos []auth.TokenInfo, err error) *UserActiveTokensCache_ListUserTokens_Call {
_c.Call.Return(tokenInfos, err)
return _c
}
func (_c *UserActiveTokensCache_ListUserTokens_Call) RunAndReturn(run func(ctx context.Context, userID string) ([]auth.TokenInfo, error)) *UserActiveTokensCache_ListUserTokens_Call {
_c.Call.Return(run)
return _c
}
// RemoveActive provides a mock function for the type UserActiveTokensCache
func (_mock *UserActiveTokensCache) RemoveActive(ctx context.Context, userID string, tokenID string) error {
ret := _mock.Called(ctx, userID, tokenID)
if len(ret) == 0 {
panic("no return value specified for RemoveActive")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = returnFunc(ctx, userID, tokenID)
} else {
r0 = ret.Error(0)
}
return r0
}
// UserActiveTokensCache_RemoveActive_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveActive'
type UserActiveTokensCache_RemoveActive_Call struct {
*mock.Call
}
// RemoveActive is a helper method to define mock.On call
// - ctx context.Context
// - userID string
// - tokenID string
func (_e *UserActiveTokensCache_Expecter) RemoveActive(ctx interface{}, userID interface{}, tokenID interface{}) *UserActiveTokensCache_RemoveActive_Call {
return &UserActiveTokensCache_RemoveActive_Call{Call: _e.mock.On("RemoveActive", ctx, userID, tokenID)}
}
func (_c *UserActiveTokensCache_RemoveActive_Call) Run(run func(ctx context.Context, userID string, tokenID string)) *UserActiveTokensCache_RemoveActive_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *UserActiveTokensCache_RemoveActive_Call) Return(err error) *UserActiveTokensCache_RemoveActive_Call {
_c.Call.Return(err)
return _c
}
func (_c *UserActiveTokensCache_RemoveActive_Call) RunAndReturn(run func(ctx context.Context, userID string, tokenID string) error) *UserActiveTokensCache_RemoveActive_Call {
_c.Call.Return(run)
return _c
}
// SaveActive provides a mock function for the type UserActiveTokensCache
func (_mock *UserActiveTokensCache) SaveActive(ctx context.Context, userID string, tokenID string, description string, expiry time.Time) error {
ret := _mock.Called(ctx, userID, tokenID, description, expiry)
if len(ret) == 0 {
panic("no return value specified for SaveActive")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, time.Time) error); ok {
r0 = returnFunc(ctx, userID, tokenID, description, expiry)
} else {
r0 = ret.Error(0)
}
return r0
}
// UserActiveTokensCache_SaveActive_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveActive'
type UserActiveTokensCache_SaveActive_Call struct {
*mock.Call
}
// SaveActive is a helper method to define mock.On call
// - ctx context.Context
// - userID string
// - tokenID string
// - description string
// - expiry time.Time
func (_e *UserActiveTokensCache_Expecter) SaveActive(ctx interface{}, userID interface{}, tokenID interface{}, description interface{}, expiry interface{}) *UserActiveTokensCache_SaveActive_Call {
return &UserActiveTokensCache_SaveActive_Call{Call: _e.mock.On("SaveActive", ctx, userID, tokenID, description, expiry)}
}
func (_c *UserActiveTokensCache_SaveActive_Call) Run(run func(ctx context.Context, userID string, tokenID string, description string, expiry time.Time)) *UserActiveTokensCache_SaveActive_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
var arg3 string
if args[3] != nil {
arg3 = args[3].(string)
}
var arg4 time.Time
if args[4] != nil {
arg4 = args[4].(time.Time)
}
run(
arg0,
arg1,
arg2,
arg3,
arg4,
)
})
return _c
}
func (_c *UserActiveTokensCache_SaveActive_Call) Return(err error) *UserActiveTokensCache_SaveActive_Call {
_c.Call.Return(err)
return _c
}
func (_c *UserActiveTokensCache_SaveActive_Call) RunAndReturn(run func(ctx context.Context, userID string, tokenID string, description string, expiry time.Time) error) *UserActiveTokensCache_SaveActive_Call {
_c.Call.Return(run)
return _c
}
+143 -144
View File
@@ -12,114 +12,53 @@ import (
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/pkg/errors"
)
const AnyIDs = "*"
type Operation uint32
const (
CreateOp Operation = iota
ReadOp
ListOp
UpdateOp
DeleteOp
ShareOp
UnshareOp
PublishOp
SubscribeOp
"github.com/absmach/supermq/pkg/permissions"
)
const (
createOpStr = "create"
readOpStr = "read"
listOpStr = "list"
updateOpStr = "update"
deleteOpStr = "delete"
shareOpStr = "share"
UnshareOpStr = "unshare"
PublishOpStr = "publish"
SubscribeOpStr = "subscribe"
AnyIDs = "*"
RoleOperationPrefix = "role_"
)
func (op Operation) String() string {
switch op {
case CreateOp:
return createOpStr
case ReadOp:
return readOpStr
case ListOp:
return listOpStr
case UpdateOp:
return updateOpStr
case DeleteOp:
return deleteOpStr
case ShareOp:
return shareOpStr
case UnshareOp:
return UnshareOpStr
case PublishOp:
return PublishOpStr
case SubscribeOp:
return SubscribeOpStr
default:
return fmt.Sprintf("unknown operation type %d", op)
}
}
const (
OpCreate = "create"
OpList = "list"
func (op Operation) ValidString() (string, error) {
str := op.String()
if str == fmt.Sprintf("unknown operation type %d", op) {
return "", errors.New(str)
}
return str, nil
}
OpCreateClients = "create_clients"
OpListClients = "list_clients"
OpCreateChannels = "create_channels"
OpListChannels = "list_channels"
OpCreateGroups = "create_groups"
OpListGroups = "list_groups"
func ParseOperation(op string) (Operation, error) {
switch op {
case createOpStr:
return CreateOp, nil
case readOpStr:
return ReadOp, nil
case listOpStr:
return ListOp, nil
case updateOpStr:
return UpdateOp, nil
case deleteOpStr:
return DeleteOp, nil
case shareOpStr:
return ShareOp, nil
case UnshareOpStr:
return UnshareOp, nil
case PublishOpStr:
return PublishOp, nil
case SubscribeOpStr:
return SubscribeOp, nil
default:
return 0, fmt.Errorf("unknown operation type %s", op)
}
}
OpShare = "share"
OpUnshare = "unshare"
func (op Operation) MarshalJSON() ([]byte, error) {
return json.Marshal(op.String())
}
OpDashboardShare = "dashboard_share"
OpDashboardUnshare = "dashboard_unshare"
func (op *Operation) UnmarshalJSON(data []byte) error {
str := strings.Trim(string(data), "\"")
val, err := ParseOperation(str)
*op = val
return err
}
OpPublish = "publish"
OpSubscribe = "subscribe"
func (op Operation) MarshalText() (text []byte, err error) {
return []byte(op.String()), nil
}
OpMessagePublish = "message_publish"
OpMessageSubscribe = "message_subscribe"
)
func (op *Operation) UnmarshalText(data []byte) (err error) {
str := strings.Trim(string(data), "\"")
*op, err = ParseOperation(str)
return err
}
var errInvalidEntityOp = errors.NewRequestError("operation not valid for entity type")
type Operation = permissions.Operation
// Dashboard operations.
const (
DashboardShareOp Operation = iota + 400
DashboardUnshareOp
)
// Messages operations.
const (
MessagePublishOp Operation = iota + 500
MessageSubscribeOp
)
type EntityType uint32
@@ -127,20 +66,20 @@ const (
GroupsType EntityType = iota
ChannelsType
ClientsType
DomainsType
UsersType
DashboardType
MessagesType
DomainsType
UsersType
)
const (
GroupsScopeStr = "groups"
ChannelsScopeStr = "channels"
ClientsScopeStr = "clients"
DomainsStr = "domains"
UsersStr = "users"
DashboardsStr = "dashboards"
MessagesStr = "messages"
DomainsStr = "domains"
UsersStr = "users"
)
func (et EntityType) String() string {
@@ -151,27 +90,19 @@ func (et EntityType) String() string {
return ChannelsScopeStr
case ClientsType:
return ClientsScopeStr
case DomainsType:
return DomainsStr
case UsersType:
return UsersStr
case DashboardType:
return DashboardsStr
case MessagesType:
return MessagesStr
case DomainsType:
return DomainsStr
case UsersType:
return UsersStr
default:
return fmt.Sprintf("unknown domain entity type %d", et)
}
}
func (et EntityType) ValidString() (string, error) {
str := et.String()
if str == fmt.Sprintf("unknown operation type %d", et) {
return "", errors.New(str)
}
return str, nil
}
func ParseEntityType(et string) (EntityType, error) {
switch et {
case GroupsScopeStr:
@@ -180,12 +111,14 @@ func ParseEntityType(et string) (EntityType, error) {
return ChannelsType, nil
case ClientsScopeStr:
return ClientsType, nil
case DashboardsStr:
return DashboardType, nil
case MessagesStr:
return MessagesType, nil
case DomainsStr:
return DomainsType, nil
case UsersStr:
return UsersType, nil
case DashboardsStr:
return DashboardType, nil
default:
return 0, fmt.Errorf("unknown domain entity type %s", et)
}
@@ -212,39 +145,101 @@ func (et *EntityType) UnmarshalText(data []byte) (err error) {
return err
}
func IsValidOperationForEntity(entityType EntityType, operation string) bool {
switch entityType {
case ClientsType, ChannelsType, GroupsType, DomainsType:
return true
case DashboardType:
return operation == OpDashboardShare || operation == OpDashboardUnshare
case MessagesType:
return operation == OpMessagePublish || operation == OpMessageSubscribe
default:
return false
}
}
// Example Scope as JSON
//
// [
// {
// "optional_domain_id": "domain_1",
// "domain_id": "domain_1",
// "entity_type": "groups",
// "operation": "create",
// "operation": "view",
// "entity_id": "*"
// },
// {
// "optional_domain_id": "domain_1",
// "domain_id": "domain_1",
// "entity_type": "channels",
// "operation": "delete",
// "entity_id": "channel1"
// },
// {
// "optional_domain_id": "domain_1",
// "entity_type": "things",
// "domain_id": "domain_1",
// "entity_type": "clients",
// "operation": "update",
// "entity_id": "*"
// }
// ]
type Scope struct {
ID string `json:"id"`
PatID string `json:"pat_id"`
OptionalDomainID string `json:"optional_domain_id"`
EntityType EntityType `json:"entity_type"`
EntityID string `json:"entity_id"`
Operation Operation `json:"operation"`
ID string `json:"id"`
PatID string `json:"pat_id"`
DomainID string `json:"domain_id"`
EntityType EntityType `json:"entity_type"`
EntityID string `json:"entity_id"`
Operation string `json:"operation"`
}
func (s *Scope) Authorized(entityType EntityType, optionalDomainID string, operation Operation, entityID string) bool {
func (s *Scope) UnmarshalJSON(data []byte) error {
type Alias Scope
aux := (*Alias)(s)
if err := json.Unmarshal(data, aux); err != nil {
return err
}
switch s.EntityType {
case ClientsType:
switch s.Operation {
case OpCreate:
s.Operation = OpCreateClients
case OpList:
s.Operation = OpListClients
}
case ChannelsType:
switch s.Operation {
case OpCreate:
s.Operation = OpCreateChannels
case OpList:
s.Operation = OpListChannels
}
case GroupsType:
switch s.Operation {
case OpCreate:
s.Operation = OpCreateGroups
case OpList:
s.Operation = OpListGroups
}
case DashboardType:
switch s.Operation {
case OpShare:
s.Operation = OpDashboardShare
case OpUnshare:
s.Operation = OpDashboardUnshare
}
case MessagesType:
switch s.Operation {
case OpPublish:
s.Operation = OpMessagePublish
case OpSubscribe:
s.Operation = OpMessageSubscribe
}
}
return nil
}
func (s *Scope) Authorized(entityType EntityType, domainID string, operation string, entityID string) bool {
if s == nil {
return false
}
@@ -253,7 +248,7 @@ func (s *Scope) Authorized(entityType EntityType, optionalDomainID string, opera
return false
}
if optionalDomainID != "" && s.OptionalDomainID != optionalDomainID {
if s.DomainID != "" && s.DomainID != domainID {
return false
}
@@ -268,6 +263,7 @@ func (s *Scope) Authorized(entityType EntityType, optionalDomainID string, opera
if s.EntityID == entityID {
return true
}
return false
}
@@ -279,16 +275,27 @@ func (s *Scope) Validate() error {
return apiutil.ErrMissingEntityID
}
switch s.EntityType {
case ChannelsType, GroupsType, ClientsType:
if s.OptionalDomainID == "" {
return apiutil.ErrMissingDomainID
}
if s.DomainID == "" {
return apiutil.ErrMissingDomainID
}
if !IsValidOperationForEntity(s.EntityType, s.Operation) {
return errors.Wrap(apiutil.ErrInvalidQueryParams, errInvalidEntityOp)
}
return nil
}
// PATAuthz represents the PAT authorization request fields.
type PATAuthz struct {
PatID string
UserID string
EntityType EntityType
EntityID string
Operation string
Domain string
}
// PAT represents Personal Access Token.
type PAT struct {
ID string `json:"id,omitempty"`
@@ -356,14 +363,6 @@ func (pat *PAT) Validate() error {
return nil
}
func (pat *PAT) String() string {
str, err := json.MarshalIndent(pat, "", " ")
if err != nil {
return fmt.Sprintf("failed to convert PAT to string: json marshal error :%s", err.Error())
}
return string(str)
}
// PATS specifies function which are required for Personal access Token implementation.
type PATS interface {
// Create function creates new PAT for given valid inputs.
@@ -409,7 +408,7 @@ type PATS interface {
IdentifyPAT(ctx context.Context, paToken string) (PAT, error)
// AuthorizePAT function will valid the secret and check the given scope exists.
AuthorizePAT(ctx context.Context, userID, patID string, entityType EntityType, optionalDomainID string, operation Operation, entityID string) error
AuthorizePAT(ctx context.Context, userID, patID string, entityType EntityType, domainID string, operation string, entityID string) error
}
// PATSRepository specifies PATS persistence API.
@@ -454,7 +453,7 @@ type PATSRepository interface {
RemoveScope(ctx context.Context, userID string, scopesIDs ...string) error
CheckScope(ctx context.Context, userID, patID string, entityType EntityType, optionalDomainID string, operation Operation, entityID string) error
CheckScope(ctx context.Context, userID, patID string, entityType EntityType, domainID string, operation string, entityID string) error
RemoveAllScope(ctx context.Context, patID string) error
}
@@ -462,7 +461,7 @@ type PATSRepository interface {
type Cache interface {
Save(ctx context.Context, userID string, scopes []Scope) error
CheckScope(ctx context.Context, userID, patID, optionalDomainID string, entityType EntityType, operation Operation, entityID string) bool
CheckScope(ctx context.Context, userID, patID, optionalDomainID string, entityType EntityType, operation string, entityID string) bool
Remove(ctx context.Context, userID string, scopesID []string) error
+257
View File
@@ -0,0 +1,257 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package auth_test
import (
"testing"
"github.com/absmach/supermq/auth"
"github.com/stretchr/testify/assert"
)
func TestEntityTypeString(t *testing.T) {
cases := []struct {
desc string
et auth.EntityType
expected string
}{
{
desc: "Groups entity type",
et: auth.GroupsType,
expected: "groups",
},
{
desc: "Channels entity type",
et: auth.ChannelsType,
expected: "channels",
},
{
desc: "Clients entity type",
et: auth.ClientsType,
expected: "clients",
},
{
desc: "Dashboard entity type",
et: auth.DashboardType,
expected: "dashboards",
},
{
desc: "Messages entity type",
et: auth.MessagesType,
expected: "messages",
},
{
desc: "Unknown entity type",
et: auth.EntityType(100),
expected: "unknown domain entity type 100",
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got := tc.et.String()
assert.Equal(t, tc.expected, got, "String() = %v, expected %v", got, tc.expected)
})
}
}
func TestParseEntityType(t *testing.T) {
cases := []struct {
desc string
et string
expected auth.EntityType
err bool
}{
{
desc: "Parse groups",
et: "groups",
expected: auth.GroupsType,
err: false,
},
{
desc: "Parse channels",
et: "channels",
expected: auth.ChannelsType,
err: false,
},
{
desc: "Parse clients",
et: "clients",
expected: auth.ClientsType,
err: false,
},
{
desc: "Parse dashboards",
et: "dashboards",
expected: auth.DashboardType,
err: false,
},
{
desc: "Parse unknown entity type",
et: "unknown",
expected: auth.EntityType(0),
err: true,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got, err := auth.ParseEntityType(tc.et)
if tc.err {
assert.Error(t, err, "ParseEntityType() should return error")
} else {
assert.NoError(t, err, "ParseEntityType() should not return error")
assert.Equal(t, tc.expected, got, "ParseEntityType() = %v, expected %v", got, tc.expected)
}
})
}
}
func TestEntityTypeMarshalJSON(t *testing.T) {
cases := []struct {
desc string
et auth.EntityType
expected []byte
err error
}{
{
desc: "Marshal groups",
et: auth.GroupsType,
expected: []byte(`"groups"`),
err: nil,
},
{
desc: "Marshal channels",
et: auth.ChannelsType,
expected: []byte(`"channels"`),
err: nil,
},
{
desc: "Marshal clients",
et: auth.ClientsType,
expected: []byte(`"clients"`),
err: nil,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got, err := tc.et.MarshalJSON()
assert.Equal(t, tc.err, err, "MarshalJSON() error = %v, expected %v", err, tc.err)
assert.Equal(t, tc.expected, got, "MarshalJSON() = %v, expected %v", got, tc.expected)
})
}
}
func TestEntityTypeUnmarshalJSON(t *testing.T) {
cases := []struct {
desc string
data []byte
expected auth.EntityType
err bool
}{
{
desc: "Unmarshal groups",
data: []byte(`"groups"`),
expected: auth.GroupsType,
err: false,
},
{
desc: "Unmarshal channels",
data: []byte(`"channels"`),
expected: auth.ChannelsType,
err: false,
},
{
desc: "Unmarshal unknown",
data: []byte(`"unknown"`),
expected: auth.EntityType(0),
err: true,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
var et auth.EntityType
err := et.UnmarshalJSON(tc.data)
if tc.err {
assert.Error(t, err, "UnmarshalJSON() should return error")
} else {
assert.NoError(t, err, "UnmarshalJSON() should not return error")
assert.Equal(t, tc.expected, et, "UnmarshalJSON() = %v, expected %v", et, tc.expected)
}
})
}
}
func TestEntityTypeMarshalText(t *testing.T) {
cases := []struct {
desc string
et auth.EntityType
expected []byte
err error
}{
{
desc: "Marshal groups as text",
et: auth.GroupsType,
expected: []byte("groups"),
err: nil,
},
{
desc: "Marshal channels as text",
et: auth.ChannelsType,
expected: []byte("channels"),
err: nil,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got, err := tc.et.MarshalText()
assert.Equal(t, tc.err, err, "MarshalText() error = %v, expected %v", err, tc.err)
assert.Equal(t, tc.expected, got, "MarshalText() = %v, expected %v", got, tc.expected)
})
}
}
func TestEntityTypeUnmarshalText(t *testing.T) {
cases := []struct {
desc string
data []byte
expected auth.EntityType
err bool
}{
{
desc: "Unmarshal groups from text",
data: []byte("groups"),
expected: auth.GroupsType,
err: false,
},
{
desc: "Unmarshal channels from text",
data: []byte("channels"),
expected: auth.ChannelsType,
err: false,
},
{
desc: "Unmarshal unknown from text",
data: []byte("unknown"),
expected: auth.EntityType(0),
err: true,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
var et auth.EntityType
err := et.UnmarshalText(tc.data)
if tc.err {
assert.Error(t, err, "UnmarshalText() should return error")
} else {
assert.NoError(t, err, "UnmarshalText() should not return error")
assert.Equal(t, tc.expected, et, "UnmarshalText() = %v, expected %v", et, tc.expected)
}
})
}
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package postgres
import "github.com/absmach/supermq/pkg/errors"
var _ errors.Mapper = (*duplicateErrors)(nil)
type duplicateErrors struct{}
// GetError maps constraint names to known errors.
func (d duplicateErrors) GetError(constraint string) (error, bool) {
switch constraint {
case "revoked_tokens_pkey":
return errors.NewRequestError("revoked token already exists"), true
default:
return nil, false
}
}
func NewDuplicateErrors() errors.Mapper {
return duplicateErrors{}
}
+9
View File
@@ -125,6 +125,15 @@ func Migration() *migrate.MemoryMigrationSource {
`ALTER TABLE pats ALTER COLUMN last_used_at TYPE TIMESTAMP;`,
},
},
{
Id: "auth_7",
Up: []string{
`ALTER TABLE pat_scopes RENAME COLUMN optional_domain_id TO domain_id;`,
},
Down: []string{
`ALTER TABLE pat_scopes RENAME COLUMN domain_id TO optional_domain_id;`,
},
},
},
}
}
+18 -22
View File
@@ -26,12 +26,12 @@ type dbPat struct {
}
type dbScope struct {
ID string `db:"id,omitempty"`
PatID string `db:"pat_id,omitempty"`
OptionalDomainID string `db:"optional_domain_id,omitempty"`
EntityType string `db:"entity_type,omitempty"`
EntityID string `db:"entity_id,omitempty"`
Operation string `db:"operation,omitempty"`
ID string `db:"id,omitempty"`
PatID string `db:"pat_id,omitempty"`
DomainID string `db:"domain_id,omitempty"`
EntityType string `db:"entity_type,omitempty"`
EntityID string `db:"entity_id,omitempty"`
Operation string `db:"operation,omitempty"`
}
type dbPagemeta struct {
@@ -92,17 +92,13 @@ func toAuthScope(dsc []dbScope) ([]auth.Scope, error) {
if err != nil {
return []auth.Scope{}, err
}
operation, err := auth.ParseOperation(s.Operation)
if err != nil {
return []auth.Scope{}, err
}
scope = append(scope, auth.Scope{
ID: s.ID,
PatID: s.PatID,
OptionalDomainID: s.OptionalDomainID,
EntityType: entityType,
EntityID: s.EntityID,
Operation: operation,
ID: s.ID,
PatID: s.PatID,
DomainID: s.DomainID,
EntityType: entityType,
EntityID: s.EntityID,
Operation: s.Operation,
})
}
@@ -152,12 +148,12 @@ func toDBScope(sc []auth.Scope) []dbScope {
var scopes []dbScope
for _, s := range sc {
scopes = append(scopes, dbScope{
ID: s.ID,
PatID: s.PatID,
OptionalDomainID: s.OptionalDomainID,
EntityType: s.EntityType.String(),
EntityID: s.EntityID,
Operation: s.Operation.String(),
ID: s.ID,
PatID: s.PatID,
DomainID: s.DomainID,
EntityType: s.EntityType.String(),
EntityID: s.EntityID,
Operation: s.Operation,
})
}
return scopes
+36 -38
View File
@@ -18,6 +18,8 @@ import (
var _ auth.PATSRepository = (*patRepo)(nil)
var errInsufficientPATScope = errors.NewRequestError("PAT does not have the required scope permissions")
type patRepo struct {
db postgres.Database
cache auth.Cache
@@ -374,8 +376,8 @@ func (pr *patRepo) RemoveAllPAT(ctx context.Context, userID string) error {
func (pr *patRepo) AddScope(ctx context.Context, userID string, scopes []auth.Scope) error {
q := `
INSERT INTO pat_scopes (id, pat_id, entity_type, optional_domain_id, operation, entity_id)
VALUES (:id, :pat_id, :entity_type, :optional_domain_id, :operation, :entity_id)`
INSERT INTO pat_scopes (id, pat_id, entity_type, domain_id, operation, entity_id)
VALUES (:id, :pat_id, :entity_type, :domain_id, :operation, :entity_id)`
var newScopes []auth.Scope
@@ -410,17 +412,17 @@ func (pr *patRepo) processScope(ctx context.Context, sc auth.Scope) (auth.Scope,
FROM pat_scopes
WHERE pat_id = :pat_id
AND entity_type = :entity_type
AND optional_domain_id = :optional_domain_id
AND domain_id = :domain_id
AND operation = :operation
AND entity_id = :entity_id
LIMIT 1`
params := dbScope{
PatID: sc.PatID,
OptionalDomainID: sc.OptionalDomainID,
EntityType: sc.EntityType.String(),
Operation: sc.Operation.String(),
EntityID: auth.AnyIDs,
PatID: sc.PatID,
DomainID: sc.DomainID,
EntityType: sc.EntityType.String(),
Operation: sc.Operation,
EntityID: auth.AnyIDs,
}
rows, err := pr.db.NamedQueryContext(ctx, q, params)
@@ -442,10 +444,10 @@ func (pr *patRepo) processScope(ctx context.Context, sc auth.Scope) (auth.Scope,
if sc.EntityID == auth.AnyIDs {
newParams := dbScope{
PatID: sc.PatID,
OptionalDomainID: sc.OptionalDomainID,
EntityType: sc.EntityType.String(),
Operation: sc.Operation.String(),
PatID: sc.PatID,
DomainID: sc.DomainID,
EntityType: sc.EntityType.String(),
Operation: sc.Operation,
}
checkEntityQuery := `
@@ -453,7 +455,7 @@ func (pr *patRepo) processScope(ctx context.Context, sc auth.Scope) (auth.Scope,
FROM pat_scopes
WHERE pat_id = :pat_id
AND entity_type = :entity_type
AND optional_domain_id = :optional_domain_id
AND domain_id = :domain_id
AND operation = :operation
LIMIT 1`
@@ -476,7 +478,7 @@ func (pr *patRepo) processScope(ctx context.Context, sc auth.Scope) (auth.Scope,
SET entity_id = :entity_id
WHERE pat_id = :pat_id
AND entity_type = :entity_type
AND optional_domain_id = :optional_domain_id
AND domain_id = :domain_id
AND operation = :operation`
rows, err = pr.db.NamedQueryContext(ctx, updateWithWildcardQuery, params)
@@ -493,9 +495,9 @@ func (pr *patRepo) processScope(ctx context.Context, sc auth.Scope) (auth.Scope,
}
func (pr *patRepo) RemoveScope(ctx context.Context, userID string, scopesIDs ...string) error {
deleteScopesQuery := fmt.Sprintf(`DELETE FROM pat_scopes WHERE id IN ('%s')`, strings.Join(scopesIDs, ","))
deleteScopesQuery := `DELETE FROM pat_scopes WHERE id = ANY($1)`
res, err := pr.db.ExecContext(ctx, deleteScopesQuery)
res, err := pr.db.ExecContext(ctx, deleteScopesQuery, scopesIDs)
if err != nil {
return errors.Wrap(repoerr.ErrRemoveEntity, err)
}
@@ -511,28 +513,28 @@ func (pr *patRepo) RemoveScope(ctx context.Context, userID string, scopesIDs ...
return nil
}
func (pr *patRepo) CheckScope(ctx context.Context, userID, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error {
func (pr *patRepo) CheckScope(ctx context.Context, userID, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error {
q := `
SELECT id, pat_id, entity_type, optional_domain_id, operation, entity_id
SELECT id, pat_id, entity_type, domain_id, operation, entity_id
FROM pat_scopes
WHERE pat_id = :pat_id
AND entity_type = :entity_type
AND optional_domain_id = :optional_domain_id
AND domain_id = :domain_id
AND operation = :operation
AND (entity_id = :entity_id OR entity_id = '*')
LIMIT 1`
authorized := pr.cache.CheckScope(ctx, userID, patID, optionalDomainID, entityType, operation, entityID)
authorized := pr.cache.CheckScope(ctx, userID, patID, domainID, entityType, operation, entityID)
if authorized {
return nil
}
scope := dbScope{
PatID: patID,
EntityType: entityType.String(),
OptionalDomainID: optionalDomainID,
Operation: operation.String(),
EntityID: entityID,
PatID: patID,
EntityType: entityType.String(),
DomainID: domainID,
Operation: operation,
EntityID: entityID,
}
rows, err := pr.db.NamedQueryContext(ctx, q, scope)
@@ -551,29 +553,25 @@ func (pr *patRepo) CheckScope(ctx context.Context, userID, patID string, entityT
if err != nil {
return errors.Wrap(repoerr.ErrViewEntity, err)
}
operation, err := auth.ParseOperation(sc.Operation)
if err != nil {
return errors.Wrap(repoerr.ErrViewEntity, err)
}
authScope := auth.Scope{
ID: sc.ID,
PatID: sc.PatID,
OptionalDomainID: sc.OptionalDomainID,
EntityType: entityType,
EntityID: sc.EntityID,
Operation: operation,
ID: sc.ID,
PatID: sc.PatID,
DomainID: sc.DomainID,
EntityType: entityType,
EntityID: sc.EntityID,
Operation: sc.Operation,
}
if err := pr.cache.Save(ctx, userID, []auth.Scope{authScope}); err != nil {
return err
}
if authScope.Authorized(entityType, optionalDomainID, operation, entityID) {
if authScope.Authorized(entityType, domainID, operation, entityID) {
return nil
}
}
return repoerr.ErrNotFound
return errInsufficientPATScope
}
func (pr *patRepo) RemoveAllScope(ctx context.Context, patID string) error {
@@ -625,7 +623,7 @@ func (pr *patRepo) RetrieveScope(ctx context.Context, pm auth.ScopesPageMeta) (a
func (pr *patRepo) retrieveScopeFromDB(ctx context.Context, pm dbPagemeta) ([]auth.Scope, error) {
q := `
SELECT id, pat_id, entity_type, optional_domain_id, operation, entity_id
SELECT id, pat_id, entity_type, domain_id, operation, entity_id
FROM pat_scopes WHERE pat_id = :pat_id OFFSET :offset LIMIT :limit`
scopeRows, err := pr.db.NamedQueryContext(ctx, q, pm)
if err != nil {
+376
View File
@@ -0,0 +1,376 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package auth_test
import (
"testing"
"time"
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/auth"
channelsOps "github.com/absmach/supermq/channels/operations"
clientsOps "github.com/absmach/supermq/clients/operations"
groupsOps "github.com/absmach/supermq/groups/operations"
"github.com/stretchr/testify/assert"
)
func TestScopeAuthorized(t *testing.T) {
cases := []struct {
desc string
scope *auth.Scope
entityType auth.EntityType
domainID string
operation string
entityID string
expected bool
}{
{
desc: "Authorized with matching entity type, domain, operation and entity ID",
scope: &auth.Scope{
EntityType: auth.GroupsType,
DomainID: "domain1",
Operation: "view",
EntityID: "entity1",
},
entityType: auth.GroupsType,
domainID: "domain1",
operation: "view",
entityID: "entity1",
expected: true,
},
{
desc: "Authorized with wildcard entity ID",
scope: &auth.Scope{
EntityType: auth.GroupsType,
DomainID: "domain1",
Operation: "view",
EntityID: "*",
},
entityType: auth.GroupsType,
domainID: "domain1",
operation: "view",
entityID: "any-entity",
expected: true,
},
{
desc: "Authorized without domain ID",
scope: &auth.Scope{
EntityType: auth.ClientsType,
DomainID: "",
Operation: "view",
EntityID: "client1",
},
entityType: auth.ClientsType,
domainID: "domain1",
operation: "view",
entityID: "client1",
expected: true,
},
{
desc: "Not authorized with different entity type",
scope: &auth.Scope{
EntityType: auth.GroupsType,
DomainID: "domain1",
Operation: "view",
EntityID: "entity1",
},
entityType: auth.ChannelsType,
domainID: "domain1",
operation: "view",
entityID: "entity1",
expected: false,
},
{
desc: "Not authorized with different domain ID",
scope: &auth.Scope{
EntityType: auth.GroupsType,
DomainID: "domain1",
Operation: "view",
EntityID: "entity1",
},
entityType: auth.GroupsType,
domainID: "domain2",
operation: "view",
entityID: "entity1",
expected: false,
},
{
desc: "Not authorized with different operation",
scope: &auth.Scope{
EntityType: auth.GroupsType,
DomainID: "domain1",
Operation: "view",
EntityID: "entity1",
},
entityType: auth.GroupsType,
domainID: "domain1",
operation: "delete",
entityID: "entity1",
expected: false,
},
{
desc: "Not authorized with different entity ID",
scope: &auth.Scope{
EntityType: auth.GroupsType,
DomainID: "domain1",
Operation: "view",
EntityID: "entity1",
},
entityType: auth.GroupsType,
domainID: "domain1",
operation: "view",
entityID: "entity2",
expected: false,
},
{
desc: "Not authorized with nil scope",
scope: nil,
entityType: auth.GroupsType,
domainID: "domain1",
operation: "view",
entityID: "entity1",
expected: false,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
result := tc.scope.Authorized(tc.entityType, tc.domainID, tc.operation, tc.entityID)
assert.Equal(t, tc.expected, result, "Authorized() = %v, expected %v", result, tc.expected)
})
}
}
func TestScopeValidate(t *testing.T) {
cases := []struct {
desc string
scope *auth.Scope
err error
}{
{
desc: "Valid scope for groups with domain ID",
scope: &auth.Scope{
EntityType: auth.GroupsType,
DomainID: "domain1",
Operation: "view",
EntityID: "entity1",
},
err: nil,
},
{
desc: "Valid scope for channels with domain ID",
scope: &auth.Scope{
EntityType: auth.ChannelsType,
DomainID: "domain1",
Operation: "view",
EntityID: "channel1",
},
err: nil,
},
{
desc: "Valid scope for clients with domain ID",
scope: &auth.Scope{
EntityType: auth.ClientsType,
DomainID: "domain1",
Operation: "update",
EntityID: "client1",
},
err: nil,
},
{
desc: "Valid scope for messages with domain ID",
scope: &auth.Scope{
EntityType: auth.MessagesType,
DomainID: "domain1",
Operation: "message_publish",
EntityID: "message1",
},
err: nil,
},
{
desc: "Valid scope for dashboard with domain ID",
scope: &auth.Scope{
EntityType: auth.DashboardType,
DomainID: "domain1",
Operation: "dashboard_share",
EntityID: "dashboard1",
},
err: nil,
},
{
desc: "Valid scope with wildcard entity ID",
scope: &auth.Scope{
EntityType: auth.GroupsType,
DomainID: "domain1",
Operation: "view",
EntityID: "*",
},
err: nil,
},
{
desc: "Invalid nil scope",
scope: nil,
err: assert.AnError, // Will be checked with Contains
},
{
desc: "Invalid scope without entity ID",
scope: &auth.Scope{
EntityType: auth.GroupsType,
DomainID: "domain1",
Operation: groupsOps.OperationDetails()[groupsOps.OpViewGroup].Name,
EntityID: "",
},
err: apiutil.ErrMissingEntityID,
},
{
desc: "Invalid scope for groups without domain ID",
scope: &auth.Scope{
EntityType: auth.GroupsType,
DomainID: "",
Operation: groupsOps.OperationDetails()[groupsOps.OpViewGroup].Name,
EntityID: "entity1",
},
err: apiutil.ErrMissingDomainID,
},
{
desc: "Invalid scope for channels without domain ID",
scope: &auth.Scope{
EntityType: auth.ChannelsType,
DomainID: "",
Operation: channelsOps.OperationDetails()[channelsOps.OpViewChannel].Name,
EntityID: "channel1",
},
err: apiutil.ErrMissingDomainID,
},
{
desc: "Invalid scope for clients without domain ID",
scope: &auth.Scope{
EntityType: auth.ClientsType,
DomainID: "",
Operation: clientsOps.OperationDetails()[clientsOps.OpViewClient].Name,
EntityID: "client1",
},
err: apiutil.ErrMissingDomainID,
},
{
desc: "Invalid scope for dashboard without domain ID",
scope: &auth.Scope{
EntityType: auth.DashboardType,
DomainID: "",
Operation: auth.OpShare,
EntityID: "dashboard1",
},
err: apiutil.ErrMissingDomainID,
},
{
desc: "Invalid scope for messages without domain ID",
scope: &auth.Scope{
EntityType: auth.MessagesType,
DomainID: "",
Operation: auth.OpPublish,
EntityID: "message1",
},
err: apiutil.ErrMissingDomainID,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.scope.Validate()
if tc.err != nil {
assert.Error(t, err, "Validate() should return error")
if tc.err != assert.AnError {
assert.Equal(t, tc.err, err, "Validate() error = %v, expected %v", err, tc.err)
}
} else {
assert.NoError(t, err, "Validate() should not return error")
}
})
}
}
func TestPATValidate(t *testing.T) {
cases := []struct {
desc string
pat *auth.PAT
err bool
}{
{
desc: "Valid PAT",
pat: &auth.PAT{
ID: "pat-id",
User: "user-id",
Name: "test-pat",
Description: "test description",
},
err: false,
},
{
desc: "Invalid nil PAT",
pat: nil,
err: true,
},
{
desc: "Invalid PAT without name",
pat: &auth.PAT{
ID: "pat-id",
User: "user-id",
Name: "",
Description: "test description",
},
err: true,
},
{
desc: "Invalid PAT without user",
pat: &auth.PAT{
ID: "pat-id",
User: "",
Name: "test-pat",
Description: "test description",
},
err: true,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.pat.Validate()
if tc.err {
assert.Error(t, err, "Validate() should return error")
} else {
assert.NoError(t, err, "Validate() should not return error")
}
})
}
}
func TestPATMarshalUnmarshalBinary(t *testing.T) {
pat := auth.PAT{
ID: "pat-id",
User: "user-id",
Name: "test-pat",
Description: "test description",
Secret: "secret",
IssuedAt: time.Now().UTC().Round(time.Second),
ExpiresAt: time.Now().UTC().Add(24 * time.Hour).Round(time.Second),
Status: auth.ActiveStatus,
}
// Marshal
data, err := pat.MarshalBinary()
assert.NoError(t, err, "MarshalBinary() should not return error")
assert.NotNil(t, data, "MarshalBinary() should return data")
// Unmarshal
var newPAT auth.PAT
err = newPAT.UnmarshalBinary(data)
assert.NoError(t, err, "UnmarshalBinary() should not return error")
assert.Equal(t, pat.ID, newPAT.ID, "ID mismatch")
assert.Equal(t, pat.User, newPAT.User, "User mismatch")
assert.Equal(t, pat.Name, newPAT.Name, "Name mismatch")
assert.Equal(t, pat.Description, newPAT.Description, "Description mismatch")
assert.Equal(t, pat.Secret, newPAT.Secret, "Secret mismatch")
assert.Equal(t, pat.Status, newPAT.Status, "Status mismatch")
}
+119 -77
View File
@@ -5,13 +5,14 @@ package auth
import (
"context"
"crypto/rand"
"encoding/base64"
"math/rand"
"strings"
"time"
"github.com/absmach/supermq"
"github.com/absmach/supermq/pkg/errors"
repoerr "github.com/absmach/supermq/pkg/errors/repository"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/absmach/supermq/pkg/policies"
"github.com/google/uuid"
@@ -19,7 +20,6 @@ import (
const (
recoveryDuration = 5 * time.Minute
defLimit = 100
randStr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&&*|+-="
patPrefix = "pat"
patSecretSeparator = "_"
@@ -29,22 +29,25 @@ var (
// ErrExpiry indicates that the token is expired.
ErrExpiry = errors.New("token is expired")
errIssueUser = errors.New("failed to issue new login key")
errIssueTmp = errors.New("failed to issue new temporary key")
errRevoke = errors.New("failed to remove key")
errRetrieve = errors.New("failed to retrieve key data")
errIdentify = errors.New("failed to validate token")
errPlatform = errors.New("invalid platform id")
errRoleAuth = errors.New("failed to authorize user role")
errIssueUser = errors.New("failed to issue new login key")
errIssueTmp = errors.New("failed to issue new temporary key")
errRevoke = errors.New("failed to remove key")
errRetrieve = errors.New("failed to retrieve key data")
errIdentify = errors.New("failed to validate token")
errPlatform = errors.New("invalid platform id")
errRoleAuth = errors.New("failed to authorize user role")
errSaveRefreshKey = errors.NewServiceError("failed to save refresh key")
errRevokeRefreshKey = errors.NewServiceError("failed to revoke refresh key")
errListRefreshKeys = errors.NewServiceError("failed to list refresh keys")
errMalformedPAT = errors.New("malformed personal access token")
errFailedToParseUUID = errors.New("failed to parse string to UUID")
errInvalidLenFor2UUIDs = errors.New("invalid input length for 2 UUID, excepted 32 byte")
errRevokedPAT = errors.New("revoked pat")
errCreatePAT = errors.New("failed to create PAT")
errUpdatePAT = errors.New("failed to update PAT")
errRetrievePAT = errors.New("failed to retrieve PAT")
errDeletePAT = errors.New("failed to delete PAT")
errRevokedPAT = errors.NewServiceError("revoked pat")
errCreatePAT = errors.NewServiceError("failed to create PAT")
errUpdatePAT = errors.NewServiceError("failed to update PAT")
errRetrievePAT = errors.NewServiceError("failed to retrieve PAT")
errDeletePAT = errors.NewServiceError("failed to delete PAT")
errInvalidScope = errors.New("invalid scope")
)
@@ -56,7 +59,7 @@ type Authz interface {
// `object`. Authorize returns a non-nil error if the subject has
// no relation on the object (which simply means the operation is
// denied).
Authorize(ctx context.Context, pr policies.Policy) error
Authorize(ctx context.Context, pr policies.Policy, patAuthz *PATAuthz) error
}
// Authn specifies an API that must be fulfilled by the domain service
@@ -67,6 +70,9 @@ type Authn interface {
// Issue issues a new Key, returning its token value alongside.
Issue(ctx context.Context, token string, key Key) (Token, error)
// RevokeToken revokes the refresh token by its ID.
RevokeToken(ctx context.Context, userID, tokenID string) error
// Revoke removes the Key with the provided id that is
// issued by the user identified by the provided key.
Revoke(ctx context.Context, token, id string) error
@@ -79,6 +85,12 @@ type Authn interface {
// is returned. If token is invalid, or invocation failed for some
// other reason, non-nil error value is returned in response.
Identify(ctx context.Context, token string) (Key, error)
// RetrieveJWKS retrieves public keys to validate issued tokens.
RetrieveJWKS() []PublicKeyInfo
// ListUserRefreshTokens lists all active refresh token sessions for a user.
ListUserRefreshTokens(ctx context.Context, userID string) ([]TokenInfo, error)
}
// Service specifies an API that must be fulfilled by the domain service
@@ -97,6 +109,7 @@ type service struct {
keys KeyRepository
pats PATSRepository
cache Cache
tokensCache UserActiveTokensCache
hasher Hasher
idProvider supermq.IDProvider
evaluator policies.Evaluator
@@ -108,12 +121,13 @@ type service struct {
}
// New instantiates the auth service implementation.
func New(keys KeyRepository, pats PATSRepository, cache Cache, hasher Hasher, idp supermq.IDProvider, tokenizer Tokenizer, policyEvaluator policies.Evaluator, policyService policies.Service, loginDuration, refreshDuration, invitationDuration time.Duration) Service {
func New(keys KeyRepository, pats PATSRepository, cache Cache, tokensCache UserActiveTokensCache, hasher Hasher, idp supermq.IDProvider, tokenizer Tokenizer, policyEvaluator policies.Evaluator, policyService policies.Service, loginDuration, refreshDuration, invitationDuration time.Duration) Service {
return &service{
tokenizer: tokenizer,
keys: keys,
pats: pats,
cache: cache,
tokensCache: tokensCache,
hasher: hasher,
idProvider: idp,
evaluator: policyEvaluator,
@@ -140,8 +154,16 @@ func (svc service) Issue(ctx context.Context, token string, key Key) (Token, err
}
}
func (svc service) RevokeToken(ctx context.Context, userID, tokenID string) error {
if err := svc.tokensCache.RemoveActive(ctx, userID, tokenID); err != nil {
return errors.Wrap(errRevokeRefreshKey, err)
}
return nil
}
func (svc service) Revoke(ctx context.Context, token, id string) error {
issuerID, _, err := svc.authenticate(token)
issuerID, _, err := svc.authenticate(ctx, token)
if err != nil {
return errors.Wrap(errRevoke, err)
}
@@ -152,7 +174,7 @@ func (svc service) Revoke(ctx context.Context, token, id string) error {
}
func (svc service) RetrieveKey(ctx context.Context, token, id string) (Key, error) {
issuerID, _, err := svc.authenticate(token)
issuerID, _, err := svc.authenticate(ctx, token)
if err != nil {
return Key{}, errors.Wrap(errRetrieve, err)
}
@@ -165,7 +187,7 @@ func (svc service) RetrieveKey(ctx context.Context, token, id string) (Key, erro
}
func (svc service) Identify(ctx context.Context, token string) (Key, error) {
key, err := svc.tokenizer.Parse(token)
key, err := svc.tokenizer.Parse(ctx, token)
if errors.Contains(err, ErrExpiry) {
err = svc.keys.Remove(ctx, key.Issuer, key.ID)
return Key{}, errors.Wrap(svcerr.ErrAuthentication, errors.Wrap(ErrKeyExpired, err))
@@ -175,6 +197,12 @@ func (svc service) Identify(ctx context.Context, token string) (Key, error) {
}
switch key.Type {
case PersonalAccessToken:
res, err := svc.IdentifyPAT(ctx, token)
if err != nil {
return Key{}, err
}
return Key{ID: res.ID, Type: PersonalAccessToken, Subject: res.User, Role: res.Role}, nil
case RecoveryKey, AccessKey, InvitationKey, RefreshKey:
return key, nil
case APIKey:
@@ -188,23 +216,33 @@ func (svc service) Identify(ctx context.Context, token string) (Key, error) {
}
}
func (svc service) Authorize(ctx context.Context, pr policies.Policy) error {
func (svc service) RetrieveJWKS() []PublicKeyInfo {
keys, err := svc.tokenizer.RetrieveJWKS()
if err != nil {
return nil
}
return keys
}
func (svc service) ListUserRefreshTokens(ctx context.Context, userID string) ([]TokenInfo, error) {
tokenInfo, err := svc.tokensCache.ListUserTokens(ctx, userID)
if err != nil {
return nil, errors.Wrap(errListRefreshKeys, err)
}
return tokenInfo, nil
}
func (svc service) Authorize(ctx context.Context, pr policies.Policy, patAuthz *PATAuthz) error {
if patAuthz != nil {
if err := svc.AuthorizePAT(ctx, patAuthz.UserID, patAuthz.PatID, patAuthz.EntityType, patAuthz.Domain, patAuthz.Operation, patAuthz.EntityID); err != nil {
return err
}
}
if err := svc.PolicyValidation(pr); err != nil {
return errors.Wrap(svcerr.ErrMalformedEntity, err)
}
if pr.SubjectKind == policies.TokenKind {
key, err := svc.Identify(ctx, pr.Subject)
if err != nil {
return errors.Wrap(svcerr.ErrAuthentication, err)
}
if key.Subject == "" {
if pr.ObjectType == policies.GroupType || pr.ObjectType == policies.ClientType || pr.ObjectType == policies.DomainType {
return svcerr.ErrDomainAuthorization
}
return svcerr.ErrAuthentication
}
pr.Subject = key.Subject
}
if err := svc.checkPolicy(ctx, pr); err != nil {
return err
}
@@ -213,39 +251,12 @@ func (svc service) Authorize(ctx context.Context, pr policies.Policy) error {
}
func (svc service) checkPolicy(ctx context.Context, pr policies.Policy) error {
// Domain status is required for if user sent authorization request on clients, channels, groups and domains
if pr.SubjectType == policies.UserType && (pr.ObjectType == policies.GroupType || pr.ObjectType == policies.ClientType || pr.ObjectType == policies.DomainType) {
domainID := pr.Domain
if domainID == "" {
if pr.ObjectType != policies.DomainType {
return svcerr.ErrDomainAuthorization
}
domainID = pr.Object
}
if err := svc.checkDomain(ctx, pr.SubjectType, pr.Subject, domainID); err != nil {
return err
}
}
if err := svc.evaluator.CheckPolicy(ctx, pr); err != nil {
return errors.Wrap(svcerr.ErrAuthorization, err)
}
return nil
}
func (svc service) checkDomain(ctx context.Context, subjectType, subject, domainID string) error {
if err := svc.evaluator.CheckPolicy(ctx, policies.Policy{
Subject: subject,
SubjectType: subjectType,
Permission: policies.MembershipPermission,
Object: domainID,
ObjectType: policies.DomainType,
}); err != nil {
return svcerr.ErrDomainAuthorization
}
return nil
}
func (svc service) PolicyValidation(pr policies.Policy) error {
if pr.ObjectType == policies.PlatformType && pr.Object != policies.SuperMQObject {
return errPlatform
@@ -282,10 +293,20 @@ func (svc service) accessKey(ctx context.Context, key Key) (Token, error) {
key.ExpiresAt = time.Now().UTC().Add(svc.refreshDuration)
key.Type = RefreshKey
id, err := svc.idProvider.ID()
if err != nil {
return Token{}, errors.Wrap(errIssueTmp, err)
}
key.ID = id
refresh, err := svc.tokenizer.Issue(key)
if err != nil {
return Token{}, errors.Wrap(errIssueTmp, err)
}
if key.Subject != "" && key.ExpiresAt.After(time.Now()) {
if err := svc.tokensCache.SaveActive(ctx, key.Subject, key.ID, key.Description, key.ExpiresAt); err != nil {
return Token{}, errors.Wrap(errSaveRefreshKey, err)
}
}
return Token{AccessToken: access, RefreshToken: refresh}, nil
}
@@ -308,13 +329,20 @@ func (svc service) invitationKey(ctx context.Context, key Key) (Token, error) {
}
func (svc service) refreshKey(ctx context.Context, token string, key Key) (Token, error) {
k, err := svc.tokenizer.Parse(token)
k, err := svc.tokenizer.Parse(ctx, token)
if err != nil {
return Token{}, errors.Wrap(errRetrieve, err)
}
if k.Type != RefreshKey {
return Token{}, errIssueUser
}
ok, err := svc.tokensCache.IsActive(ctx, key.ID)
if err != nil {
return Token{}, errors.Wrap(svcerr.ErrViewEntity, err)
}
if !ok {
return Token{}, ErrRevokedToken
}
key.ID = k.ID
key.Type = AccessKey
key.Subject = k.Subject
@@ -330,7 +358,7 @@ func (svc service) refreshKey(ctx context.Context, token string, key Key) (Token
return Token{}, errors.Wrap(errIssueTmp, err)
}
key.ExpiresAt = time.Now().UTC().Add(svc.refreshDuration)
key.ExpiresAt = k.ExpiresAt
key.Type = RefreshKey
refresh, err := svc.tokenizer.Issue(key)
if err != nil {
@@ -349,7 +377,7 @@ func (svc service) checkUserRole(ctx context.Context, key Key) (err error) {
Permission: policies.AdminPermission,
Object: policies.SuperMQObject,
ObjectType: policies.PlatformType,
}); err != nil {
}, nil); err != nil {
return errRoleAuth
}
return nil
@@ -360,7 +388,7 @@ func (svc service) checkUserRole(ctx context.Context, key Key) (err error) {
Permission: policies.MembershipPermission,
Object: policies.SuperMQObject,
ObjectType: policies.PlatformType,
}); err != nil {
}, nil); err != nil {
return errRoleAuth
}
return nil
@@ -377,7 +405,7 @@ func (svc service) getUserRole(ctx context.Context, userID string) (role Role) {
Permission: policies.AdminPermission,
Object: policies.SuperMQObject,
ObjectType: policies.PlatformType,
}); err == nil {
}, nil); err == nil {
rl = AdminRole
}
@@ -385,7 +413,7 @@ func (svc service) getUserRole(ctx context.Context, userID string) (role Role) {
}
func (svc service) userKey(ctx context.Context, token string, key Key) (Token, error) {
id, sub, err := svc.authenticate(token)
id, sub, err := svc.authenticate(ctx, token)
if err != nil {
return Token{}, errors.Wrap(errIssueUser, err)
}
@@ -416,8 +444,8 @@ func (svc service) userKey(ctx context.Context, token string, key Key) (Token, e
return Token{AccessToken: tkn}, nil
}
func (svc service) authenticate(token string) (string, string, error) {
key, err := svc.tokenizer.Parse(token)
func (svc service) authenticate(ctx context.Context, token string) (string, string, error) {
key, err := svc.tokenizer.Parse(ctx, token)
if err != nil {
return "", "", errors.Wrap(svcerr.ErrAuthentication, err)
}
@@ -702,11 +730,12 @@ func (svc service) IdentifyPAT(ctx context.Context, secret string) (PAT, error)
return PAT{}, errors.Wrap(svcerr.ErrAuthentication, err)
}
role := svc.getUserRole(ctx, userID.String())
return PAT{ID: patID.String(), User: userID.String(), Role: role}, nil
pat := PAT{ID: patID.String(), User: userID.String(), Role: role}
return pat, nil
}
func (svc service) AuthorizePAT(ctx context.Context, userID, patID string, entityType EntityType, optionalDomainID string, operation Operation, entityID string) error {
if err := svc.pats.CheckScope(ctx, userID, patID, entityType, optionalDomainID, operation, entityID); err != nil {
func (svc service) AuthorizePAT(ctx context.Context, userID, patID string, entityType EntityType, domainID string, operation string, entityID string) error {
if err := svc.pats.CheckScope(ctx, userID, patID, entityType, domainID, operation, entityID); err != nil {
return errors.Wrap(svcerr.ErrAuthorization, err)
}
@@ -723,7 +752,12 @@ func (svc service) generateSecretAndHash(userID, patID string) (string, string,
return "", "", errors.Wrap(errFailedToParseUUID, err)
}
secret := patPrefix + patSecretSeparator + encode(uID, pID) + patSecretSeparator + generateRandomString(100)
randomPart, err := generateRandomString(100)
if err != nil {
return "", "", err
}
secret := patPrefix + patSecretSeparator + encode(uID, pID) + patSecretSeparator + randomPart
secretHash, err := svc.hasher.Hash(secret)
return secret, secretHash, err
}
@@ -750,14 +784,19 @@ func decode(encoded string) (uuid.UUID, uuid.UUID, error) {
return userID, patID, nil
}
func generateRandomString(n int) string {
func generateRandomString(n int) (string, error) {
letterRunes := []rune(randStr)
rand.New(rand.NewSource(time.Now().UnixNano()))
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
randBytes := make([]byte, n)
if _, err := rand.Read(randBytes); err != nil {
return "", errors.Wrap(errors.New("failed to generate random string"), err)
}
return string(b)
for i := range b {
b[i] = letterRunes[int(randBytes[i])%len(letterRunes)]
}
return string(b), nil
}
func (svc service) authnAuthzUserPAT(ctx context.Context, token, patID string) (Key, error) {
@@ -768,6 +807,9 @@ func (svc service) authnAuthzUserPAT(ctx context.Context, token, patID string) (
_, err = svc.pats.Retrieve(ctx, key.Subject, patID)
if err != nil {
if errors.Contains(err, repoerr.ErrNotFound) {
return Key{}, svcerr.ErrNotFound
}
return Key{}, errors.Wrap(svcerr.ErrAuthorization, err)
}
+782 -436
View File
File diff suppressed because it is too large Load Diff
+252
View File
@@ -0,0 +1,252 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package auth_test
import (
"testing"
"github.com/absmach/supermq/auth"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/stretchr/testify/assert"
)
func TestStatusString(t *testing.T) {
cases := []struct {
desc string
status auth.Status
expected string
}{
{
desc: "Active",
status: auth.ActiveStatus,
expected: "active",
},
{
desc: "Revoked",
status: auth.RevokedStatus,
expected: "revoked",
},
{
desc: "Expired",
status: auth.ExpiredStatus,
expected: "expired",
},
{
desc: "All",
status: auth.AllStatus,
expected: "all",
},
{
desc: "Unknown",
status: auth.Status(100),
expected: "unknown",
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got := tc.status.String()
assert.Equal(t, tc.expected, got, "String() = %v, expected %v", got, tc.expected)
})
}
}
func TestToStatus(t *testing.T) {
cases := []struct {
desc string
status string
expected auth.Status
err error
}{
{
desc: "Active",
status: "active",
expected: auth.ActiveStatus,
err: nil,
},
{
desc: "Empty string defaults to Active",
status: "",
expected: auth.ActiveStatus,
err: nil,
},
{
desc: "Revoked",
status: "revoked",
expected: auth.RevokedStatus,
err: nil,
},
{
desc: "Expired",
status: "expired",
expected: auth.ExpiredStatus,
err: nil,
},
{
desc: "All",
status: "all",
expected: auth.AllStatus,
err: nil,
},
{
desc: "Unknown",
status: "unknown",
expected: auth.Status(0),
err: svcerr.ErrInvalidStatus,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got, err := auth.ToStatus(tc.status)
assert.Equal(t, tc.err, err, "ToStatus() error = %v, expected %v", err, tc.err)
assert.Equal(t, tc.expected, got, "ToStatus() = %v, expected %v", got, tc.expected)
})
}
}
func TestStatusMarshalJSON(t *testing.T) {
cases := []struct {
desc string
expected []byte
status auth.Status
err error
}{
{
desc: "Active",
expected: []byte(`"active"`),
status: auth.ActiveStatus,
err: nil,
},
{
desc: "Revoked",
expected: []byte(`"revoked"`),
status: auth.RevokedStatus,
err: nil,
},
{
desc: "Expired",
expected: []byte(`"expired"`),
status: auth.ExpiredStatus,
err: nil,
},
{
desc: "All",
expected: []byte(`"all"`),
status: auth.AllStatus,
err: nil,
},
{
desc: "Unknown",
expected: []byte(`"unknown"`),
status: auth.Status(100),
err: nil,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got, err := tc.status.MarshalJSON()
assert.Equal(t, tc.err, err, "MarshalJSON() error = %v, expected %v", err, tc.err)
assert.Equal(t, tc.expected, got, "MarshalJSON() = %v, expected %v", got, tc.expected)
})
}
}
func TestStatusUnmarshalJSON(t *testing.T) {
cases := []struct {
desc string
expected auth.Status
status []byte
err error
}{
{
desc: "Active",
expected: auth.ActiveStatus,
status: []byte(`"active"`),
err: nil,
},
{
desc: "Revoked",
expected: auth.RevokedStatus,
status: []byte(`"revoked"`),
err: nil,
},
{
desc: "Expired",
expected: auth.ExpiredStatus,
status: []byte(`"expired"`),
err: nil,
},
{
desc: "All",
expected: auth.AllStatus,
status: []byte(`"all"`),
err: nil,
},
{
desc: "Unknown",
expected: auth.Status(0),
status: []byte(`"unknown"`),
err: svcerr.ErrInvalidStatus,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
var s auth.Status
err := s.UnmarshalJSON(tc.status)
assert.Equal(t, tc.err, err, "UnmarshalJSON() error = %v, expected %v", err, tc.err)
assert.Equal(t, tc.expected, s, "UnmarshalJSON() = %v, expected %v", s, tc.expected)
})
}
}
func TestPATMarshalJSON(t *testing.T) {
cases := []struct {
desc string
pat auth.PAT
expected string
err error
}{
{
desc: "Active PAT",
pat: auth.PAT{
ID: "test-id",
Name: "test-pat",
Status: auth.ActiveStatus,
},
expected: `"status":"active"`,
err: nil,
},
{
desc: "Revoked PAT",
pat: auth.PAT{
ID: "test-id",
Name: "test-pat",
Status: auth.RevokedStatus,
},
expected: `"status":"revoked"`,
err: nil,
},
{
desc: "Expired PAT",
pat: auth.PAT{
ID: "test-id",
Name: "test-pat",
Status: auth.ExpiredStatus,
},
expected: `"status":"expired"`,
err: nil,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got, err := tc.pat.MarshalJSON()
assert.Equal(t, tc.err, err, "MarshalJSON() error = %v, expected %v", err, tc.err)
assert.Contains(t, string(got), tc.expected, "MarshalJSON() should contain %v", tc.expected)
})
}
}
-13
View File
@@ -1,13 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package auth
// Tokenizer specifies API for encoding and decoding between string and Key.
type Tokenizer interface {
// Issue converts API Key to its string representation.
Issue(key Key) (token string, err error)
// Parse extracts API Key data from string token.
Parse(token string) (key Key, err error)
}
+168
View File
@@ -0,0 +1,168 @@
# Asymmetric Tokenizer
EdDSA (Ed25519) tokenizer with support for zero-downtime key rotation.
## Features
- **Single-key mode** - Simple setup with one active key
- **Two-key mode** - Active + retiring keys for _zero-downtime rotation_
- **JWKS endpoint** - Publishes all valid public keys for token verification
## Configuration
The tokenizer uses environment variables to specify key file paths:
| Environment Variable | Required | Description |
| --------------------------------- | -------- | ------------------------------------------------ |
| `SMQ_AUTH_KEYS_ACTIVE_KEY_PATH` | Yes | Path to active private key file |
| `SMQ_AUTH_KEYS_RETIRING_KEY_PATH` | No | Path to retiring private key file (for rotation) |
Please note that key names are used as **key IDs (kid)**.
### Single-Key Mode
Set only the active key path:
```bash
export SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/private.key"
```
The tokenizer will:
- Issue new tokens signed with the active key
- Verify tokens using the active key
- Return one public key in JWKS endpoint
### Two-Key Mode (Key Rotation)
Set both active and retiring key paths:
```bash
export SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/active.key"
export SMQ_AUTH_KEYS_RETIRING_KEY_PATH="./keys/retiring.key"
```
The tokenizer will:
- Issue new tokens signed with the active key
- Verify tokens using both active and retiring keys
- Return both public keys in JWKS endpoint
## Key Rotation Process
Zero-downtime key rotation in 3 simple steps:
### 1. Generate New Key
```bash
openssl genpkey -algorithm Ed25519 -out keys/new.key
```
### 2. Update Environment & Restart
Move the current active key to retiring position and set the new key as active:
```bash
# Before rotation
SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/current.key"
SMQ_AUTH_KEYS_RETIRING_KEY_PATH="" # No retiring key
# During rotation (both keys active for grace period)
SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/new.key"
SMQ_AUTH_KEYS_RETIRING_KEY_PATH="./keys/current.key"
# After rotation (restart service with new config)
docker-compose restart auth
```
During the grace period, tokens signed with either key remain valid.
### 3. Clean Up After Grace Period
After the grace period expires (typically 7-30 days), remove the retiring key:
```bash
# Remove retiring key configuration
SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/new.key"
SMQ_AUTH_KEYS_RETIRING_KEY_PATH="" # Remove retiring key
# Restart service
docker-compose restart auth
# Delete old key file
rm keys/current.key
```
## Grace Period Recommendations
**Recommended:** 168 hours (7 days)
**Minimum:** 24 hours
**Maximum:** 720 hours (30 days)
The grace period should be longer than your longest-lived access token duration.
## Security Best Practices
- Store private keys with `0600` permissions
- Use cryptographically secure key generation:
```bash
openssl genpkey -algorithm Ed25519 -out private.key
chmod 600 private.key
```
- Rotate keys regularly:
- Standard environments: every 90 days
- High-security environments: every 30 days
- Never commit keys to version control
- Use secrets management in production (HashiCorp Vault, AWS Secrets Manager, etc.)
## Example: Complete Rotation
```bash
# Day 0: Normal operation
export SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/key-2024.pem"
export SMQ_AUTH_KEYS_RETIRING_KEY_PATH=""
# Day 1: Start rotation - generate new key
openssl genpkey -algorithm Ed25519 -out ./keys/key-2025.pem
chmod 600 ./keys/key-2025.pem
# Day 1: Update config and restart
export SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/key-2025.pem"
export SMQ_AUTH_KEYS_RETIRING_KEY_PATH="./keys/key-2024.pem"
docker-compose restart auth
# Day 8: Grace period expired - remove old key
export SMQ_AUTH_KEYS_RETIRING_KEY_PATH=""
docker-compose restart auth
rm ./keys/key-2024.pem
```
## Troubleshooting
### Active key not found
```bash
Error: active key file not found: ./keys/active.key
```
**Solution:** Ensure the file exists and path is correct. Verify `SMQ_AUTH_KEYS_ACTIVE_KEY_PATH` environment variable.
### Retiring key warning
If the retiring key path is set but the file is missing or invalid, the tokenizer logs a warning but continues with only the active key:
```bash
WARN: failed to load retiring key, continuing without it
```
This is by design - a missing retiring key won't prevent startup.
### Invalid key format
```bash
Error: failed to parse private key
```
**Solution:** Ensure you're using Ed25519 keys in PEM format (PKCS8).
+160
View File
@@ -0,0 +1,160 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package asymmetric_test
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/absmach/supermq/auth"
"github.com/absmach/supermq/auth/tokenizer/asymmetric"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type incrementingIDProvider struct {
counter int
}
func (p *incrementingIDProvider) ID() (string, error) {
p.counter++
return fmt.Sprintf("key-id-%d", p.counter), nil
}
func TestTwoKeyRotation(t *testing.T) {
tmpDir := t.TempDir()
_, activePriv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
_, retiringPriv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
activeKeyPath := filepath.Join(tmpDir, "active.key")
retiringKeyPath := filepath.Join(tmpDir, "retiring.key")
saveKey(t, activePriv, activeKeyPath)
saveKey(t, retiringPriv, retiringKeyPath)
idProvider := &incrementingIDProvider{}
tokenizer, err := asymmetric.NewTokenizer(activeKeyPath, retiringKeyPath, idProvider, newTestLogger())
require.NoError(t, err)
testKey := auth.Key{
ID: "test-key",
Type: auth.AccessKey,
Subject: "user-123",
Role: auth.UserRole,
IssuedAt: time.Now().UTC(),
ExpiresAt: time.Now().Add(1 * time.Hour).UTC(),
Verified: true,
}
token, err := tokenizer.Issue(testKey)
require.NoError(t, err)
assert.NotEmpty(t, token)
verified, err := tokenizer.Parse(context.Background(), token)
require.NoError(t, err, "Should work with active token")
assert.Equal(t, testKey.Subject, verified.Subject)
publicKeys, err := tokenizer.RetrieveJWKS()
require.NoError(t, err)
assert.Len(t, publicKeys, 2, "Should return both active and retiring keys")
keyIDs := make(map[string]bool)
for _, pk := range publicKeys {
keyIDs[pk.KeyID] = true
}
assert.Len(t, keyIDs, 2, "Both keys should have unique IDs")
}
func TestSingleKeyMode(t *testing.T) {
tmpDir := t.TempDir()
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
keyPath := filepath.Join(tmpDir, "single.key")
saveKey(t, privateKey, keyPath)
idProvider := &mockIDProvider{id: "single-id"}
tokenizer, err := asymmetric.NewTokenizer(keyPath, "", idProvider, newTestLogger())
require.NoError(t, err)
testKey := auth.Key{
ID: "test",
Type: auth.AccessKey,
Subject: "user",
Role: auth.UserRole,
IssuedAt: time.Now().UTC(),
ExpiresAt: time.Now().Add(1 * time.Hour).UTC(),
}
token, err := tokenizer.Issue(testKey)
require.NoError(t, err)
_, err = tokenizer.Parse(context.Background(), token)
require.NoError(t, err)
publicKeys, err := tokenizer.RetrieveJWKS()
require.NoError(t, err, "Should return one active key")
assert.Len(t, publicKeys, 1, "Should return only the active key")
}
func TestMissingRetiringKey(t *testing.T) {
tmpDir := t.TempDir()
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
activeKeyPath := filepath.Join(tmpDir, "active.key")
saveKey(t, privateKey, activeKeyPath)
retiringKeyPath := filepath.Join(tmpDir, "nonexistent.key")
idProvider := &mockIDProvider{id: "test-id"}
tokenizer, err := asymmetric.NewTokenizer(activeKeyPath, retiringKeyPath, idProvider, newTestLogger())
require.NoError(t, err, "Should succeed even if retiring key is missing")
testKey := auth.Key{
ID: "test",
Type: auth.AccessKey,
Subject: "user",
Role: auth.UserRole,
IssuedAt: time.Now().UTC(),
ExpiresAt: time.Now().Add(1 * time.Hour).UTC(),
}
token, err := tokenizer.Issue(testKey)
require.NoError(t, err)
_, err = tokenizer.Parse(context.Background(), token)
require.NoError(t, err)
publicKeys, err := tokenizer.RetrieveJWKS()
require.NoError(t, err)
assert.Len(t, publicKeys, 1, "Should return only active key when retiring key is missing")
}
func saveKey(t *testing.T, privateKey ed25519.PrivateKey, path string) {
pkcs8Key, err := x509.MarshalPKCS8PrivateKey(privateKey)
require.NoError(t, err)
pemBlock := &pem.Block{
Type: "PRIVATE KEY",
Bytes: pkcs8Key,
}
err = os.WriteFile(path, pem.EncodeToMemory(pemBlock), 0o600)
require.NoError(t, err)
}

Some files were not shown because too many files have changed in this diff Show More