From 17600f25f1458720f09d8583e8f30876026b4947 Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Mon, 24 Feb 2025 12:22:56 +0300 Subject: [PATCH 01/18] chore: update go modules Signed-off-by: Rodney Osodo --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 5dfe6f7f43..503ef1873e 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,11 @@ go 1.23.4 require ( github.com/0x6flab/namegenerator v1.4.0 github.com/absmach/callhome v0.14.0 - github.com/absmach/certs v0.0.0-20250127084046-fb0da0712b2b + github.com/absmach/certs v0.0.0-20250218103329-9fee99849118 github.com/absmach/mgate v0.4.5 github.com/absmach/senml v1.0.6 github.com/authzed/authzed-go v1.3.0 - github.com/authzed/grpcutil v0.0.0-20240123194739-2ea1e3d2d98b + github.com/authzed/grpcutil v0.0.0-20250221190651-1985b19b35b8 github.com/authzed/spicedb v1.40.1 github.com/caarlos0/env/v11 v11.3.1 github.com/cenkalti/backoff/v4 v4.3.0 @@ -51,7 +51,7 @@ require ( golang.org/x/crypto v0.35.0 golang.org/x/oauth2 v0.27.0 golang.org/x/sync v0.11.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 + google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 google.golang.org/grpc v1.70.0 google.golang.org/protobuf v1.36.5 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df @@ -112,7 +112,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect github.com/jzelinskie/stringz v0.0.3 // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.6 // indirect @@ -155,12 +155,12 @@ require ( go.opentelemetry.io/otel/metric v1.34.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect + golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.10.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250207221924-e9438ea467c6 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 89921583e7..7ec5b41ae4 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrd github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/absmach/callhome v0.14.0 h1:zB4tIZJ1YUmZ1VGHFPfMA/Lo6/Mv19y2dvoOiXj2BWs= github.com/absmach/callhome v0.14.0/go.mod h1:l12UJOfibK4Muvg/AbupHuquNV9qSz/ROdTEPg7f2Vk= -github.com/absmach/certs v0.0.0-20250127084046-fb0da0712b2b h1:EGIqL1bARjRSS7kH98Q5O/g7lZN/Q0KtAVX5mxRcq84= -github.com/absmach/certs v0.0.0-20250127084046-fb0da0712b2b/go.mod h1:g6Kqge7RVxwt+LRxqt+09cqa2SgPAwXvIPoyPsEqZlQ= +github.com/absmach/certs v0.0.0-20250218103329-9fee99849118 h1:2LetQURw5XAJG6eTNOltEENkVqalEuvRfQtnC3FDb1A= +github.com/absmach/certs v0.0.0-20250218103329-9fee99849118/go.mod h1:kgB2x591CBWs/pZxt1cWqN9PAKAfdnzfED6FuAVhaF0= github.com/absmach/mgate v0.4.5 h1:l6RmrEsR9jxkdb9WHUSecmT0HA41TkZZQVffFfUAIfI= github.com/absmach/mgate v0.4.5/go.mod h1:IvRIHZexZPEIAPmmaJF0L5DY2ERjj+GxRGitOW4s6qo= github.com/absmach/senml v1.0.6 h1:WPeIl6vQ00k7ghWSZYT/QP0KUxq2+4zQoaC7240pLFk= @@ -31,8 +31,8 @@ github.com/authzed/authzed-go v1.3.0 h1:jKIMpYDy+6WoOwl32HRURxLZxNGm+I7ObUlTntEP github.com/authzed/authzed-go v1.3.0/go.mod h1:MYkXImtFAxrM/bVZvmC/WO+gZC9RLlvpCM51SLaUZb0= github.com/authzed/cel-go v0.20.2 h1:GlmLecGry7Z8HU0k+hmaHHUV05ZHrsFxduXHtIePvck= github.com/authzed/cel-go v0.20.2/go.mod h1:pJHVFWbqUHV1J+klQoZubdKswlbxcsbojda3mye9kiU= -github.com/authzed/grpcutil v0.0.0-20240123194739-2ea1e3d2d98b h1:wbh8IK+aMLTCey9sZasO7b6BWLAJnHHvb79fvWCXwxw= -github.com/authzed/grpcutil v0.0.0-20240123194739-2ea1e3d2d98b/go.mod h1:s3qC7V7XIbiNWERv7Lfljy/Lx25/V1Qlexb0WJuA8uQ= +github.com/authzed/grpcutil v0.0.0-20250221190651-1985b19b35b8 h1:y17oq4U8n+k1OcIGGDsjYdIdp4QywGcE7ZphIvtfEbo= +github.com/authzed/grpcutil v0.0.0-20250221190651-1985b19b35b8/go.mod h1:Pf1ZSi41EePvx1GC1DeEJw5dn35iUcxZHqpHuG1Rpic= github.com/authzed/spicedb v1.40.1 h1:Ka9424FJvnYvfnWzN5aK3q/1xnDEXf5fBAwSHKaPHBc= github.com/authzed/spicedb v1.40.1/go.mod h1:W/BC/b7hM+yJRp8T2W47kPE/L/ymqQ6w54wcuA7aB9M= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -250,8 +250,8 @@ github.com/jzelinskie/stringz v0.0.3 h1:0GhG3lVMYrYtIvRbxvQI6zqRTT1P1xyQlpa0FhfU github.com/jzelinskie/stringz v0.0.3/go.mod h1:hHYbgxJuNLRw91CmpuFsYEOyQqpDVFg8pvEh23vy4P0= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -481,8 +481,8 @@ golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZP golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= -golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -595,10 +595,10 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20250207221924-e9438ea467c6 h1:L9JNMl/plZH9wmzQUHleO/ZZDSN+9Gh41wPczNy+5Fk= -google.golang.org/genproto/googleapis/api v0.0.0-20250207221924-e9438ea467c6/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 h1:35ZFtrCgaAjF7AFAK0+lRSf+4AyYnWRbH7og13p7rZ4= +google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:W9ynFDP/shebLB1Hl/ESTOap2jHd6pmLXPNZC7SVDbA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= From 982941c9109e446c2a271574d2d7d6b74f355f8a Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Mon, 24 Feb 2025 13:06:22 +0300 Subject: [PATCH 02/18] feat(auth-callout): Enable auth to make external requests for different purposes for policy enforcement Signed-off-by: Rodney Osodo --- auth/README.md | 144 +++++++++++++++------------- auth/api/http/keys/endpoint_test.go | 3 +- auth/callback.go | 105 ++++++++++++++++++++ auth/callback_test.go | 129 +++++++++++++++++++++++++ auth/mocks/callback.go | 49 ++++++++++ auth/service.go | 11 ++- auth/service_test.go | 65 ++++++++++--- cmd/auth/main.go | 48 ++++++---- docker/.env | 3 + docker/docker-compose.yml | 3 + 10 files changed, 461 insertions(+), 99 deletions(-) create mode 100644 auth/callback.go create mode 100644 auth/callback_test.go create mode 100644 auth/mocks/callback.go diff --git a/auth/README.md b/auth/README.md index 3f1c23108e..672380e81e 100644 --- a/auth/README.md +++ b/auth/README.md @@ -59,42 +59,45 @@ 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 | | -| SMQ_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 | -| SMQ_SEND_TELEMETRY | Send telemetry to supermq call home server | true | -| SMQ_AUTH_ADAPTER_INSTANCE_ID | Adapter instance ID | "" | +| 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 | | +| SMQ_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 | +| SMQ_SEND_TELEMETRY | Send telemetry to supermq call home server | true | +| SMQ_AUTH_ADAPTER_INSTANCE_ID | Adapter instance ID | "" | +| SMQ_AUTH_CALLOUT_URLS | Comma-separated list of callout URLs | "" | +| SMQ_AUTH_CALLOUT_METHOD | Callout method | POST | +| SMQ_AUTH_CALLOUT_TLS_VERIFICATION | Enable TLS verification for callouts | true | ## Deployment @@ -148,6 +151,9 @@ SMQ_JAEGER_URL=http://localhost:14268/api/traces \ SMQ_JAEGER_TRACE_RATIO=1.0 \ SMQ_SEND_TELEMETRY=true \ SMQ_AUTH_ADAPTER_INSTANCE_ID="" \ +SMQ_AUTH_CALLOUT_URLS="" \ +SMQ_AUTH_CALLOUT_METHOD="POST" \ +SMQ_AUTH_CALLOUT_TLS_VERIFICATION=true \ $GOBIN/supermq-auth ``` @@ -171,11 +177,13 @@ PATs in SuperMQ are designed with the following features: ### Token Structure A PAT consists of three parts separated by underscores: + ``` pat__ ``` Where: + - `pat` is a fixed prefix - `` is a base64-encoded combination of the user ID and PAT ID - `` is a randomly generated string for additional security @@ -184,31 +192,31 @@ Where: SuperMQ supports the following operations for PATs: -| Operation | Description | -|-----------|-------------| -| `create` | Create a new resource | -| `read` | Read/view a resource | -| `list` | List resources | -| `update` | Update/modify a resource | -| `delete` | Delete a resource | -| `share` | Share a resource with others | -| `unshare` | Remove sharing permissions | -| `publish` | Publish messages to a channel | +| Operation | Description | +| ----------- | ------------------------------------ | +| `create` | Create a new resource | +| `read` | Read/view a resource | +| `list` | List resources | +| `update` | Update/modify a resource | +| `delete` | Delete a resource | +| `share` | Share a resource with others | +| `unshare` | Remove sharing permissions | +| `publish` | Publish messages to a channel | | `subscribe` | Subscribe to messages from a channel | ### Entity Types PATs can be scoped to the following entity types: -| Entity Type | Description | -|-------------|-------------| -| `groups` | User groups | -| `channels` | Communication channels | -| `clients` | Client applications | -| `domains` | Organizational domains | -| `users` | User accounts | -| `dashboards` | Dashboard interfaces | -| `messages` | Message content | +| Entity Type | Description | +| ------------ | ---------------------- | +| `groups` | User groups | +| `channels` | Communication channels | +| `clients` | Client applications | +| `domains` | Organizational domains | +| `users` | User accounts | +| `dashboards` | Dashboard interfaces | +| `messages` | Message content | ### API Examples @@ -226,6 +234,7 @@ curl --location 'http://localhost:9001/pats' \ ``` Response: + ```json { "id": "a2500226-95dc-4285-87e2-e693e4a0a976", @@ -333,6 +342,7 @@ This example shows how to create a client in a specific domain (`c16c980a-9d4c-4 When defining scopes for PATs, you can use the wildcard character `*` for the `entity_id` field to grant permissions for all entities of a specific type. This is particularly useful for automation tasks that need to operate on multiple resources. For example: + - `"entity_id": "*"` - Grants permission for all entities of the specified type - `"entity_id": "specific-id"` - Grants permission only for the entity with the specified ID @@ -344,10 +354,10 @@ Using wildcards should be done carefully, as they grant broader permissions. Alw ```json { - "optional_domain_id": "domain_id", - "entity_type": "clients", - "operation": "create", - "entity_id": "*" + "optional_domain_id": "domain_id", + "entity_type": "clients", + "operation": "create", + "entity_id": "*" } ``` @@ -357,10 +367,10 @@ This scope allows the PAT to create any client within the specified domain. The ```json { - "optional_domain_id": "domain_id", - "entity_type": "channels", - "operation": "publish", - "entity_id": "channel_id" + "optional_domain_id": "domain_id", + "entity_type": "channels", + "operation": "publish", + "entity_id": "channel_id" } ``` @@ -370,10 +380,10 @@ This scope restricts the PAT to only publish to a specific channel (`channel_id` ```json { - "optional_domain_id": "domain_id", - "entity_type": "dashboards", - "operation": "read", - "entity_id": "*" + "optional_domain_id": "domain_id", + "entity_type": "dashboards", + "operation": "read", + "entity_id": "*" } ``` diff --git a/auth/api/http/keys/endpoint_test.go b/auth/api/http/keys/endpoint_test.go index 52801c7d40..32830f320b 100644 --- a/auth/api/http/keys/endpoint_test.go +++ b/auth/api/http/keys/endpoint_test.go @@ -76,8 +76,9 @@ func newService() (auth.Service, *mocks.KeyRepository) { pService := new(policymocks.Service) pEvaluator := new(policymocks.Evaluator) t := jwt.New([]byte(secret)) + callback := new(mocks.CallBack) - return auth.New(krepo, pRepo, cache, hash, idProvider, t, pEvaluator, pService, loginDuration, refreshDuration, invalidDuration), krepo + return auth.New(krepo, pRepo,cache, hash, idProvider, t, pEvaluator, pService, loginDuration, refreshDuration, invalidDuration, callback), krepo } func newServer(svc auth.Service) *httptest.Server { diff --git a/auth/callback.go b/auth/callback.go new file mode 100644 index 0000000000..f47806b215 --- /dev/null +++ b/auth/callback.go @@ -0,0 +1,105 @@ +package auth + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/absmach/supermq/pkg/errors" + svcerr "github.com/absmach/supermq/pkg/errors/service" + "github.com/absmach/supermq/pkg/policies" + "golang.org/x/sync/errgroup" +) + +type callback struct { + httpClient *http.Client + urls []string + method string +} + +// CallBack send auth request to an external service +// +//go:generate mockery --name CallBack --output=./mocks --filename callback.go --quiet --note "Copyright (c) Abstract Machines" +type CallBack interface { + Authorize(ctx context.Context, pr policies.Policy) error +} + +// NewCallback creates a new instance of CallBack +func NewCallback(httpClient *http.Client, method string, urls []string) CallBack { + if httpClient == nil { + httpClient = http.DefaultClient + } + + return &callback{ + httpClient: httpClient, + urls: urls, + method: method, + } +} + +func (c *callback) Authorize(ctx context.Context, pr policies.Policy) error { + g, ctx := errgroup.WithContext(ctx) + + payload := map[string]string{ + "domain": pr.Domain, + "subject": pr.Subject, + "subject_type": pr.SubjectType, + "subject_kind": pr.SubjectKind, + "subject_relation": pr.SubjectRelation, + "object": pr.Object, + "object_type": pr.ObjectType, + "object_kind": pr.ObjectKind, + "relation": pr.Relation, + "permission": pr.Permission, + } + for i := range c.urls { + url := c.urls[i] + g.Go(func() error { + return c.makeRequest(ctx, c.method, url, payload) + }) + } + + return g.Wait() +} + +func (c *callback) makeRequest(ctx context.Context, method, urlStr string, params map[string]string) error { + if len(c.urls) == 0 { + return nil + } + + var req *http.Request + var err error + + if method == http.MethodGet { + query := url.Values{} + for key, value := range params { + query.Set(key, value) + } + req, err = http.NewRequestWithContext(ctx, method, urlStr+"?"+query.Encode(), nil) + } else { + data, err := json.Marshal(params) + if err != nil { + return err + } + req, err = http.NewRequestWithContext(ctx, method, urlStr, bytes.NewReader(data)) + } + + if err != nil { + return err + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return errors.Wrap(svcerr.ErrAuthorization, fmt.Errorf("status code %d", resp.StatusCode)) + } + + return nil +} diff --git a/auth/callback_test.go b/auth/callback_test.go new file mode 100644 index 0000000000..a8278eb7ab --- /dev/null +++ b/auth/callback_test.go @@ -0,0 +1,129 @@ +package auth_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/absmach/supermq/auth" + "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" +) + +func TestCallback_Authorize(t *testing.T) { + policy := policies.Policy{ + Domain: "test-domain", + Subject: "test-subject", + SubjectType: "user", + SubjectKind: "individual", + SubjectRelation: "owner", + Object: "test-object", + ObjectType: "message", + ObjectKind: "event", + Relation: "publish", + Permission: "allow", + } + + cases := []struct { + desc string + method string + respStatus int + expectError bool + }{ + { + desc: "successful GET authorization", + method: http.MethodGet, + respStatus: http.StatusOK, + expectError: false, + }, + { + desc: "successful POST authorization", + method: http.MethodPost, + respStatus: http.StatusOK, + expectError: false, + }, + { + desc: "failed authorization", + method: http.MethodPost, + respStatus: http.StatusForbidden, + expectError: true, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, tc.method, r.Method) + + if tc.method == http.MethodGet { + query := r.URL.Query() + assert.Equal(t, policy.Domain, query.Get("domain")) + assert.Equal(t, policy.Subject, query.Get("subject")) + } + + w.WriteHeader(tc.respStatus) + })) + defer ts.Close() + + cb := auth.NewCallback(http.DefaultClient, tc.method, []string{ts.URL}) + err := cb.Authorize(context.Background(), policy) + + if tc.expectError { + assert.Error(t, err) + assert.True(t, errors.Contains(err, svcerr.ErrAuthorization), "expected authorization error") + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestCallback_MultipleURLs(t *testing.T) { + ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer ts1.Close() + + ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer ts2.Close() + + cb := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{ts1.URL, ts2.URL}) + err := cb.Authorize(context.Background(), policies.Policy{}) + assert.NoError(t, err) +} + +func TestCallback_InvalidURL(t *testing.T) { + cb := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{"http://invalid-url"}) + err := cb.Authorize(context.Background(), policies.Policy{}) + assert.Error(t, err) +} + +func TestCallback_CancelledContext(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + cb := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{ts.URL}) + err := cb.Authorize(ctx, policies.Policy{}) + assert.Error(t, err) +} + +func TestNewCallback_NilClient(t *testing.T) { + cb := auth.NewCallback(nil, http.MethodPost, []string{"test"}) + assert.NotNil(t, cb) +} + +func TestCallback_NoURL(t *testing.T) { + cb := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{}) + err := cb.Authorize(context.Background(), policies.Policy{}) + assert.NoError(t, err) +} diff --git a/auth/mocks/callback.go b/auth/mocks/callback.go new file mode 100644 index 0000000000..1c62f01045 --- /dev/null +++ b/auth/mocks/callback.go @@ -0,0 +1,49 @@ +// Code generated by mockery v2.52.3. DO NOT EDIT. + +// Copyright (c) Abstract Machines + +package mocks + +import ( + context "context" + + policies "github.com/absmach/supermq/pkg/policies" + mock "github.com/stretchr/testify/mock" +) + +// CallBack is an autogenerated mock type for the CallBack type +type CallBack struct { + mock.Mock +} + +// Authorize provides a mock function with given fields: ctx, pr +func (_m *CallBack) Authorize(ctx context.Context, pr policies.Policy) error { + ret := _m.Called(ctx, pr) + + if len(ret) == 0 { + panic("no return value specified for Authorize") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, policies.Policy) error); ok { + r0 = rf(ctx, pr) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewCallBack creates a new instance of CallBack. 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 NewCallBack(t interface { + mock.TestingT + Cleanup(func()) +}) *CallBack { + mock := &CallBack{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/auth/service.go b/auth/service.go index a3c28fb183..cc07b3b04b 100644 --- a/auth/service.go +++ b/auth/service.go @@ -108,14 +108,15 @@ type service struct { loginDuration time.Duration refreshDuration time.Duration invitationDuration time.Duration + callback CallBack } // New instantiates the auth service implementation. -func New(keys KeyRepository, repo 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, hasher Hasher, idp supermq.IDProvider, tokenizer Tokenizer, policyEvaluator policies.Evaluator, policyService policies.Service, loginDuration, refreshDuration, invitationDuration time.Duration, callback CallBack) Service { return &service{ tokenizer: tokenizer, keys: keys, - pats: repo, + pats: pats, cache: cache, hasher: hasher, idProvider: idp, @@ -124,6 +125,7 @@ func New(keys KeyRepository, repo PATSRepository, cache Cache, hasher Hasher, id loginDuration: loginDuration, refreshDuration: refreshDuration, invitationDuration: invitationDuration, + callback: callback, } } @@ -212,6 +214,11 @@ func (svc service) Authorize(ctx context.Context, pr policies.Policy) error { if err := svc.checkPolicy(ctx, pr); err != nil { return err } + + if err := svc.callback.Authorize(ctx, pr); err != nil { + return err + } + return nil } diff --git a/auth/service_test.go b/auth/service_test.go index f3c8b0d97b..58fd9df099 100644 --- a/auth/service_test.go +++ b/auth/service_test.go @@ -52,6 +52,7 @@ var ( patsrepo *mocks.PATSRepository cache *mocks.Cache hasher *mocks.Hasher + callback *mocks.CallBack ) func newService() (auth.Service, string) { @@ -62,6 +63,7 @@ func newService() (auth.Service, string) { patsrepo = new(mocks.PATSRepository) hasher = new(mocks.Hasher) idProvider := uuid.NewMock() + callback = new(mocks.CallBack) t := jwt.New([]byte(secret)) key := auth.Key{ @@ -74,7 +76,7 @@ func newService() (auth.Service, string) { } token, _ := t.Issue(key) - return auth.New(krepo, patsrepo, cache, hasher, idProvider, t, pEvaluator, pService, loginDuration, refreshDuration, invalidDuration), token + return auth.New(krepo, patsrepo, cache, hasher, idProvider, t, pEvaluator, pService, loginDuration, refreshDuration, invalidDuration, callback), token } func TestIssue(t *testing.T) { @@ -296,12 +298,14 @@ func TestIssue(t *testing.T) { repoCall := krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, tc.saveErr) repoCall1 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkPolicyRequest).Return(tc.checkPolicyErr) repoCall2 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkPlatformPolicyReq).Return(tc.checkPolicyErr1) - repoCall4 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkDomainPolicyReq).Return(tc.checkPolicyErr) + repoCall3 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkDomainPolicyReq).Return(tc.checkPolicyErr) + repoCall4 := callback.On("Authorize", mock.Anything, mock.Anything).Return(nil) _, err := svc.Issue(context.Background(), tc.token, tc.key) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err)) repoCall.Unset() repoCall1.Unset() repoCall2.Unset() + repoCall3.Unset() repoCall4.Unset() } @@ -609,10 +613,12 @@ func TestIssue(t *testing.T) { for _, tc := range cases4 { repoCall := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkPlatformAdminReq).Return(tc.checkPlatformAdminErr) repoCall1 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkDomainMemberReq).Return(tc.checkDomainMemberErr) + repoCall2 := callback.On("Authorize", mock.Anything, mock.Anything).Return(nil) _, err := svc.Issue(context.Background(), tc.token, tc.key) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err)) repoCall.Unset() repoCall1.Unset() + repoCall2.Unset() } } @@ -744,10 +750,12 @@ func TestIdentify(t *testing.T) { repocall := krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, nil) repocall1 := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(nil) + repoCall2 := callback.On("Authorize", mock.Anything, mock.Anything).Return(nil) loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, User: id, IssuedAt: time.Now(), Domain: groupName}) assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err)) repocall.Unset() repocall1.Unset() + repoCall2.Unset() repocall2 := krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, nil) recoverySecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.RecoveryKey, IssuedAt: time.Now(), Subject: id}) @@ -847,24 +855,28 @@ func TestIdentify(t *testing.T) { func TestAuthorize(t *testing.T) { svc, accessToken := newService() - repocall := krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, nil) - repocall1 := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(nil) + repoCall := krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, nil) + repoCall1 := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(nil) + repoCall2 := callback.On("Authorize", mock.Anything, mock.Anything).Return(nil) loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, User: id, IssuedAt: time.Now(), Domain: groupName}) assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err)) - repocall.Unset() - repocall1.Unset() - saveCall := krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, nil) + repoCall.Unset() + repoCall1.Unset() + repoCall2.Unset() + + repoCall = krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, nil) exp1 := time.Now().Add(-2 * time.Second) expSecret, err := svc.Issue(context.Background(), loginSecret.AccessToken, auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), ExpiresAt: exp1}) assert.Nil(t, err, fmt.Sprintf("Issuing expired login key expected to succeed: %s", err)) - saveCall.Unset() + repoCall.Unset() - repocall2 := krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, nil) - repocall3 := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(nil) + repoCall = krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, nil) + repoCall1 = pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(nil) + repoCall2 = callback.On("Authorize", mock.Anything, mock.Anything).Return(nil) emptySubject, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, User: "", IssuedAt: time.Now(), Domain: groupName}) assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err)) - repocall2.Unset() - repocall3.Unset() + repoCall.Unset() + repoCall1.Unset() te := jwt.New([]byte(secret)) key := auth.Key{ @@ -881,6 +893,7 @@ func TestAuthorize(t *testing.T) { policyReq policies.Policy checkDomainPolicyReq policies.Policy checkPolicyReq policies.Policy + callBackErr error checkPolicyErr error checkDomainPolicyErr error err error @@ -1110,16 +1123,44 @@ func TestAuthorize(t *testing.T) { }, err: svcerr.ErrDomainAuthorization, }, + { + desc: "failed to authorize a user via callback", + policyReq: policies.Policy{ + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: policies.SuperMQObject, + ObjectType: policies.PlatformType, + Permission: policies.AdminPermission, + }, + checkPolicyReq: policies.Policy{ + SubjectType: policies.UserType, + SubjectKind: policies.UsersKind, + Object: policies.SuperMQObject, + ObjectType: policies.PlatformType, + Permission: policies.AdminPermission, + }, + checkDomainPolicyReq: policies.Policy{ + Subject: id, + SubjectType: policies.UserType, + Object: validID, + ObjectType: policies.DomainType, + Permission: policies.MembershipPermission, + }, + callBackErr: svcerr.ErrAuthorization, + err: svcerr.ErrAuthorization, + }, } for _, tc := range cases { policyCall := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkPolicyReq).Return(tc.checkPolicyErr) policyCall1 := pEvaluator.On("CheckPolicy", mock.Anything, tc.checkDomainPolicyReq).Return(tc.checkDomainPolicyErr) repoCall := krepo.On("Remove", mock.Anything, mock.Anything, mock.Anything).Return(nil) + callbackCall := callback.On("Authorize", mock.Anything, tc.checkPolicyReq).Return(tc.callBackErr) err := svc.Authorize(context.Background(), tc.policyReq) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err)) policyCall.Unset() policyCall1.Unset() repoCall.Unset() + callbackCall.Unset() } } diff --git a/cmd/auth/main.go b/cmd/auth/main.go index 130056061e..3ee38cf52b 100644 --- a/cmd/auth/main.go +++ b/cmd/auth/main.go @@ -5,9 +5,11 @@ package main import ( "context" + "crypto/tls" "fmt" "log" "log/slog" + "net/http" "net/url" "os" "time" @@ -60,22 +62,25 @@ const ( ) type config struct { - LogLevel string `env:"SMQ_AUTH_LOG_LEVEL" envDefault:"info"` - SecretKey string `env:"SMQ_AUTH_SECRET_KEY" envDefault:"secret"` - JaegerURL url.URL `env:"SMQ_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"` - SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"` - InstanceID string `env:"SMQ_AUTH_ADAPTER_INSTANCE_ID" envDefault:""` - AccessDuration time.Duration `env:"SMQ_AUTH_ACCESS_TOKEN_DURATION" envDefault:"1h"` - RefreshDuration time.Duration `env:"SMQ_AUTH_REFRESH_TOKEN_DURATION" envDefault:"24h"` - InvitationDuration time.Duration `env:"SMQ_AUTH_INVITATION_DURATION" envDefault:"168h"` - SpicedbHost string `env:"SMQ_SPICEDB_HOST" envDefault:"localhost"` - SpicedbPort string `env:"SMQ_SPICEDB_PORT" envDefault:"50051"` - SpicedbSchemaFile string `env:"SMQ_SPICEDB_SCHEMA_FILE" envDefault:"./docker/spicedb/schema.zed"` - SpicedbPreSharedKey string `env:"SMQ_SPICEDB_PRE_SHARED_KEY" envDefault:"12345678"` - TraceRatio float64 `env:"SMQ_JAEGER_TRACE_RATIO" envDefault:"1.0"` - ESURL string `env:"SMQ_ES_URL" envDefault:"nats://localhost:4222"` - CacheURL string `env:"SMQ_AUTH_CACHE_URL" envDefault:"redis://localhost:6379/0"` - CacheKeyDuration time.Duration `env:"SMQ_AUTH_CACHE_KEY_DURATION" envDefault:"10m"` + LogLevel string `env:"SMQ_AUTH_LOG_LEVEL" envDefault:"info"` + SecretKey string `env:"SMQ_AUTH_SECRET_KEY" envDefault:"secret"` + JaegerURL url.URL `env:"SMQ_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"` + SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"` + InstanceID string `env:"SMQ_AUTH_ADAPTER_INSTANCE_ID" envDefault:""` + AccessDuration time.Duration `env:"SMQ_AUTH_ACCESS_TOKEN_DURATION" envDefault:"1h"` + RefreshDuration time.Duration `env:"SMQ_AUTH_REFRESH_TOKEN_DURATION" envDefault:"24h"` + InvitationDuration time.Duration `env:"SMQ_AUTH_INVITATION_DURATION" envDefault:"168h"` + SpicedbHost string `env:"SMQ_SPICEDB_HOST" envDefault:"localhost"` + SpicedbPort string `env:"SMQ_SPICEDB_PORT" envDefault:"50051"` + SpicedbSchemaFile string `env:"SMQ_SPICEDB_SCHEMA_FILE" envDefault:"./docker/spicedb/schema.zed"` + SpicedbPreSharedKey string `env:"SMQ_SPICEDB_PRE_SHARED_KEY" envDefault:"12345678"` + TraceRatio float64 `env:"SMQ_JAEGER_TRACE_RATIO" envDefault:"1.0"` + ESURL string `env:"SMQ_ES_URL" envDefault:"nats://localhost:4222"` + CacheURL string `env:"SMQ_AUTH_CACHE_URL" envDefault:"redis://localhost:6379/0"` + CacheKeyDuration time.Duration `env:"SMQ_AUTH_CACHE_KEY_DURATION" envDefault:"10m"` + AuthCalloutURLs []string `env:"SMQ_AUTH_CALLOUT_URLS" envDefault:"" envSeparator:","` + AuthCalloutMethod string `env:"SMQ_AUTH_CALLOUT_METHOD" envDefault:"POST"` + AuthCalloutTLSVerification bool `env:"SMQ_AUTH_CALLOUT_TLS_VERIFICATION" envDefault:"true"` } func main() { @@ -239,7 +244,16 @@ func newService(_ context.Context, db *sqlx.DB, tracer trace.Tracer, cfg config, t := jwt.New([]byte(cfg.SecretKey)) - svc := auth.New(keysRepo, patsRepo, nil, hasher, idProvider, t, pEvaluator, pService, cfg.AccessDuration, cfg.RefreshDuration, cfg.InvitationDuration) + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: !cfg.AuthCalloutTLSVerification, + }, + }, + } + callback := auth.NewCallback(httpClient, cfg.AuthCalloutMethod, cfg.AuthCalloutURLs) + + svc := auth.New(keysRepo, patsRepo, nil, hasher, idProvider, t, pEvaluator, pService, cfg.AccessDuration, cfg.RefreshDuration, cfg.InvitationDuration, callback) svc = api.LoggingMiddleware(svc, logger) counter, latency := prometheus.MakeMetrics("auth", "api") svc = api.MetricsMiddleware(svc, counter, latency) diff --git a/docker/.env b/docker/.env index d2029ab890..2d1cb8a5bc 100644 --- a/docker/.env +++ b/docker/.env @@ -101,6 +101,9 @@ SMQ_AUTH_INVITATION_DURATION="168h" SMQ_AUTH_ADAPTER_INSTANCE_ID= SMQ_AUTH_CACHE_URL=redis://auth-redis:${SMQ_REDIS_TCP_PORT}/0 SMQ_AUTH_CACHE_KEY_DURATION=10m +SMQ_AUTH_CALLOUT_URLS="" +SMQ_AUTH_CALLOUT_METHOD="POST" +SMQ_AUTH_CALLOUT_TLS_VERIFICATION="false" #### Auth Client Config SMQ_AUTH_URL=auth:9001 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index ef45982a41..a67d23d17b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -142,6 +142,9 @@ services: SMQ_ES_URL: ${SMQ_ES_URL} SMQ_AUTH_CACHE_URL: ${SMQ_AUTH_CACHE_URL} SMQ_AUTH_CACHE_KEY_DURATION: ${SMQ_AUTH_CACHE_KEY_DURATION} + SMQ_AUTH_CALLOUT_URLS: ${SMQ_AUTH_CALLOUT_URLS} + SMQ_AUTH_CALLOUT_METHOD: ${SMQ_AUTH_CALLOUT_METHOD} + SMQ_AUTH_CALLOUT_TLS_VERIFICATION: ${SMQ_AUTH_CALLOUT_TLS_VERIFICATION} ports: - ${SMQ_AUTH_HTTP_PORT}:${SMQ_AUTH_HTTP_PORT} - ${SMQ_AUTH_GRPC_PORT}:${SMQ_AUTH_GRPC_PORT} From f48bd47f05501e6fefdb87762b4a0867d105b4c4 Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Mon, 24 Feb 2025 13:30:15 +0300 Subject: [PATCH 03/18] fix: add content type to header Signed-off-by: Rodney Osodo --- auth/callback.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/auth/callback.go b/auth/callback.go index f47806b215..7fc22a5190 100644 --- a/auth/callback.go +++ b/auth/callback.go @@ -79,12 +79,14 @@ func (c *callback) makeRequest(ctx context.Context, method, urlStr string, param query.Set(key, value) } req, err = http.NewRequestWithContext(ctx, method, urlStr+"?"+query.Encode(), nil) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } else { data, err := json.Marshal(params) if err != nil { return err } req, err = http.NewRequestWithContext(ctx, method, urlStr, bytes.NewReader(data)) + req.Header.Set("Content-Type", "application/json") } if err != nil { From 71b61b4bb47fd7ad5abb6bfef0c955cbbb54546b Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Mon, 24 Feb 2025 13:31:06 +0300 Subject: [PATCH 04/18] fix: don't use go routines if there is one URL Signed-off-by: Rodney Osodo --- auth/callback.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/auth/callback.go b/auth/callback.go index 7fc22a5190..43c95b8aa0 100644 --- a/auth/callback.go +++ b/auth/callback.go @@ -41,8 +41,6 @@ func NewCallback(httpClient *http.Client, method string, urls []string) CallBack } func (c *callback) Authorize(ctx context.Context, pr policies.Policy) error { - g, ctx := errgroup.WithContext(ctx) - payload := map[string]string{ "domain": pr.Domain, "subject": pr.Subject, @@ -55,6 +53,16 @@ func (c *callback) Authorize(ctx context.Context, pr policies.Policy) error { "relation": pr.Relation, "permission": pr.Permission, } + + if len(c.urls) == 0 { + return nil + } + + if len(c.urls) == 1 { + return c.makeRequest(ctx, c.method, c.urls[0], payload) + } + + g, ctx := errgroup.WithContext(ctx) for i := range c.urls { url := c.urls[i] g.Go(func() error { @@ -66,10 +74,6 @@ func (c *callback) Authorize(ctx context.Context, pr policies.Policy) error { } func (c *callback) makeRequest(ctx context.Context, method, urlStr string, params map[string]string) error { - if len(c.urls) == 0 { - return nil - } - var req *http.Request var err error From b39ec620a4e8ed485b9623be035b646ffe815b80 Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Mon, 24 Feb 2025 13:46:42 +0300 Subject: [PATCH 05/18] style: add copyright license Signed-off-by: Rodney Osodo --- auth/callback.go | 3 +++ auth/callback_test.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/auth/callback.go b/auth/callback.go index 43c95b8aa0..52f36ebbf5 100644 --- a/auth/callback.go +++ b/auth/callback.go @@ -1,3 +1,6 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + package auth import ( diff --git a/auth/callback_test.go b/auth/callback_test.go index a8278eb7ab..5a3a46bc0a 100644 --- a/auth/callback_test.go +++ b/auth/callback_test.go @@ -1,3 +1,6 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + package auth_test import ( From 44cffec17c99a4799a7b7d2e310b05fb1f9699f1 Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Mon, 24 Feb 2025 13:47:57 +0300 Subject: [PATCH 06/18] style: fix ineffectual assignment to err and add period at the end Signed-off-by: Rodney Osodo --- auth/callback.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/auth/callback.go b/auth/callback.go index 52f36ebbf5..d15d6be734 100644 --- a/auth/callback.go +++ b/auth/callback.go @@ -23,14 +23,14 @@ type callback struct { method string } -// CallBack send auth request to an external service +// CallBack send auth request to an external service. // //go:generate mockery --name CallBack --output=./mocks --filename callback.go --quiet --note "Copyright (c) Abstract Machines" type CallBack interface { Authorize(ctx context.Context, pr policies.Policy) error } -// NewCallback creates a new instance of CallBack +// NewCallback creates a new instance of CallBack. func NewCallback(httpClient *http.Client, method string, urls []string) CallBack { if httpClient == nil { httpClient = http.DefaultClient @@ -88,9 +88,9 @@ func (c *callback) makeRequest(ctx context.Context, method, urlStr string, param req, err = http.NewRequestWithContext(ctx, method, urlStr+"?"+query.Encode(), nil) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } else { - data, err := json.Marshal(params) - if err != nil { - return err + data, jsonErr := json.Marshal(params) + if jsonErr != nil { + return jsonErr } req, err = http.NewRequestWithContext(ctx, method, urlStr, bytes.NewReader(data)) req.Header.Set("Content-Type", "application/json") From 7f5184127eaf6e808dade8eacaf43dc6f525aea7 Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Mon, 24 Feb 2025 14:21:08 +0300 Subject: [PATCH 07/18] fix: remove content type from GET request Signed-off-by: Rodney Osodo --- auth/callback.go | 1 - 1 file changed, 1 deletion(-) diff --git a/auth/callback.go b/auth/callback.go index d15d6be734..7c927f848b 100644 --- a/auth/callback.go +++ b/auth/callback.go @@ -86,7 +86,6 @@ func (c *callback) makeRequest(ctx context.Context, method, urlStr string, param query.Set(key, value) } req, err = http.NewRequestWithContext(ctx, method, urlStr+"?"+query.Encode(), nil) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } else { data, jsonErr := json.Marshal(params) if jsonErr != nil { From d8623e6f810b2c9e7934caa9d1ea467387bfaba1 Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Mon, 24 Feb 2025 16:10:57 +0300 Subject: [PATCH 08/18] fix(auth-callout): only support GET and POST http methods Signed-off-by: Rodney Osodo --- auth/callback.go | 12 ++++++++---- auth/callback_test.go | 33 ++++++++++++++++++++++----------- cmd/auth/main.go | 18 +++++++++++++----- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/auth/callback.go b/auth/callback.go index 7c927f848b..697decebda 100644 --- a/auth/callback.go +++ b/auth/callback.go @@ -31,16 +31,19 @@ type CallBack interface { } // NewCallback creates a new instance of CallBack. -func NewCallback(httpClient *http.Client, method string, urls []string) CallBack { +func NewCallback(httpClient *http.Client, method string, urls []string) (CallBack, error) { if httpClient == nil { httpClient = http.DefaultClient } + if method != http.MethodPost && method != http.MethodGet { + return nil, fmt.Errorf("unsupported auth callback method: %s", method) + } return &callback{ httpClient: httpClient, urls: urls, method: method, - } + }, nil } func (c *callback) Authorize(ctx context.Context, pr policies.Policy) error { @@ -80,13 +83,14 @@ func (c *callback) makeRequest(ctx context.Context, method, urlStr string, param var req *http.Request var err error - if method == http.MethodGet { + switch method { + case http.MethodGet: query := url.Values{} for key, value := range params { query.Set(key, value) } req, err = http.NewRequestWithContext(ctx, method, urlStr+"?"+query.Encode(), nil) - } else { + case http.MethodPost: data, jsonErr := json.Marshal(params) if jsonErr != nil { return jsonErr diff --git a/auth/callback_test.go b/auth/callback_test.go index 5a3a46bc0a..6a27dd7732 100644 --- a/auth/callback_test.go +++ b/auth/callback_test.go @@ -71,8 +71,9 @@ func TestCallback_Authorize(t *testing.T) { })) defer ts.Close() - cb := auth.NewCallback(http.DefaultClient, tc.method, []string{ts.URL}) - err := cb.Authorize(context.Background(), policy) + cb, err := auth.NewCallback(http.DefaultClient, tc.method, []string{ts.URL}) + assert.NoError(t, err) + err = cb.Authorize(context.Background(), policy) if tc.expectError { assert.Error(t, err) @@ -95,14 +96,21 @@ func TestCallback_MultipleURLs(t *testing.T) { })) defer ts2.Close() - cb := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{ts1.URL, ts2.URL}) - err := cb.Authorize(context.Background(), policies.Policy{}) + cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{ts1.URL, ts2.URL}) + assert.NoError(t, err) + err = cb.Authorize(context.Background(), policies.Policy{}) assert.NoError(t, err) } func TestCallback_InvalidURL(t *testing.T) { - cb := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{"http://invalid-url"}) - err := cb.Authorize(context.Background(), policies.Policy{}) + cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{"http://invalid-url"}) + assert.NoError(t, err) + err = cb.Authorize(context.Background(), policies.Policy{}) + assert.Error(t, err) +} + +func TestCallback_InvalidMethod(t *testing.T) { + _, err := auth.NewCallback(http.DefaultClient, "invalid-method", []string{"http://example.com"}) assert.Error(t, err) } @@ -115,18 +123,21 @@ func TestCallback_CancelledContext(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() - cb := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{ts.URL}) - err := cb.Authorize(ctx, policies.Policy{}) + cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{ts.URL}) + assert.NoError(t, err) + err = cb.Authorize(ctx, policies.Policy{}) assert.Error(t, err) } func TestNewCallback_NilClient(t *testing.T) { - cb := auth.NewCallback(nil, http.MethodPost, []string{"test"}) + cb, err := auth.NewCallback(nil, http.MethodPost, []string{"test"}) + assert.NoError(t, err) assert.NotNil(t, cb) } func TestCallback_NoURL(t *testing.T) { - cb := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{}) - err := cb.Authorize(context.Background(), policies.Policy{}) + cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{}) + assert.NoError(t, err) + err = cb.Authorize(context.Background(), policies.Policy{}) assert.NoError(t, err) } diff --git a/cmd/auth/main.go b/cmd/auth/main.go index 3ee38cf52b..a5fb4fa8bd 100644 --- a/cmd/auth/main.go +++ b/cmd/auth/main.go @@ -44,7 +44,7 @@ import ( "github.com/caarlos0/env/v11" "github.com/jmoiron/sqlx" "github.com/redis/go-redis/v9" - "go.opentelemetry.io/otel/trace" + "go.opencensus.io/trace" "golang.org/x/sync/errgroup" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -150,7 +150,12 @@ func main() { return } - svc := newService(ctx, db, tracer, cfg, dbConfig, logger, spicedbclient, cacheclient, cfg.CacheKeyDuration) + svc, err := newService(ctx, db, tracer, cfg, dbConfig, logger, spicedbclient, cacheclient, cfg.CacheKeyDuration) + if err != nil { + logger.Error(fmt.Sprintf("failed to create service : %s\n", err.Error())) + exitCode = 1 + return + } grpcServerConfig := server.Config{Port: defSvcGRPCPort} if err := env.ParseWithOptions(&grpcServerConfig, env.Options{Prefix: envPrefixGrpc}); err != nil { @@ -230,7 +235,7 @@ func initSchema(ctx context.Context, client *authzed.ClientWithExperimental, sch return nil } -func newService(_ context.Context, db *sqlx.DB, tracer trace.Tracer, cfg config, dbConfig pgclient.Config, logger *slog.Logger, spicedbClient *authzed.ClientWithExperimental, cacheClient *redis.Client, keyDuration time.Duration) auth.Service { +func newService(_ context.Context, db *sqlx.DB, tracer trace.Tracer, cfg config, dbConfig pgclient.Config, logger *slog.Logger, spicedbClient *authzed.ClientWithExperimental, cacheClient *redis.Client, keyDuration time.Duration) (auth.Service, error) { cache := cache.NewPatsCache(cacheClient, keyDuration) database := pgclient.NewDatabase(db, dbConfig, tracer) @@ -251,7 +256,10 @@ func newService(_ context.Context, db *sqlx.DB, tracer trace.Tracer, cfg config, }, }, } - callback := auth.NewCallback(httpClient, cfg.AuthCalloutMethod, cfg.AuthCalloutURLs) + callback, err := auth.NewCallback(httpClient, cfg.AuthCalloutMethod, cfg.AuthCalloutURLs) + if err != nil { + return nil, err + } svc := auth.New(keysRepo, patsRepo, nil, hasher, idProvider, t, pEvaluator, pService, cfg.AccessDuration, cfg.RefreshDuration, cfg.InvitationDuration, callback) svc = api.LoggingMiddleware(svc, logger) @@ -259,5 +267,5 @@ func newService(_ context.Context, db *sqlx.DB, tracer trace.Tracer, cfg config, svc = api.MetricsMiddleware(svc, counter, latency) svc = tracing.New(svc, tracer) - return svc + return svc, nil } From 0813030b93ae889f053e306b38d38d61a53479ef Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Mon, 24 Feb 2025 16:24:06 +0300 Subject: [PATCH 09/18] fix(auth-callout): use one url and others as fallabcks Signed-off-by: Rodney Osodo --- auth/callback.go | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/auth/callback.go b/auth/callback.go index 697decebda..07d70ea80b 100644 --- a/auth/callback.go +++ b/auth/callback.go @@ -14,7 +14,6 @@ import ( "github.com/absmach/supermq/pkg/errors" svcerr "github.com/absmach/supermq/pkg/errors/service" "github.com/absmach/supermq/pkg/policies" - "golang.org/x/sync/errgroup" ) type callback struct { @@ -47,6 +46,10 @@ func NewCallback(httpClient *http.Client, method string, urls []string) (CallBac } func (c *callback) Authorize(ctx context.Context, pr policies.Policy) error { + if len(c.urls) == 0 { + return nil + } + payload := map[string]string{ "domain": pr.Domain, "subject": pr.Subject, @@ -60,23 +63,14 @@ func (c *callback) Authorize(ctx context.Context, pr policies.Policy) error { "permission": pr.Permission, } - if len(c.urls) == 0 { - return nil - } - - if len(c.urls) == 1 { - return c.makeRequest(ctx, c.method, c.urls[0], payload) - } - - g, ctx := errgroup.WithContext(ctx) + var err error for i := range c.urls { - url := c.urls[i] - g.Go(func() error { - return c.makeRequest(ctx, c.method, url, payload) - }) + if err = c.makeRequest(ctx, c.method, c.urls[i], payload); err == nil { + return nil + } } - return g.Wait() + return err } func (c *callback) makeRequest(ctx context.Context, method, urlStr string, params map[string]string) error { From f2bdfd394c7f1f14a07f6afc2f16904e66b64ee7 Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Mon, 24 Feb 2025 16:26:29 +0300 Subject: [PATCH 10/18] fix(auth-callout): use sdk error Signed-off-by: Rodney Osodo --- auth/callback.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/callback.go b/auth/callback.go index 07d70ea80b..c73abe81c1 100644 --- a/auth/callback.go +++ b/auth/callback.go @@ -104,7 +104,7 @@ func (c *callback) makeRequest(ctx context.Context, method, urlStr string, param defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return errors.Wrap(svcerr.ErrAuthorization, fmt.Errorf("status code %d", resp.StatusCode)) + return errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, resp.StatusCode) } return nil From f663082f4b88c9aebbfbce17efbc27dfc8529480 Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Tue, 25 Feb 2025 17:35:58 +0300 Subject: [PATCH 11/18] fix(auth-callout): not pass method as function paramter Signed-off-by: Rodney Osodo --- auth/callback.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/auth/callback.go b/auth/callback.go index c73abe81c1..048d5157b0 100644 --- a/auth/callback.go +++ b/auth/callback.go @@ -65,7 +65,7 @@ func (c *callback) Authorize(ctx context.Context, pr policies.Policy) error { var err error for i := range c.urls { - if err = c.makeRequest(ctx, c.method, c.urls[i], payload); err == nil { + if err = c.makeRequest(ctx, c.urls[i], payload); err == nil { return nil } } @@ -73,23 +73,23 @@ func (c *callback) Authorize(ctx context.Context, pr policies.Policy) error { return err } -func (c *callback) makeRequest(ctx context.Context, method, urlStr string, params map[string]string) error { +func (c *callback) makeRequest(ctx context.Context, urlStr string, params map[string]string) error { var req *http.Request var err error - switch method { + switch c.method { case http.MethodGet: query := url.Values{} for key, value := range params { query.Set(key, value) } - req, err = http.NewRequestWithContext(ctx, method, urlStr+"?"+query.Encode(), nil) + req, err = http.NewRequestWithContext(ctx, c.method, urlStr+"?"+query.Encode(), nil) case http.MethodPost: data, jsonErr := json.Marshal(params) if jsonErr != nil { return jsonErr } - req, err = http.NewRequestWithContext(ctx, method, urlStr, bytes.NewReader(data)) + req, err = http.NewRequestWithContext(ctx, c.method, urlStr, bytes.NewReader(data)) req.Header.Set("Content-Type", "application/json") } From 8cf805a38a4312a9f4d5787dea3058506659347b Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Wed, 26 Feb 2025 12:50:51 +0300 Subject: [PATCH 12/18] chore: update go modules Signed-off-by: Rodney Osodo --- cmd/auth/main.go | 6 +++--- go.mod | 4 ++-- go.sum | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/auth/main.go b/cmd/auth/main.go index a5fb4fa8bd..9e2162a591 100644 --- a/cmd/auth/main.go +++ b/cmd/auth/main.go @@ -44,7 +44,7 @@ import ( "github.com/caarlos0/env/v11" "github.com/jmoiron/sqlx" "github.com/redis/go-redis/v9" - "go.opencensus.io/trace" + "go.opentelemetry.io/otel/trace" "golang.org/x/sync/errgroup" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -150,7 +150,7 @@ func main() { return } - svc, err := newService(ctx, db, tracer, cfg, dbConfig, logger, spicedbclient, cacheclient, cfg.CacheKeyDuration) + svc, err := newService(db, tracer, cfg, dbConfig, logger, spicedbclient, cacheclient, cfg.CacheKeyDuration) if err != nil { logger.Error(fmt.Sprintf("failed to create service : %s\n", err.Error())) exitCode = 1 @@ -235,7 +235,7 @@ func initSchema(ctx context.Context, client *authzed.ClientWithExperimental, sch return nil } -func newService(_ context.Context, db *sqlx.DB, tracer trace.Tracer, cfg config, dbConfig pgclient.Config, logger *slog.Logger, spicedbClient *authzed.ClientWithExperimental, cacheClient *redis.Client, keyDuration time.Duration) (auth.Service, error) { +func newService(db *sqlx.DB, tracer trace.Tracer, cfg config, dbConfig pgclient.Config, logger *slog.Logger, spicedbClient *authzed.ClientWithExperimental, cacheClient *redis.Client, keyDuration time.Duration) (auth.Service, error) { cache := cache.NewPatsCache(cacheClient, keyDuration) database := pgclient.NewDatabase(db, dbConfig, tracer) diff --git a/go.mod b/go.mod index 503ef1873e..d4d14f48e9 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( golang.org/x/crypto v0.35.0 golang.org/x/oauth2 v0.27.0 golang.org/x/sync v0.11.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 + google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99 google.golang.org/grpc v1.70.0 google.golang.org/protobuf v1.36.5 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df @@ -160,7 +160,7 @@ require ( golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.10.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250224174004-546df14abb99 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 7ec5b41ae4..15232ca27e 100644 --- a/go.sum +++ b/go.sum @@ -595,10 +595,10 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 h1:35ZFtrCgaAjF7AFAK0+lRSf+4AyYnWRbH7og13p7rZ4= -google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:W9ynFDP/shebLB1Hl/ESTOap2jHd6pmLXPNZC7SVDbA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/genproto/googleapis/api v0.0.0-20250224174004-546df14abb99 h1:ilJhrCga0AptpJZXmUYG4MCrx/zf3l1okuYz7YK9PPw= +google.golang.org/genproto/googleapis/api v0.0.0-20250224174004-546df14abb99/go.mod h1:Xsh8gBVxGCcbV8ZeTB9wI5XPyZ5RvC6V3CTeeplHbiA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99 h1:ZSlhAUqC4r8TPzqLXQ0m3upBNZeF+Y8jQ3c4CR3Ujms= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= From 425f408158e29585b23b8adf510515a47bb6a43e Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Fri, 28 Feb 2025 10:27:09 +0300 Subject: [PATCH 13/18] feat(auth-callout): Add request timeout Signed-off-by: Rodney Osodo --- auth/README.md | 1 + cmd/auth/main.go | 34 ++++++++++++++++++---------------- docker/.env | 1 + docker/docker-compose.yml | 1 + 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/auth/README.md b/auth/README.md index 672380e81e..dc768af86c 100644 --- a/auth/README.md +++ b/auth/README.md @@ -98,6 +98,7 @@ The service is configured using the environment variables presented in the follo | SMQ_AUTH_CALLOUT_URLS | Comma-separated list of callout URLs | "" | | SMQ_AUTH_CALLOUT_METHOD | Callout method | POST | | SMQ_AUTH_CALLOUT_TLS_VERIFICATION | Enable TLS verification for callouts | true | +| SMQ_AUTH_CALLOUT_TIMEOUT | Callout timeout | 10s | ## Deployment diff --git a/cmd/auth/main.go b/cmd/auth/main.go index 9e2162a591..80d6dad8fa 100644 --- a/cmd/auth/main.go +++ b/cmd/auth/main.go @@ -62,25 +62,26 @@ const ( ) type config struct { - LogLevel string `env:"SMQ_AUTH_LOG_LEVEL" envDefault:"info"` - SecretKey string `env:"SMQ_AUTH_SECRET_KEY" envDefault:"secret"` - JaegerURL url.URL `env:"SMQ_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"` - SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"` - InstanceID string `env:"SMQ_AUTH_ADAPTER_INSTANCE_ID" envDefault:""` - AccessDuration time.Duration `env:"SMQ_AUTH_ACCESS_TOKEN_DURATION" envDefault:"1h"` - RefreshDuration time.Duration `env:"SMQ_AUTH_REFRESH_TOKEN_DURATION" envDefault:"24h"` - InvitationDuration time.Duration `env:"SMQ_AUTH_INVITATION_DURATION" envDefault:"168h"` - SpicedbHost string `env:"SMQ_SPICEDB_HOST" envDefault:"localhost"` - SpicedbPort string `env:"SMQ_SPICEDB_PORT" envDefault:"50051"` - SpicedbSchemaFile string `env:"SMQ_SPICEDB_SCHEMA_FILE" envDefault:"./docker/spicedb/schema.zed"` - SpicedbPreSharedKey string `env:"SMQ_SPICEDB_PRE_SHARED_KEY" envDefault:"12345678"` - TraceRatio float64 `env:"SMQ_JAEGER_TRACE_RATIO" envDefault:"1.0"` - ESURL string `env:"SMQ_ES_URL" envDefault:"nats://localhost:4222"` - CacheURL string `env:"SMQ_AUTH_CACHE_URL" envDefault:"redis://localhost:6379/0"` - CacheKeyDuration time.Duration `env:"SMQ_AUTH_CACHE_KEY_DURATION" envDefault:"10m"` + LogLevel string `env:"SMQ_AUTH_LOG_LEVEL" envDefault:"info"` + SecretKey string `env:"SMQ_AUTH_SECRET_KEY" envDefault:"secret"` + JaegerURL url.URL `env:"SMQ_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"` + SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"` + InstanceID string `env:"SMQ_AUTH_ADAPTER_INSTANCE_ID" envDefault:""` + AccessDuration time.Duration `env:"SMQ_AUTH_ACCESS_TOKEN_DURATION" envDefault:"1h"` + RefreshDuration time.Duration `env:"SMQ_AUTH_REFRESH_TOKEN_DURATION" envDefault:"24h"` + InvitationDuration time.Duration `env:"SMQ_AUTH_INVITATION_DURATION" envDefault:"168h"` + SpicedbHost string `env:"SMQ_SPICEDB_HOST" envDefault:"localhost"` + SpicedbPort string `env:"SMQ_SPICEDB_PORT" envDefault:"50051"` + SpicedbSchemaFile string `env:"SMQ_SPICEDB_SCHEMA_FILE" envDefault:"./docker/spicedb/schema.zed"` + SpicedbPreSharedKey string `env:"SMQ_SPICEDB_PRE_SHARED_KEY" envDefault:"12345678"` + TraceRatio float64 `env:"SMQ_JAEGER_TRACE_RATIO" envDefault:"1.0"` + ESURL string `env:"SMQ_ES_URL" envDefault:"nats://localhost:4222"` + CacheURL string `env:"SMQ_AUTH_CACHE_URL" envDefault:"redis://localhost:6379/0"` + CacheKeyDuration time.Duration `env:"SMQ_AUTH_CACHE_KEY_DURATION" envDefault:"10m"` AuthCalloutURLs []string `env:"SMQ_AUTH_CALLOUT_URLS" envDefault:"" envSeparator:","` AuthCalloutMethod string `env:"SMQ_AUTH_CALLOUT_METHOD" envDefault:"POST"` AuthCalloutTLSVerification bool `env:"SMQ_AUTH_CALLOUT_TLS_VERIFICATION" envDefault:"true"` + AuthCalloutTimeout time.Duration `env:"SMQ_AUTH_CALLOUT_TIMEOUT" envDefault:"10s"` } func main() { @@ -255,6 +256,7 @@ func newService(db *sqlx.DB, tracer trace.Tracer, cfg config, dbConfig pgclient. InsecureSkipVerify: !cfg.AuthCalloutTLSVerification, }, }, + Timeout: cfg.AuthCalloutTimeout, } callback, err := auth.NewCallback(httpClient, cfg.AuthCalloutMethod, cfg.AuthCalloutURLs) if err != nil { diff --git a/docker/.env b/docker/.env index 2d1cb8a5bc..eab041c9c7 100644 --- a/docker/.env +++ b/docker/.env @@ -104,6 +104,7 @@ SMQ_AUTH_CACHE_KEY_DURATION=10m SMQ_AUTH_CALLOUT_URLS="" SMQ_AUTH_CALLOUT_METHOD="POST" SMQ_AUTH_CALLOUT_TLS_VERIFICATION="false" +SMQ_AUTH_CALLOUT_TIMEOUT="10s" #### Auth Client Config SMQ_AUTH_URL=auth:9001 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index a67d23d17b..791ad1645d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -145,6 +145,7 @@ services: SMQ_AUTH_CALLOUT_URLS: ${SMQ_AUTH_CALLOUT_URLS} SMQ_AUTH_CALLOUT_METHOD: ${SMQ_AUTH_CALLOUT_METHOD} SMQ_AUTH_CALLOUT_TLS_VERIFICATION: ${SMQ_AUTH_CALLOUT_TLS_VERIFICATION} + SMQ_AUTH_CALLOUT_TIMEOUT: ${SMQ_AUTH_CALLOUT_TIMEOUT} ports: - ${SMQ_AUTH_HTTP_PORT}:${SMQ_AUTH_HTTP_PORT} - ${SMQ_AUTH_GRPC_PORT}:${SMQ_AUTH_GRPC_PORT} From cdceed798a303d63e521a8337731069bc9d2739c Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Fri, 28 Feb 2025 10:47:43 +0300 Subject: [PATCH 14/18] feat(auth-callout): Add support for custom certificates for HTTS requests Signed-off-by: Rodney Osodo --- auth/README.md | 3 +++ cmd/auth/main.go | 32 +++++++++++++++++++++++++++++--- docker/.env | 3 +++ docker/docker-compose.yml | 19 +++++++++++++++++++ 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/auth/README.md b/auth/README.md index dc768af86c..e6f8c32443 100644 --- a/auth/README.md +++ b/auth/README.md @@ -99,6 +99,9 @@ The service is configured using the environment variables presented in the follo | SMQ_AUTH_CALLOUT_METHOD | Callout method | POST | | SMQ_AUTH_CALLOUT_TLS_VERIFICATION | Enable TLS verification for callouts | true | | SMQ_AUTH_CALLOUT_TIMEOUT | Callout timeout | 10s | +| SMQ_AUTH_CALLOUT_CA_CERT | Path to CA certificate file | "" | +| SMQ_AUTH_CALLOUT_CERT | Path to client certificate file | "" | +| SMQ_AUTH_CALLOUT_KEY | Path to client key file | "" | ## Deployment diff --git a/cmd/auth/main.go b/cmd/auth/main.go index 80d6dad8fa..d8c81a9ac1 100644 --- a/cmd/auth/main.go +++ b/cmd/auth/main.go @@ -6,6 +6,8 @@ package main import ( "context" "crypto/tls" + "crypto/x509" + "errors" "fmt" "log" "log/slog" @@ -82,6 +84,9 @@ type config struct { AuthCalloutMethod string `env:"SMQ_AUTH_CALLOUT_METHOD" envDefault:"POST"` AuthCalloutTLSVerification bool `env:"SMQ_AUTH_CALLOUT_TLS_VERIFICATION" envDefault:"true"` AuthCalloutTimeout time.Duration `env:"SMQ_AUTH_CALLOUT_TIMEOUT" envDefault:"10s"` + AuthCalloutCACert string `env:"SMQ_AUTH_CALLOUT_CA_CERT" envDefault:""` + AuthCalloutCert string `env:"SMQ_AUTH_CALLOUT_CERT" envDefault:""` + AuthCalloutKey string `env:"SMQ_AUTH_CALLOUT_KEY" envDefault:""` } func main() { @@ -250,11 +255,32 @@ func newService(db *sqlx.DB, tracer trace.Tracer, cfg config, dbConfig pgclient. t := jwt.New([]byte(cfg.SecretKey)) + tlsConfig := &tls.Config{ + InsecureSkipVerify: !cfg.AuthCalloutTLSVerification, + } + if cfg.AuthCalloutCert != "" || cfg.AuthCalloutKey != "" { + clientTLSCert, err := tls.LoadX509KeyPair(cfg.AuthCalloutCert, cfg.AuthCalloutKey) + if err != nil { + return nil, err + } + certPool, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + caCert, err := os.ReadFile(cfg.AuthCalloutCACert) + if err != nil { + return nil, err + } + if !certPool.AppendCertsFromPEM(caCert) { + return nil, errors.New("failed to append CA certificate") + } + tlsConfig.RootCAs = certPool + tlsConfig.Certificates = []tls.Certificate{clientTLSCert} + } + httpClient := &http.Client{ Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: !cfg.AuthCalloutTLSVerification, - }, + TLSClientConfig: tlsConfig, }, Timeout: cfg.AuthCalloutTimeout, } diff --git a/docker/.env b/docker/.env index eab041c9c7..c5a8e8e1e0 100644 --- a/docker/.env +++ b/docker/.env @@ -105,6 +105,9 @@ SMQ_AUTH_CALLOUT_URLS="" SMQ_AUTH_CALLOUT_METHOD="POST" SMQ_AUTH_CALLOUT_TLS_VERIFICATION="false" SMQ_AUTH_CALLOUT_TIMEOUT="10s" +SMQ_AUTH_CALLOUT_CA_CERT="" +SMQ_AUTH_CALLOUT_CERT="" +SMQ_AUTH_CALLOUT_KEY="" #### Auth Client Config SMQ_AUTH_URL=auth:9001 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 791ad1645d..043c84cd60 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -146,6 +146,9 @@ services: SMQ_AUTH_CALLOUT_METHOD: ${SMQ_AUTH_CALLOUT_METHOD} SMQ_AUTH_CALLOUT_TLS_VERIFICATION: ${SMQ_AUTH_CALLOUT_TLS_VERIFICATION} SMQ_AUTH_CALLOUT_TIMEOUT: ${SMQ_AUTH_CALLOUT_TIMEOUT} + SMQ_AUTH_CALLOUT_CA_CERT: ${SMQ_AUTH_CALLOUT_CA_CERT} + SMQ_AUTH_CALLOUT_CERT: ${SMQ_AUTH_CALLOUT_CERT} + SMQ_AUTH_CALLOUT_KEY: ${SMQ_AUTH_CALLOUT_KEY} ports: - ${SMQ_AUTH_HTTP_PORT}:${SMQ_AUTH_HTTP_PORT} - ${SMQ_AUTH_GRPC_PORT}:${SMQ_AUTH_GRPC_PORT} @@ -175,6 +178,22 @@ services: target: /auth-grpc-client-ca${SMQ_AUTH_GRPC_CLIENT_CA_CERTS:+.crt} bind: create_host_path: true + # Auth Callout Client Certificates + - type: bind + source: ${SMQ_AUTH_CALLOUT_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /auth-callout-client${SMQ_AUTH_CALLOUT_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${SMQ_AUTH_CALLOUT_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /auth-callout-client${SMQ_AUTH_CALLOUT_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${SMQ_AUTH_CALLOUT_CLIENT_CA_CERTS:-ssl/certs/dummy/client_ca_certs} + target: /auth-callout-client-ca${SMQ_AUTH_CALLOUT_CLIENT_CA_CERTS:+.crt} + bind: + create_host_path: true domains-db: image: postgres:16.2-alpine From ff4fbff64b2bd0f0eea9bc97a30f4d325403d58c Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Fri, 28 Feb 2025 10:50:23 +0300 Subject: [PATCH 15/18] chore: update go modules Signed-off-by: Rodney Osodo --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index d4d14f48e9..66add36e07 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.4 require ( github.com/0x6flab/namegenerator v1.4.0 github.com/absmach/callhome v0.14.0 - github.com/absmach/certs v0.0.0-20250218103329-9fee99849118 + github.com/absmach/certs v0.0.0-20250226124728-fa26b7d3aa28 github.com/absmach/mgate v0.4.5 github.com/absmach/senml v1.0.6 github.com/authzed/authzed-go v1.3.0 @@ -51,7 +51,7 @@ require ( golang.org/x/crypto v0.35.0 golang.org/x/oauth2 v0.27.0 golang.org/x/sync v0.11.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99 + google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e google.golang.org/grpc v1.70.0 google.golang.org/protobuf v1.36.5 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df @@ -160,7 +160,7 @@ require ( golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.10.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250224174004-546df14abb99 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250227231956-55c901821b1e // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 15232ca27e..04243963f3 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrd github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/absmach/callhome v0.14.0 h1:zB4tIZJ1YUmZ1VGHFPfMA/Lo6/Mv19y2dvoOiXj2BWs= github.com/absmach/callhome v0.14.0/go.mod h1:l12UJOfibK4Muvg/AbupHuquNV9qSz/ROdTEPg7f2Vk= -github.com/absmach/certs v0.0.0-20250218103329-9fee99849118 h1:2LetQURw5XAJG6eTNOltEENkVqalEuvRfQtnC3FDb1A= -github.com/absmach/certs v0.0.0-20250218103329-9fee99849118/go.mod h1:kgB2x591CBWs/pZxt1cWqN9PAKAfdnzfED6FuAVhaF0= +github.com/absmach/certs v0.0.0-20250226124728-fa26b7d3aa28 h1:CTSj7kbihYXfMg9oLGYdrK4axYAdclvKVrySPIK5jFk= +github.com/absmach/certs v0.0.0-20250226124728-fa26b7d3aa28/go.mod h1:ZOChAukRsoylcCNyeKpdXNeRIlfmyfO5L6I9thHQKKo= github.com/absmach/mgate v0.4.5 h1:l6RmrEsR9jxkdb9WHUSecmT0HA41TkZZQVffFfUAIfI= github.com/absmach/mgate v0.4.5/go.mod h1:IvRIHZexZPEIAPmmaJF0L5DY2ERjj+GxRGitOW4s6qo= github.com/absmach/senml v1.0.6 h1:WPeIl6vQ00k7ghWSZYT/QP0KUxq2+4zQoaC7240pLFk= @@ -595,10 +595,10 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20250224174004-546df14abb99 h1:ilJhrCga0AptpJZXmUYG4MCrx/zf3l1okuYz7YK9PPw= -google.golang.org/genproto/googleapis/api v0.0.0-20250224174004-546df14abb99/go.mod h1:Xsh8gBVxGCcbV8ZeTB9wI5XPyZ5RvC6V3CTeeplHbiA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99 h1:ZSlhAUqC4r8TPzqLXQ0m3upBNZeF+Y8jQ3c4CR3Ujms= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/genproto/googleapis/api v0.0.0-20250227231956-55c901821b1e h1:nsxey/MfoGzYNduN0NN/+hqP9iiCIYsrVbXb/8hjFM8= +google.golang.org/genproto/googleapis/api v0.0.0-20250227231956-55c901821b1e/go.mod h1:Xsh8gBVxGCcbV8ZeTB9wI5XPyZ5RvC6V3CTeeplHbiA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e h1:YA5lmSs3zc/5w+xsRcHqpETkaYyK63ivEPzNTcUUlSA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= From 17cf2075ee13da25aa8e966e8068f35d2dd3c65b Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Mon, 3 Mar 2025 13:42:56 +0300 Subject: [PATCH 16/18] style: fix linter Signed-off-by: Rodney Osodo --- auth/api/http/keys/endpoint_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/api/http/keys/endpoint_test.go b/auth/api/http/keys/endpoint_test.go index 32830f320b..511d9244cd 100644 --- a/auth/api/http/keys/endpoint_test.go +++ b/auth/api/http/keys/endpoint_test.go @@ -78,7 +78,7 @@ func newService() (auth.Service, *mocks.KeyRepository) { t := jwt.New([]byte(secret)) callback := new(mocks.CallBack) - return auth.New(krepo, pRepo,cache, hash, idProvider, t, pEvaluator, pService, loginDuration, refreshDuration, invalidDuration, callback), krepo + return auth.New(krepo, pRepo, cache, hash, idProvider, t, pEvaluator, pService, loginDuration, refreshDuration, invalidDuration, callback), krepo } func newServer(svc auth.Service) *httptest.Server { From 8fe5350ec95b695cacf3d406ad6e139c0ad91272 Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Mon, 3 Mar 2025 18:31:57 +0300 Subject: [PATCH 17/18] docs(auth-callback): Add comment showing why we use first positive result Signed-off-by: Rodney Osodo --- auth/callback.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/auth/callback.go b/auth/callback.go index 048d5157b0..5bed70b7ab 100644 --- a/auth/callback.go +++ b/auth/callback.go @@ -64,6 +64,8 @@ func (c *callback) Authorize(ctx context.Context, pr policies.Policy) error { } var err error + // We use a single URL at a time and others as fallbacks + // the first positive result returned by a callback in the chain is considered to be final for i := range c.urls { if err = c.makeRequest(ctx, c.urls[i], payload); err == nil { return nil From 2e7f687258818e70003aa56596126514cbcf569a Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Mon, 3 Mar 2025 18:38:07 +0300 Subject: [PATCH 18/18] test(pats): use globally defined errors Signed-off-by: Rodney Osodo --- api/http/common.go | 4 +++- api/http/util/errors.go | 3 +++ auth/pat.go | 6 ++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/api/http/common.go b/api/http/common.go index a9358c07a7..d8aab06ee1 100644 --- a/api/http/common.go +++ b/api/http/common.go @@ -215,7 +215,9 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) { errors.Contains(err, apiutil.ErrMissingRoleName), errors.Contains(err, apiutil.ErrMissingRoleID), errors.Contains(err, apiutil.ErrMissingPolicyEntityType), - errors.Contains(err, apiutil.ErrMissingRoleMembers): + errors.Contains(err, apiutil.ErrMissingRoleMembers), + errors.Contains(err, apiutil.ErrMissingDescription), + errors.Contains(err, apiutil.ErrMissingEntityID): err = unwrap(err) w.WriteHeader(http.StatusBadRequest) diff --git a/api/http/util/errors.go b/api/http/util/errors.go index c2265df16a..c03db978c8 100644 --- a/api/http/util/errors.go +++ b/api/http/util/errors.go @@ -21,6 +21,9 @@ var ( // ErrMissingID indicates missing entity ID. ErrMissingID = errors.New("missing entity id") + // ErrMissingEntityID indicates missing entity ID. + ErrMissingEntityID = errors.New("missing entity id") + // ErrMissingClientID indicates missing client ID. ErrMissingClientID = errors.New("missing cient id") diff --git a/auth/pat.go b/auth/pat.go index 8fe084437c..b90af9b05c 100644 --- a/auth/pat.go +++ b/auth/pat.go @@ -10,6 +10,7 @@ import ( "strings" "time" + apiutil "github.com/absmach/supermq/api/http/util" "github.com/absmach/supermq/pkg/errors" ) @@ -275,15 +276,16 @@ func (s *Scope) Validate() error { return errInvalidScope } if s.EntityID == "" { - return errors.New("missing entityID") + return apiutil.ErrMissingEntityID } switch s.EntityType { case ChannelsType, GroupsType, ClientsType: if s.OptionalDomainID == "" { - return errors.New("missing domainID") + return apiutil.ErrMissingDomainID } } + return nil }