mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-23 04:10:28 +00:00
SMQ-2724 - Add Auth Callout (#2731)
Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>
This commit is contained in:
+3
-1
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
+81
-67
@@ -59,42 +59,49 @@ 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_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 | <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_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 |
|
||||
| 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
|
||||
|
||||
@@ -148,6 +155,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 +181,13 @@ PATs in SuperMQ are designed with the following features:
|
||||
### Token Structure
|
||||
|
||||
A PAT consists of three parts separated by underscores:
|
||||
|
||||
```
|
||||
pat_<encoded-user-and-pat-id>_<random-string>
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `pat` is a fixed prefix
|
||||
- `<encoded-user-and-pat-id>` is a base64-encoded combination of the user ID and PAT ID
|
||||
- `<random-string>` is a randomly generated string for additional security
|
||||
@@ -184,31 +196,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 +238,7 @@ curl --location 'http://localhost:9001/pats' \
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "a2500226-95dc-4285-87e2-e693e4a0a976",
|
||||
@@ -333,6 +346,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 +358,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 +371,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 +384,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": "*"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
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, 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 {
|
||||
if len(c.urls) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *callback) makeRequest(ctx context.Context, urlStr string, params map[string]string) error {
|
||||
var req *http.Request
|
||||
var err error
|
||||
|
||||
switch c.method {
|
||||
case http.MethodGet:
|
||||
query := url.Values{}
|
||||
for key, value := range params {
|
||||
query.Set(key, value)
|
||||
}
|
||||
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, c.method, urlStr, bytes.NewReader(data))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
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.NewSDKErrorWithStatus(svcerr.ErrAuthorization, resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
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, 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)
|
||||
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, 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, 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)
|
||||
}
|
||||
|
||||
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, 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, err := auth.NewCallback(nil, http.MethodPost, []string{"test"})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, cb)
|
||||
}
|
||||
|
||||
func TestCallback_NoURL(t *testing.T) {
|
||||
cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{})
|
||||
assert.NoError(t, err)
|
||||
err = cb.Authorize(context.Background(), policies.Policy{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+4
-2
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+9
-2
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+53
-12
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+70
-20
@@ -5,9 +5,13 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
@@ -60,22 +64,29 @@ 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"`
|
||||
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() {
|
||||
@@ -145,7 +156,12 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
svc := 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
|
||||
return
|
||||
}
|
||||
|
||||
grpcServerConfig := server.Config{Port: defSvcGRPCPort}
|
||||
if err := env.ParseWithOptions(&grpcServerConfig, env.Options{Prefix: envPrefixGrpc}); err != nil {
|
||||
@@ -225,7 +241,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(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)
|
||||
@@ -239,11 +255,45 @@ 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)
|
||||
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: tlsConfig,
|
||||
},
|
||||
Timeout: cfg.AuthCalloutTimeout,
|
||||
}
|
||||
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)
|
||||
counter, latency := prometheus.MakeMetrics("auth", "api")
|
||||
svc = api.MetricsMiddleware(svc, counter, latency)
|
||||
svc = tracing.New(svc, tracer)
|
||||
|
||||
return svc
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
@@ -101,6 +101,13 @@ 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"
|
||||
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
|
||||
|
||||
@@ -142,6 +142,13 @@ 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}
|
||||
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}
|
||||
@@ -171,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
|
||||
|
||||
@@ -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-20250226124728-fa26b7d3aa28
|
||||
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-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
|
||||
@@ -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-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
|
||||
|
||||
@@ -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-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=
|
||||
@@ -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-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=
|
||||
|
||||
Reference in New Issue
Block a user