mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 15:25:48 +00:00
Compare commits
131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8410b8940 | |||
| 2260293dfc | |||
| c9f34e3759 | |||
| aeb1d9d0a1 | |||
| 03a8feb679 | |||
| 9c2608659f | |||
| 2c476c17ee | |||
| 3b2dd09ab4 | |||
| 5255d887ef | |||
| bb8917cc34 | |||
| 4792a99c9a | |||
| fe49305238 | |||
| 03143d4142 | |||
| 15a6c026e9 | |||
| 175f0e08ab | |||
| 41526634f8 | |||
| 73a640e646 | |||
| 6ca535155a | |||
| 4a449629b3 | |||
| 1cd00fbd83 | |||
| a871fa926e | |||
| c7fad0d2ca | |||
| 51e782693d | |||
| e3a3e052d2 | |||
| 1e8437cfc2 | |||
| f8f4ccac79 | |||
| 56bb07273d | |||
| aba4d6a0b7 | |||
| b948edc573 | |||
| 20bafe0077 | |||
| 4e77ae65e2 | |||
| 61c120f947 | |||
| 0d4f4c9266 | |||
| 668338dfa2 | |||
| 38ea004e30 | |||
| 7b99baeb02 | |||
| cd31b1c6bd | |||
| 1a348b11dd | |||
| f552e08661 | |||
| 5606cdbe47 | |||
| 59d9f91813 | |||
| 5b913dd46b | |||
| 9293de7636 | |||
| 2dd8c44aa3 | |||
| 28fc9738df | |||
| ad1d67a678 | |||
| 1086f3e208 | |||
| b896c03f5f | |||
| d78c1e8d20 | |||
| 551d44d58e | |||
| 927230dbb4 | |||
| c7996ce6cf | |||
| bf7a965195 | |||
| 67c28ff134 | |||
| a526a2ccd5 | |||
| c679224596 | |||
| e1c881d2ad | |||
| 9a0d8062be | |||
| 25d8b86f2b | |||
| 24ca513eff | |||
| 52510d8c62 | |||
| 6b6bab79c6 | |||
| 6a5d28c65a | |||
| 59f8d4e4d7 | |||
| e23555ee2a | |||
| cb337a38fd | |||
| f9b503f1d6 | |||
| 0fe5a6d6e8 | |||
| f612109f24 | |||
| 3fcf2e5369 | |||
| 1355bc8bb7 | |||
| 1e7e65d642 | |||
| 3a5c1e12df | |||
| 61b95a28c2 | |||
| 4c40b8b82c | |||
| 955c556fe6 | |||
| dc437a8296 | |||
| 314537f7c5 | |||
| 7e5fd0cfa8 | |||
| 6a2c8a3ca6 | |||
| 2bea4412d8 | |||
| c8cb0daa6c | |||
| f8017c8b9f | |||
| 27fc32c153 | |||
| 9aa9487207 | |||
| 1292095cdb | |||
| 856ef1ba49 | |||
| 146028fb98 | |||
| 6cb3e635c9 | |||
| 175d27c4b6 | |||
| eec6b8be07 | |||
| e48e5dee1f | |||
| 0df8e8451c | |||
| e77e3a3bb6 | |||
| 3e6b771850 | |||
| 15e756a5a3 | |||
| c5a4336d43 | |||
| b05ae65e24 | |||
| 1a8d11acdd | |||
| 27b72db52e | |||
| e75ce59998 | |||
| cd281c2589 | |||
| 9d13d5b528 | |||
| f7dcaa949b | |||
| fa080ab0f8 | |||
| 5807cf1754 | |||
| f96ddab9dd | |||
| 914d491a13 | |||
| 843a0cae1f | |||
| 9ba9a12084 | |||
| 5ed6b258a0 | |||
| eb83edbcde | |||
| 5c12bc8c91 | |||
| 589209eae2 | |||
| a2e660b8a1 | |||
| 21731aef37 | |||
| 2f55947305 | |||
| 80806c4745 | |||
| 1947f6e78e | |||
| 5b0c556b60 | |||
| 5891e4bb75 | |||
| 299742096a | |||
| 27c57b192b | |||
| 8083cec373 | |||
| e2d9b1ddf9 | |||
| 6092bc2cf0 | |||
| 071d23a5f0 | |||
| 7bb4b298c5 | |||
| cac6c648d3 | |||
| 4ee7736b32 | |||
| 8ed8915e4f |
@@ -5,9 +5,10 @@
|
||||
coverage:
|
||||
ignore:
|
||||
- "tools/*"
|
||||
- "api/grpc/**"
|
||||
- "cmd/*"
|
||||
- "coap/*"
|
||||
- "*/mocks/*"
|
||||
- "coap/**"
|
||||
- "**/mocks/*"
|
||||
- "*/middleware/*"
|
||||
- "pkg/sid/mock.go"
|
||||
- "pkg/tracing/utils.go"
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
<!--
|
||||
Copyright (c) Abstract Machines
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SuperMQ API Documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.30.3/swagger-ui.css">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.topbar {
|
||||
display: none;
|
||||
}
|
||||
.service-selector {
|
||||
background: #1b1b1b;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.service-selector h1 {
|
||||
color: #fff;
|
||||
margin: 0 0 15px 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.service-dropdown-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.service-dropdown-container label {
|
||||
color: #fff;
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.service-dropdown {
|
||||
background: #2d2d2d;
|
||||
color: white;
|
||||
border: 1px solid #4990e2;
|
||||
padding: 10px 40px 10px 15px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
min-width: 200px;
|
||||
appearance: none;
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><path fill="%23ffffff" d="M6 9L1 4h10z"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
}
|
||||
.service-dropdown:hover {
|
||||
background-color: #3a3a3a;
|
||||
border-color: #357abd;
|
||||
}
|
||||
.service-dropdown:focus {
|
||||
outline: none;
|
||||
border-color: #4990e2;
|
||||
box-shadow: 0 0 0 2px rgba(73, 144, 226, 0.3);
|
||||
}
|
||||
|
||||
/* Responsive styles for mobile */
|
||||
@media (max-width: 768px) {
|
||||
.service-selector {
|
||||
padding: 15px 10px;
|
||||
}
|
||||
.service-selector h1 {
|
||||
font-size: 1.5rem;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
.service-dropdown-container {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.service-dropdown-container label {
|
||||
font-size: 13px;
|
||||
}
|
||||
.service-dropdown {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
min-width: auto;
|
||||
font-size: 13px;
|
||||
padding: 8px 35px 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.service-selector h1 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.service-dropdown {
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="service-selector">
|
||||
<h1>SuperMQ API Documentation</h1>
|
||||
<div class="service-dropdown-container">
|
||||
<label for="serviceDropdown">Select Service:</label>
|
||||
<select id="serviceDropdown" class="service-dropdown"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.30.3/swagger-ui-bundle.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.30.3/swagger-ui-standalone-preset.js"></script>
|
||||
<script>
|
||||
// Available API specifications
|
||||
const APIs = APIS_PLACEHOLDER;
|
||||
|
||||
// Get the service from URL query parameter, default to first service
|
||||
function getServiceFromURL() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const service = params.get('service');
|
||||
return service && APIs.includes(service) ? service : APIs[0];
|
||||
}
|
||||
|
||||
// Update URL with selected service
|
||||
function updateURL(service) {
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('service', service);
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
|
||||
// Create service selector dropdown
|
||||
function createServiceDropdown() {
|
||||
const dropdown = document.getElementById('serviceDropdown');
|
||||
const currentService = getServiceFromURL();
|
||||
|
||||
APIs.forEach(api => {
|
||||
const option = document.createElement('option');
|
||||
option.value = api;
|
||||
let serviceName = api.replace('.yaml', '').replace(/^\w/, c => c.toUpperCase());
|
||||
if (serviceName.toLowerCase() === 'http') {
|
||||
serviceName = 'HTTP';
|
||||
}
|
||||
option.textContent = serviceName;
|
||||
if (api === currentService) {
|
||||
option.selected = true;
|
||||
}
|
||||
dropdown.appendChild(option);
|
||||
});
|
||||
|
||||
// Handle dropdown change
|
||||
dropdown.addEventListener('change', (e) => {
|
||||
const selectedApi = e.target.value;
|
||||
loadSwaggerUI(selectedApi);
|
||||
updateURL(selectedApi);
|
||||
});
|
||||
}
|
||||
|
||||
// Load Swagger UI with specified API spec
|
||||
function loadSwaggerUI(apiSpec) {
|
||||
SwaggerUIBundle({
|
||||
url: apiSpec,
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize
|
||||
createServiceDropdown();
|
||||
loadSwaggerUI(getServiceFromURL());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,6 +4,21 @@
|
||||
name: Property Based Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/workflows/api-tests.yaml"
|
||||
- "api/**"
|
||||
- "auth/api/http/**"
|
||||
- "channels/api/http/**"
|
||||
- "clients/api/http/**"
|
||||
- "domains/api/http/**"
|
||||
- "groups/api/http/**"
|
||||
- "http/api/**"
|
||||
- "journal/api/**"
|
||||
- "users/api/**"
|
||||
- "apidocs/openapi/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
@@ -18,6 +33,11 @@ on:
|
||||
- "http/api/**"
|
||||
- "journal/api/**"
|
||||
- "users/api/**"
|
||||
- "apidocs/openapi/**"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
TOKENS_URL: http://localhost:9002/users/tokens/issue
|
||||
@@ -39,19 +59,80 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Go version from go.mod
|
||||
id: go-version
|
||||
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
go-version: ${{ steps.go-version.outputs.version }}
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Check for changes in specific paths
|
||||
uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
workflow:
|
||||
- ".github/workflows/api-tests.yaml"
|
||||
|
||||
journal:
|
||||
- "apidocs/openapi/journal.yaml"
|
||||
- "journal/api/**"
|
||||
|
||||
auth:
|
||||
- "apidocs/openapi/auth.yaml"
|
||||
- "auth/api/http/**"
|
||||
|
||||
domains:
|
||||
- "apidocs/openapi/domains.yaml"
|
||||
- "domains/api/http/**"
|
||||
|
||||
http:
|
||||
- "apidocs/openapi/http.yaml"
|
||||
- "http/api/**"
|
||||
|
||||
clients:
|
||||
- "apidocs/openapi/clients.yaml"
|
||||
- "clients/api/http/**"
|
||||
|
||||
channels:
|
||||
- "apidocs/openapi/channels.yaml"
|
||||
- "channels/api/http/**"
|
||||
|
||||
groups:
|
||||
- "apidocs/openapi/groups.yaml"
|
||||
- "groups/api/http/**"
|
||||
|
||||
users:
|
||||
- "apidocs/openapi/users.yaml"
|
||||
- "users/api/**"
|
||||
|
||||
- name: Build images
|
||||
run: make all -j $(nproc) && make dockers_dev -j $(nproc)
|
||||
|
||||
- name: Start containers
|
||||
run: make run up args="-d" && make run_addons up args="-d"
|
||||
run: make run_latest up args="-d" && make run_addons up args="-d"
|
||||
|
||||
- name: Wait for services to be ready
|
||||
run: |
|
||||
echo "Waiting for services to start..."
|
||||
sleep 15
|
||||
|
||||
# Check if services are responding
|
||||
for i in {1..30}; do
|
||||
if curl -f -s http://localhost:9002/health > /dev/null 2>&1; then
|
||||
echo "Services are ready!"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for services... ($i/30)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Set access token
|
||||
run: |
|
||||
@@ -61,62 +142,17 @@ jobs:
|
||||
export CLIENT_SECRET=$(supermq-cli provision test | /usr/bin/grep -Eo '"secret": "[^"]+"' | awk 'NR % 2 == 0' | sed 's/"secret": "\(.*\)"/\1/')
|
||||
echo "CLIENT_SECRET=$CLIENT_SECRET" >> $GITHUB_ENV
|
||||
|
||||
- name: Check for changes in specific paths
|
||||
uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
journal:
|
||||
- ".github/workflows/api-tests.yaml"
|
||||
- "apidocs/openapi/journal.yaml"
|
||||
- "journal/api/**"
|
||||
|
||||
auth:
|
||||
- ".github/workflows/api-tests.yaml"
|
||||
- "apidocs/openapi/auth.yaml"
|
||||
- "auth/api/http/**"
|
||||
|
||||
domains:
|
||||
- ".github/workflows/api-tests.yaml"
|
||||
- "apidocs/openapi/domains.yaml"
|
||||
- "domains/api/http/**"
|
||||
|
||||
http:
|
||||
- ".github/workflows/api-tests.yaml"
|
||||
- "apidocs/openapi/http.yaml"
|
||||
- "http/api/**"
|
||||
|
||||
clients:
|
||||
- ".github/workflows/api-tests.yaml"
|
||||
- "apidocs/openapi/clients.yaml"
|
||||
- "clients/api/http/**"
|
||||
|
||||
channels:
|
||||
- ".github/workflows/api-tests.yaml"
|
||||
- "apidocs/openapi/channels.yaml"
|
||||
- "channels/api/http/**"
|
||||
|
||||
groups:
|
||||
- ".github/workflows/api-tests.yaml"
|
||||
- "apidocs/openapi/groups.yaml"
|
||||
- "groups/api/http/**"
|
||||
|
||||
users:
|
||||
- ".github/workflows/api-tests.yaml"
|
||||
- "apidocs/openapi/users.yaml"
|
||||
- "users/api/**"
|
||||
|
||||
- name: Run Users API tests
|
||||
if: steps.changes.outputs.users == 'true'
|
||||
if: steps.changes.outputs.users == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v2.1.0
|
||||
with:
|
||||
schema: apidocs/openapi/users.yaml
|
||||
base-url: ${{ env.USERS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --exclude-operation-id=requestPasswordReset --phases=examples,stateful'
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --exclude-operation-id=requestPasswordReset --phases=examples'
|
||||
|
||||
- name: Run Groups API tests
|
||||
if: steps.changes.outputs.groups == 'true'
|
||||
if: steps.changes.outputs.groups == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v2.1.0
|
||||
with:
|
||||
schema: apidocs/openapi/groups.yaml
|
||||
@@ -125,7 +161,7 @@ jobs:
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Clients API tests
|
||||
if: steps.changes.outputs.clients == 'true'
|
||||
if: steps.changes.outputs.clients == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v2.1.0
|
||||
with:
|
||||
schema: apidocs/openapi/clients.yaml
|
||||
@@ -134,7 +170,7 @@ jobs:
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Channels API tests
|
||||
if: steps.changes.outputs.channels == 'true'
|
||||
if: steps.changes.outputs.channels == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v2.1.0
|
||||
with:
|
||||
schema: apidocs/openapi/channels.yaml
|
||||
@@ -143,7 +179,7 @@ jobs:
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run HTTP Adapter API tests
|
||||
if: steps.changes.outputs.http == 'true'
|
||||
if: steps.changes.outputs.http == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v2.1.0
|
||||
with:
|
||||
schema: apidocs/openapi/http.yaml
|
||||
@@ -152,7 +188,7 @@ jobs:
|
||||
args: '--header "Authorization: Client ${{ env.CLIENT_SECRET }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Auth API tests
|
||||
if: steps.changes.outputs.auth == 'true'
|
||||
if: steps.changes.outputs.auth == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v2.1.0
|
||||
with:
|
||||
schema: apidocs/openapi/auth.yaml
|
||||
@@ -161,7 +197,7 @@ jobs:
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Domains API tests
|
||||
if: steps.changes.outputs.domains == 'true'
|
||||
if: steps.changes.outputs.domains == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v2.1.0
|
||||
with:
|
||||
schema: apidocs/openapi/domains.yaml
|
||||
@@ -170,7 +206,7 @@ jobs:
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Journal API tests
|
||||
if: steps.changes.outputs.journal == 'true'
|
||||
if: steps.changes.outputs.journal == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v2.1.0
|
||||
with:
|
||||
schema: apidocs/openapi/journal.yaml
|
||||
@@ -180,4 +216,4 @@ jobs:
|
||||
|
||||
- name: Stop containers
|
||||
if: always()
|
||||
run: make run down args="-v" && make run_addons down args="-v"
|
||||
run: make run_latest down args="-v" && make run_addons down args="-v"
|
||||
|
||||
@@ -7,45 +7,46 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
- '.github/workflows/**'
|
||||
- 'LICENSE'
|
||||
- 'MAINTAINERS'
|
||||
- 'CODEOWNERS'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
lint-and-build:
|
||||
uses: ./.github/workflows/lint-and-build.yaml
|
||||
|
||||
build-and-push:
|
||||
name: Build and Push
|
||||
name: Build and Push Docker Images
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint-and-build]
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Fetch tags for the build
|
||||
run: |
|
||||
git fetch --prune --unshallow --tags
|
||||
- name: Get Go version from go.mod
|
||||
id: go-version
|
||||
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
go-version: ${{ steps.go-version.outputs.version }}
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Set GOBIN
|
||||
run: echo "GOBIN=$HOME/.local/bin" >> $GITHUB_ENV
|
||||
|
||||
- name: Add GOBIN to PATH
|
||||
run: echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
make test
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV }}
|
||||
files: ./coverage/*.out
|
||||
verbose: true
|
||||
|
||||
- name: Set up Docker Build
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
@@ -56,22 +57,7 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Compile check for rabbitmq
|
||||
run: |
|
||||
SMQ_MESSAGE_BROKER_TYPE=msg_rabbitmq make mqtt
|
||||
|
||||
- name: Compile check for redis
|
||||
run: |
|
||||
SMQ_ES_TYPE=es_redis make mqtt
|
||||
|
||||
- name: Build and push Docker images
|
||||
run: |
|
||||
make latest -j $(nproc)
|
||||
|
||||
- name: Trigger Helm Chart Deployment
|
||||
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: peter-evans/repository-dispatch@v4
|
||||
with:
|
||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||
repository: absmach/amdm
|
||||
event-type: deploy-latest-smq-images
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
name: Check the consistency of generated files
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
@@ -14,14 +11,22 @@ on:
|
||||
jobs:
|
||||
check-generated-files:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PROTOC_VERSION: "33.0"
|
||||
PROTOC_GEN_GO_VERSION: "v1.36.11"
|
||||
PROTOC_GEN_GO_GRPC_VERSION: "v1.6.0"
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get Go version from go.mod
|
||||
id: go-version
|
||||
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
go-version: ${{ steps.go-version.outputs.version }}
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Set GOBIN
|
||||
@@ -69,32 +74,18 @@ jobs:
|
||||
- "journal/journal.go"
|
||||
- "consumers/notifier.go"
|
||||
|
||||
- name: Set up protoc
|
||||
- name: Install Protoc
|
||||
if: steps.changes.outputs.proto == 'true'
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
version: ${{ env.PROTOC_VERSION }}
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Go Protobuf Plugins
|
||||
if: steps.changes.outputs.proto == 'true'
|
||||
run: |
|
||||
PROTOC_VERSION=33.0
|
||||
PROTOC_GEN_VERSION=v1.36.10
|
||||
PROTOC_GRPC_VERSION=v1.5.1
|
||||
|
||||
# Export the variables so they are available in future steps
|
||||
echo "PROTOC_VERSION=$PROTOC_VERSION" >> $GITHUB_ENV
|
||||
echo "PROTOC_GEN_VERSION=$PROTOC_GEN_VERSION" >> $GITHUB_ENV
|
||||
echo "PROTOC_GRPC_VERSION=$PROTOC_GRPC_VERSION" >> $GITHUB_ENV
|
||||
|
||||
# Download and install protoc
|
||||
PROTOC_ZIP=protoc-$PROTOC_VERSION-linux-x86_64.zip
|
||||
curl -0L -o $PROTOC_ZIP https://github.com/protocolbuffers/protobuf/releases/download/v$PROTOC_VERSION/$PROTOC_ZIP
|
||||
unzip -o $PROTOC_ZIP -d protoc3
|
||||
sudo mv protoc3/bin/* /usr/local/bin/
|
||||
sudo mv protoc3/include/* /usr/local/include/
|
||||
rm -rf $PROTOC_ZIP protoc3
|
||||
|
||||
# Install protoc-gen-go and protoc-gen-go-grpc
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@$PROTOC_GEN_VERSION
|
||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@$PROTOC_GRPC_VERSION
|
||||
|
||||
# Add protoc to the PATH
|
||||
export PATH=$PATH:/usr/local/bin/protoc
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@$PROTOC_GEN_GO_VERSION
|
||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@$PROTOC_GEN_GO_GRPC_VERSION
|
||||
|
||||
- name: Check Protobuf is up to Date
|
||||
if: steps.changes.outputs.proto == 'true'
|
||||
@@ -110,7 +101,7 @@ jobs:
|
||||
echo "Error: Proto file and generated Go file $p are out of sync!"
|
||||
echo "Here is the difference:"
|
||||
diff $p $p.tmp || true
|
||||
echo "Please run 'make proto' with protoc version $PROTOC_VERSION, protoc-gen-go version $PROTOC_GEN_VERSION and protoc-gen-go-grpc version $PROTOC_GRPC_VERSION and commit the changes."
|
||||
echo "Please run 'make proto' with protoc version $PROTOC_VERSION, protoc-gen-go version $PROTOC_GEN_GO_VERSION and protoc-gen-go-grpc version $PROTOC_GEN_GO_GRPC_VERSION and commit the changes."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
name: Check License Header
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
@@ -16,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check License Header
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Lint and Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get Go version from go.mod
|
||||
id: go-version
|
||||
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ steps.go-version.outputs.version }}
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Run linters
|
||||
uses: golangci/golangci-lint-action@v9.2.0
|
||||
with:
|
||||
version: v2.10.1
|
||||
args: --config ./tools/config/.golangci.yaml
|
||||
|
||||
build:
|
||||
name: Build All Binaries
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get Go version from go.mod
|
||||
id: go-version
|
||||
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ steps.go-version.outputs.version }}
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Build all binaries
|
||||
run: |
|
||||
make all -j $(nproc)
|
||||
|
||||
compile-check:
|
||||
name: Compile Check ${{ matrix.variant.name }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
variant:
|
||||
- name: rabbitmq
|
||||
env: SMQ_MESSAGE_BROKER_TYPE=msg_rabbitmq
|
||||
target: mqtt
|
||||
- name: redis
|
||||
env: SMQ_ES_TYPE=es_redis
|
||||
target: mqtt
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get Go version from go.mod
|
||||
id: go-version
|
||||
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ steps.go-version.outputs.version }}
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Compile check for ${{ matrix.variant.name }}
|
||||
run: |
|
||||
${{ matrix.variant.env }} make ${{ matrix.variant.target }}
|
||||
@@ -8,24 +8,38 @@ on:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
swagger-ui:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Swagger UI action
|
||||
id: swagger-ui-action
|
||||
uses: blokovi/swagger-ui-action@main
|
||||
with:
|
||||
dir: "./apidocs/openapi"
|
||||
pattern: "*.yaml"
|
||||
debug: "true"
|
||||
- name: Build Swagger UI
|
||||
run: |
|
||||
# Create output directory
|
||||
mkdir -p swagger-ui
|
||||
|
||||
# Copy OpenAPI YAML files and schemas directory
|
||||
cp apidocs/openapi/*.yaml swagger-ui/
|
||||
cp -r apidocs/openapi/schemas swagger-ui/
|
||||
|
||||
# Get list of YAML files
|
||||
cd apidocs/openapi
|
||||
YAML_FILES=$(ls *.yaml | jq -R -s -c 'split("\n")[:-1]')
|
||||
cd ../..
|
||||
|
||||
# Generate index.html from template
|
||||
sed "s|APIS_PLACEHOLDER|$YAML_FILES|g" .github/swagger-ui-template.html > swagger-ui/index.html
|
||||
|
||||
echo "Generated Swagger UI with APIs: $YAML_FILES"
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: swagger-ui
|
||||
publish_dir: ./swagger-ui
|
||||
cname: docs.api.supermq.absmach.eu
|
||||
|
||||
+155
-141
@@ -4,24 +4,30 @@
|
||||
name: CI Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
- 'LICENSE'
|
||||
- 'MAINTAINERS'
|
||||
- 'CODEOWNERS'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
lint-and-build: # Linting and building are combined to save time for setting up Go
|
||||
name: Lint and Build
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-certs:
|
||||
name: Check Certs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
cache-dependency-path: "go.sum"
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Fetch Certs
|
||||
run: |
|
||||
@@ -34,6 +40,23 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
lint-proto:
|
||||
name: Lint Proto
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get Go version from go.mod
|
||||
id: go-version
|
||||
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ steps.go-version.outputs.version }}
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Install protolint
|
||||
run: |
|
||||
go install github.com/yoheimuta/protolint/cmd/protolint@latest
|
||||
@@ -42,46 +65,28 @@ jobs:
|
||||
run: |
|
||||
protolint .
|
||||
|
||||
- name: Run linters
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: v2.4.0
|
||||
args: --config ./tools/config/.golangci.yaml
|
||||
lint-and-build:
|
||||
needs: [check-certs, lint-proto]
|
||||
uses: ./.github/workflows/lint-and-build.yaml
|
||||
|
||||
- name: Build all Binaries
|
||||
run: |
|
||||
make all -j $(nproc)
|
||||
|
||||
- name: Compile check for rabbitmq
|
||||
run: |
|
||||
SMQ_MESSAGE_BROKER_TYPE=msg_rabbitmq make mqtt
|
||||
|
||||
- name: Compile check for redis
|
||||
run: |
|
||||
SMQ_ES_TYPE=es_redis make mqtt
|
||||
|
||||
run-tests:
|
||||
name: Run tests
|
||||
detect-changes:
|
||||
name: Detect Changes
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint-and-build
|
||||
|
||||
outputs:
|
||||
modules: ${{ steps.set-matrix.outputs.modules }}
|
||||
workflow_changed: ${{ steps.changes.outputs.workflow }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Check for changes in specific paths
|
||||
uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
base: main
|
||||
filters: |
|
||||
workflow:
|
||||
- ".github/workflows/tests.yaml"
|
||||
@@ -235,131 +240,140 @@ jobs:
|
||||
- "pkg/uuid/**"
|
||||
- "pkg/events/**"
|
||||
|
||||
ws:
|
||||
- "ws/**"
|
||||
- "cmd/ws/**"
|
||||
notifications:
|
||||
- "notifications/**"
|
||||
- "cmd/notifications/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "clients/**"
|
||||
- "pkg/messaging/**"
|
||||
- "consumers/notifier.go"
|
||||
- "pkg/events/**"
|
||||
|
||||
- name: Create coverage directory
|
||||
run: |
|
||||
mkdir coverage
|
||||
api:
|
||||
- "api/**"
|
||||
|
||||
- name: Run journal tests
|
||||
if: steps.changes.outputs.journal == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/journal.out ./journal/...
|
||||
consumers:
|
||||
- "consumers/**"
|
||||
|
||||
- name: Run auth tests
|
||||
if: steps.changes.outputs.auth == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/auth.out ./auth/...
|
||||
readers:
|
||||
- "readers/**"
|
||||
|
||||
- name: Run domains tests
|
||||
if: steps.changes.outputs.domains == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
- name: Set matrix for changed modules
|
||||
id: set-matrix
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/domains.out ./domains/...
|
||||
modules=()
|
||||
|
||||
- name: Run cli tests
|
||||
if: steps.changes.outputs.cli == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/cli.out ./cli/...
|
||||
if [[ "${{ steps.changes.outputs.workflow }}" == "true" || "${{ steps.changes.outputs.pkg-errors }}" == "true" ]]; then
|
||||
# If workflow or pkg/errors changed, test everything
|
||||
modules=("auth" "channels" "cli" "clients" "coap" "domains" "groups" "http" "internal" "journal" "logger" "mqtt" "pkg-errors" "pkg-events" "pkg-grpcclient" "pkg-messaging" "pkg-sdk" "pkg-transformers" "pkg-ulid" "pkg-uuid" "users" "notifications" "api" "consumers" "readers")
|
||||
else
|
||||
# Add only changed modules
|
||||
[[ "${{ steps.changes.outputs.auth }}" == "true" ]] && modules+=("auth")
|
||||
[[ "${{ steps.changes.outputs.channels }}" == "true" ]] && modules+=("channels")
|
||||
[[ "${{ steps.changes.outputs.cli }}" == "true" ]] && modules+=("cli")
|
||||
[[ "${{ steps.changes.outputs.clients }}" == "true" ]] && modules+=("clients")
|
||||
[[ "${{ steps.changes.outputs.coap }}" == "true" ]] && modules+=("coap")
|
||||
[[ "${{ steps.changes.outputs.domains }}" == "true" ]] && modules+=("domains")
|
||||
[[ "${{ steps.changes.outputs.groups }}" == "true" ]] && modules+=("groups")
|
||||
[[ "${{ steps.changes.outputs.http }}" == "true" ]] && modules+=("http")
|
||||
[[ "${{ steps.changes.outputs.internal }}" == "true" ]] && modules+=("internal")
|
||||
[[ "${{ steps.changes.outputs.journal }}" == "true" ]] && modules+=("journal")
|
||||
[[ "${{ steps.changes.outputs.logger }}" == "true" ]] && modules+=("logger")
|
||||
[[ "${{ steps.changes.outputs.mqtt }}" == "true" ]] && modules+=("mqtt")
|
||||
[[ "${{ steps.changes.outputs.pkg-errors }}" == "true" ]] && modules+=("pkg-errors")
|
||||
[[ "${{ steps.changes.outputs.pkg-events }}" == "true" ]] && modules+=("pkg-events")
|
||||
[[ "${{ steps.changes.outputs.pkg-grpcclient }}" == "true" ]] && modules+=("pkg-grpcclient")
|
||||
[[ "${{ steps.changes.outputs.pkg-messaging }}" == "true" ]] && modules+=("pkg-messaging")
|
||||
[[ "${{ steps.changes.outputs.pkg-sdk }}" == "true" ]] && modules+=("pkg-sdk")
|
||||
[[ "${{ steps.changes.outputs.pkg-transformers }}" == "true" ]] && modules+=("pkg-transformers")
|
||||
[[ "${{ steps.changes.outputs.pkg-ulid }}" == "true" ]] && modules+=("pkg-ulid")
|
||||
[[ "${{ steps.changes.outputs.pkg-uuid }}" == "true" ]] && modules+=("pkg-uuid")
|
||||
[[ "${{ steps.changes.outputs.users }}" == "true" ]] && modules+=("users")
|
||||
[[ "${{ steps.changes.outputs.notifications }}" == "true" ]] && modules+=("notifications")
|
||||
[[ "${{ steps.changes.outputs.api }}" == "true" ]] && modules+=("api")
|
||||
[[ "${{ steps.changes.outputs.consumers }}" == "true" ]] && modules+=("consumers")
|
||||
[[ "${{ steps.changes.outputs.readers }}" == "true" ]] && modules+=("readers")
|
||||
fi
|
||||
|
||||
- name: Run CoAP tests
|
||||
if: steps.changes.outputs.coap == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/coap.out ./coap/...
|
||||
# Convert to JSON array
|
||||
json_modules=$(printf '%s\n' "${modules[@]}" | jq -R . | jq -s -c .)
|
||||
echo "modules=$json_modules" >> $GITHUB_OUTPUT
|
||||
echo "Testing modules: $json_modules"
|
||||
|
||||
- name: Run HTTP tests
|
||||
if: steps.changes.outputs.http == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/http.out ./http/...
|
||||
run-tests:
|
||||
name: Test ${{ matrix.module }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint-and-build, detect-changes]
|
||||
if: needs.detect-changes.outputs.modules != '[]'
|
||||
strategy:
|
||||
fail-fast: true
|
||||
max-parallel: 20
|
||||
matrix:
|
||||
module: ${{ fromJSON(needs.detect-changes.outputs.modules) }}
|
||||
|
||||
- name: Run internal tests
|
||||
if: steps.changes.outputs.internal == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/internal.out ./internal/...
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Run logger tests
|
||||
if: steps.changes.outputs.logger == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/logger.out ./logger/...
|
||||
- name: Get Go version from go.mod
|
||||
id: go-version
|
||||
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run MQTT tests
|
||||
if: steps.changes.outputs.mqtt == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/mqtt.out ./mqtt/...
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ steps.go-version.outputs.version }}
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Run pkg errors tests
|
||||
if: steps.changes.outputs.pkg-errors == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
- name: Verify dependencies
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-errors.out ./pkg/errors/...
|
||||
go mod download
|
||||
go mod verify
|
||||
|
||||
- name: Run pkg events tests
|
||||
if: steps.changes.outputs.pkg-events == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
- name: Run ${{ matrix.module }} tests
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-events.out ./pkg/events/...
|
||||
# Map module names to directories
|
||||
case "${{ matrix.module }}" in
|
||||
pkg-errors) dir="pkg/errors" ;;
|
||||
pkg-events) dir="pkg/events" ;;
|
||||
pkg-grpcclient) dir="pkg/grpcclient" ;;
|
||||
pkg-messaging) dir="pkg/messaging" ;;
|
||||
pkg-sdk) dir="pkg/sdk" ;;
|
||||
pkg-transformers) dir="pkg/transformers" ;;
|
||||
pkg-ulid) dir="pkg/ulid" ;;
|
||||
pkg-uuid) dir="pkg/uuid" ;;
|
||||
*) dir="${{ matrix.module }}" ;;
|
||||
esac
|
||||
|
||||
- name: Run pkg grpcclient tests
|
||||
if: steps.changes.outputs.pkg-grpcclient == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-grpcclient.out ./pkg/grpcclient/...
|
||||
|
||||
- name: Run pkg messaging tests
|
||||
if: steps.changes.outputs.pkg-messaging == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-messaging.out ./pkg/messaging/...
|
||||
|
||||
- name: Run pkg sdk tests
|
||||
if: steps.changes.outputs.pkg-sdk == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-sdk.out ./pkg/sdk/...
|
||||
|
||||
- name: Run pkg transformers tests
|
||||
if: steps.changes.outputs.pkg-transformers == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-transformers.out ./pkg/transformers/...
|
||||
|
||||
- name: Run pkg ulid tests
|
||||
if: steps.changes.outputs.pkg-ulid == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-ulid.out ./pkg/ulid/...
|
||||
|
||||
- name: Run pkg uuid tests
|
||||
if: steps.changes.outputs.pkg-uuid == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-uuid.out ./pkg/uuid/...
|
||||
|
||||
- name: Run clients tests
|
||||
if: steps.changes.outputs.clients == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/clients.out ./clients/...
|
||||
|
||||
- name: Run channels tests
|
||||
if: steps.changes.outputs.channels == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/channels.out ./channels/...
|
||||
|
||||
- name: Run users tests
|
||||
if: steps.changes.outputs.users == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/users.out ./users/...
|
||||
|
||||
- name: Run groups tests
|
||||
if: steps.changes.outputs.groups == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/groups.out ./groups/...
|
||||
|
||||
- name: Run WebSocket tests
|
||||
if: steps.changes.outputs.ws == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/ws.out ./ws/...
|
||||
go test -mod=readonly --race -v -count=1 -failfast -coverprofile=coverage-${{ matrix.module }}.out ./$dir/...
|
||||
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: coverage-${{ matrix.module }}
|
||||
path: coverage-${{ matrix.module }}.out
|
||||
retention-days: 1
|
||||
|
||||
upload-coverage:
|
||||
name: Upload Coverage
|
||||
runs-on: ubuntu-latest
|
||||
needs: run-tests
|
||||
if: always() && needs.run-tests.result != 'cancelled'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: coverage-*
|
||||
path: coverage
|
||||
merge-multiple: true
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV }}
|
||||
directory: ./coverage
|
||||
files: ./coverage/*.out
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ hosted on the [Abstract Machines Organization](https://github.com/absmach) on Gi
|
||||
|
||||
This project adheres to the [Contributor Covenant 1.2](http://contributor-covenant.org/version/1/2/0).
|
||||
By participating, you are expected to uphold this code. Please report unacceptable behavior to
|
||||
[abuse@abstractmachines.fr](mailto:abuse@abstractmachines.fr).
|
||||
[abuse@absmach.eu](mailto:abuse@absmach.eu).
|
||||
|
||||
## Reporting issues
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015-2020 SuperMQ
|
||||
Copyright 2015-2026 SuperMQ
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
+11
-26
@@ -1,30 +1,15 @@
|
||||
# SuperMQ follows the timeless, highly efficient and totally unfair system
|
||||
# known as [Benevolent dictator for
|
||||
# life](https://en.wikipedia.org/wiki/Benevolent_Dictator_for_Life), with
|
||||
# Drasko DRASKOVIC in the role of BDFL.
|
||||
# SuperMQ Maintainers
|
||||
|
||||
[bdfl]
|
||||
|
||||
[[drasko]]
|
||||
Name = "Drasko Draskovic"
|
||||
Email = "drasko@abstractmachines.fr"
|
||||
GitHub = "drasko"
|
||||
SuperMQ follows a BDFL model for dead-lock situations; day-to-day decisions happen through discussion and pull requests.
|
||||
|
||||
# However, this role serves only in dead-lock events, or in a special and very rare cases
|
||||
# when BDFL completely disagrees with the decisions made.
|
||||
# In the normal flow of events, decisions on the project design are made through discussions,
|
||||
# most often on the Pull Requests.
|
||||
#
|
||||
# Maintainers have the special role in the project in managing and accepting PRs,
|
||||
# overall leading the project and making design decisions on the maintained subsystems.
|
||||
#
|
||||
# A reference list of all maintainers of the SuperMQ project.
|
||||
## BDFL
|
||||
|
||||
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
|
||||
- **Drasko Draskovic** — [drasko@absmach.eu](mailto:drasko@absmach.eu) — [GitHub: drasko](https://github.com/drasko)
|
||||
|
||||
[maintainers]
|
||||
|
||||
[[dusan]]
|
||||
Name = "Dusan Borovcanin"
|
||||
Email = "dusan.borovcanin@abstractmachines.fr"
|
||||
GitHub = "dborovcanin"
|
||||
## Maintainers
|
||||
|
||||
Add yourself below in alphabetical order.
|
||||
|
||||
| Name | Email | GitHub |
|
||||
| --- | --- | --- |
|
||||
| Dusan Borovcanin | [dusan.borovcanin@absmach.eu](mailto:dusan.borovcanin@absmach.eu) | [dborovcanin](https://github.com/dborovcanin) |
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
|
||||
SMQ_DOCKER_IMAGE_NAME_PREFIX ?= supermq
|
||||
BUILD_DIR ?= build
|
||||
SERVICES = auth users clients groups channels domains http coap ws cli mqtt journal
|
||||
SERVICES = auth users clients groups channels domains http coap cli mqtt journal notifications
|
||||
TEST_API_SERVICES = journal auth certs http clients users channels groups domains
|
||||
TEST_API = $(addprefix test_api_,$(TEST_API_SERVICES))
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
|
||||
CGO_ENABLED ?= 0
|
||||
GOARCH ?= amd64
|
||||
GOOS ?= linux
|
||||
DETECTED_ARCH := $(shell uname -m)
|
||||
VERSION ?= $(shell git describe --abbrev=0 --tags 2>/dev/null || echo 'unknown')
|
||||
COMMIT ?= $(shell git rev-parse HEAD)
|
||||
TIME ?= $(shell date +%F_%T)
|
||||
@@ -22,7 +24,7 @@ DOCKER_COMPOSE_COMMANDS_SUPPORTED := up down config restart
|
||||
DEFAULT_DOCKER_COMPOSE_COMMAND := up
|
||||
GRPC_MTLS_CERT_FILES_EXISTS = 0
|
||||
MOCKERY = $(GOBIN)/mockery
|
||||
MOCKERY_VERSION=3.5.5
|
||||
MOCKERY_VERSION=3.6.4
|
||||
PKG_PROTO_GEN_OUT_DIR=api/grpc
|
||||
INTERNAL_PROTO_DIR=internal/proto
|
||||
INTERNAL_PROTO_FILES := $(shell find $(INTERNAL_PROTO_DIR) -name "*.proto" | sed 's|$(INTERNAL_PROTO_DIR)/||')
|
||||
@@ -73,10 +75,50 @@ define make_docker_dev
|
||||
-f docker/Dockerfile.dev ./build
|
||||
endef
|
||||
|
||||
define run_with_arch_detection
|
||||
@echo "Detecting architecture..."
|
||||
@if [ "$(DETECTED_ARCH)" = "arm64" ] || [ "$(DETECTED_ARCH)" = "aarch64" ]; then \
|
||||
echo "ARM64 architecture detected."; \
|
||||
git checkout $(1); \
|
||||
GOARCH=arm64 $(MAKE) dockers; \
|
||||
for svc in $(SERVICES); do \
|
||||
docker tag supermq/$$svc supermq/$$svc:latest; \
|
||||
docker tag supermq/$$svc docker.io/supermq/$$svc:latest; \
|
||||
done; \
|
||||
sed -i.bak 's/^SMQ_RELEASE_TAG=.*/SMQ_RELEASE_TAG=latest/' docker/.env && rm -f docker/.env.bak; \
|
||||
docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args); \
|
||||
else \
|
||||
echo "x86_64 architecture detected."; \
|
||||
git checkout $(1); \
|
||||
sed -i.bak 's/^SMQ_RELEASE_TAG=.*/SMQ_RELEASE_TAG=$(2)/' docker/.env && rm -f docker/.env.bak; \
|
||||
docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args); \
|
||||
fi
|
||||
endef
|
||||
|
||||
ADDON_SERVICES = journal certs
|
||||
|
||||
EXTERNAL_SERVICES = prometheus
|
||||
|
||||
# Detect OS and architecture for cross-platform compatibility
|
||||
UNAME_S := $(shell uname -s)
|
||||
UNAME_M := $(shell uname -m)
|
||||
|
||||
# macOS BSD sed vs GNU sed compatibility
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED_INPLACE := sed -i ''
|
||||
else
|
||||
SED_INPLACE := sed -i
|
||||
endif
|
||||
|
||||
# Apple Silicon (arm64) Docker platform compatibility
|
||||
# Pre-built images are amd64 only, so we need to use emulation on Apple Silicon
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
ifeq ($(UNAME_M),arm64)
|
||||
DOCKER_PLATFORM := DOCKER_DEFAULT_PLATFORM=linux/amd64
|
||||
endif
|
||||
endif
|
||||
DOCKER_PLATFORM ?=
|
||||
|
||||
ifneq ($(filter run%,$(firstword $(MAKECMDGOALS))),)
|
||||
temp_args := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
||||
DOCKER_COMPOSE_COMMAND := $(if $(filter $(DOCKER_COMPOSE_COMMANDS_SUPPORTED),$(temp_args)), $(filter $(DOCKER_COMPOSE_COMMANDS_SUPPORTED),$(temp_args)), $(DEFAULT_DOCKER_COMPOSE_COMMAND))
|
||||
@@ -99,7 +141,7 @@ FILTERED_SERVICES = $(filter-out $(RUN_ADDON_ARGS), $(SERVICES))
|
||||
|
||||
all: $(SERVICES)
|
||||
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release run run_addons grpc_mtls_certs check_mtls check_certs test_api mocks
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release run_latest run_stable run_addons grpc_mtls_certs check_mtls check_certs test_api mocks
|
||||
|
||||
clean:
|
||||
rm -rf ${BUILD_DIR}
|
||||
@@ -123,11 +165,8 @@ mocks: $(MOCKERY)
|
||||
|
||||
$(MOCKERY):
|
||||
@mkdir -p $(GOBIN)
|
||||
@mkdir -p mockery
|
||||
@echo ">> downloading mockery $(MOCKERY_VERSION)..."
|
||||
@curl -sL https://github.com/vektra/mockery/releases/download/v$(MOCKERY_VERSION)/mockery_$(MOCKERY_VERSION)_Linux_x86_64.tar.gz | tar -xz -C mockery
|
||||
@mv mockery/mockery $(GOBIN)
|
||||
@rm -r mockery
|
||||
@echo ">> installing mockery $(MOCKERY_VERSION)..."
|
||||
@go install github.com/vektra/mockery/v3@v$(MOCKERY_VERSION)
|
||||
|
||||
DIRS = consumers readers postgres internal
|
||||
test: mocks
|
||||
@@ -215,6 +254,15 @@ changelog:
|
||||
latest: dockers
|
||||
$(call docker_push,latest)
|
||||
|
||||
publish_arch:
|
||||
$(MAKE) dockers GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
|
||||
for svc in $(SERVICES); do \
|
||||
docker tag $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(VERSION)-$(GOARCH); \
|
||||
docker tag $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc:latest-$(GOARCH); \
|
||||
docker push $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(VERSION)-$(GOARCH); \
|
||||
docker push $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc:latest-$(GOARCH); \
|
||||
done
|
||||
|
||||
release:
|
||||
$(eval version = $(shell git describe --abbrev=0 --tags))
|
||||
git checkout $(version)
|
||||
@@ -258,19 +306,27 @@ endif
|
||||
fetch_certs:
|
||||
@./scripts/certs.sh
|
||||
|
||||
run: check_certs
|
||||
docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
run_latest: check_certs
|
||||
git checkout main
|
||||
$(SED_INPLACE) 's/^SMQ_RELEASE_TAG=.*/SMQ_RELEASE_TAG=latest/' docker/.env
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
run_stable: check_certs
|
||||
$(eval version = $(shell git describe --abbrev=0 --tags))
|
||||
git checkout $(version)
|
||||
$(SED_INPLACE) 's/^SMQ_RELEASE_TAG=.*/SMQ_RELEASE_TAG=$(version)/' docker/.env
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
run_addons: check_certs
|
||||
$(foreach SVC,$(RUN_ADDON_ARGS),$(if $(filter $(SVC),$(ADDON_SERVICES) $(EXTERNAL_SERVICES)),,$(error Invalid Service $(SVC))))
|
||||
@docker compose -f docker/docker-compose.yaml --env-file ./docker/.env -p $(DOCKER_PROJECT) up -d auth domains jaeger
|
||||
@$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file ./docker/.env -p $(DOCKER_PROJECT) up -d auth domains jaeger
|
||||
@for SVC in $(RUN_ADDON_ARGS); do \
|
||||
if [ "$$SVC" = "certs" ]; then \
|
||||
docker compose -f docker/addons/$$SVC/docker-compose.yaml -f docker/certs-docker-compose-override.yaml --env-file ./docker/.env --env-file ./docker/addons/$$SVC/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args) & \
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/addons/$$SVC/docker-compose.yaml -f docker/certs-docker-compose-override.yaml --env-file ./docker/.env --env-file ./docker/addons/$$SVC/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args) & \
|
||||
else \
|
||||
SMQ_ADDONS_CERTS_PATH_PREFIX="../." docker compose -f docker/addons/$$SVC/docker-compose.yaml -p $(DOCKER_PROJECT) --env-file ./docker/.env $(DOCKER_COMPOSE_COMMAND) $(args) & \
|
||||
SMQ_ADDONS_CERTS_PATH_PREFIX="../." $(DOCKER_PLATFORM) docker compose -f docker/addons/$$SVC/docker-compose.yaml -p $(DOCKER_PROJECT) --env-file ./docker/.env $(DOCKER_COMPOSE_COMMAND) $(args) & \
|
||||
fi; \
|
||||
done
|
||||
|
||||
run_live: check_certs
|
||||
GOPATH=$(go env GOPATH) docker compose -f docker/docker-compose.yaml -f docker/docker-compose-live.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
GOPATH=$(go env GOPATH) $(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml -f docker/docker-compose-live.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
<div align="center">
|
||||
|
||||
# SuperMQ
|
||||
# SuperMQ
|
||||
|
||||
**Planetary event-driven infrastructure**
|
||||
### Planetary event-driven infrastructure
|
||||
|
||||
**Made with ❤️ by [Abstract Machines](https://abstractmachines.fr/)**
|
||||
**Made with ❤️ by [Abstract Machines](https://absmach.eu/)**
|
||||
|
||||
[](https://github.com/absmach/supermq/actions/workflows/build.yaml)
|
||||
[](https://goreportcard.com/report/github.com/absmach/supermq)
|
||||
[](https://deepwiki.com/absmach/supermq)
|
||||
[](https://github.com/absmach/supermq/actions/workflows/check-license.yaml)
|
||||
[](https://github.com/absmach/supermq/actions/workflows/check-generated-files.yaml)
|
||||
[](https://codecov.io/gh/absmach/supermq)
|
||||
[](LICENSE)
|
||||
[](https://matrix.to/#/#supermq:matrix.org)
|
||||
|
||||
[](https://github.com/absmach/supermq/actions/workflows/build.yaml)
|
||||
[](https://github.com/absmach/supermq/actions/workflows/check-license.yaml)
|
||||
[](https://github.com/absmach/supermq/actions/workflows/check-generated-files.yaml)
|
||||
[](https://goreportcard.com/report/github.com/absmach/supermq)
|
||||
[](https://codecov.io/gh/absmach/supermq)
|
||||
[](LICENSE)
|
||||
[](https://matrix.to/#/#supermq:matrix.org)
|
||||
|
||||
### [Guide](https://docs.supermq.abstractmachines.fr) | [Contributing](CONTRIBUTING.md) | [Website](https://abstractmachines.fr/) | [Chat](https://matrix.to/#/#supermq:matrix.org)
|
||||
### [Guide](https://docs.supermq.absmach.eu) | [Contributing](CONTRIBUTING.md) | [Website](https://absmach.eu/) | [Chat](https://matrix.to/#/#supermq:matrix.org)
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
## Introduction 📖
|
||||
|
||||
SuperMQ is a distributed, highly scalable, and secure open-source cloud platform for messaging and event-driven architecture (EDA). It is a planetarily distributed, highly scalable, and secure platform that serves as a robust foundation for building advanced real-time and reactive systems.
|
||||
@@ -46,25 +45,62 @@ SuperMQ bridges the gap between various network protocols (HTTP, MQTT, WebSocket
|
||||
|
||||
## Installation 🛠️
|
||||
|
||||
Clone the repository and start SuperMQ services:
|
||||
There are multiple ways to run SuperMQ.
|
||||
First, clone the repository and position to it:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/absmach/supermq.git
|
||||
cd supermq
|
||||
```
|
||||
|
||||
To run the latest stable (tagged) version, use:
|
||||
|
||||
```bash
|
||||
# Run with latest stable tagged version
|
||||
make run_stable
|
||||
```
|
||||
|
||||
To run the latest version, use:
|
||||
|
||||
```bash
|
||||
# Run with latest development version (from main branch)
|
||||
make run_latest
|
||||
```
|
||||
|
||||
The `make run_stable` command will:
|
||||
- Checkout the repository to the latest git tag
|
||||
- Update the version in the environment configuration
|
||||
- Start the services with the stable release
|
||||
|
||||
**Note:** After running `make run_stable`, you'll be on a detached HEAD state. To return to your working branch:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
```
|
||||
|
||||
### Running on Apple Silicon (M1/M2/M3) Macs
|
||||
|
||||
When running SuperMQ on Apple Silicon Macs, the Makefile will automatically detect your ARM64 architecture and build Docker images locally.
|
||||
|
||||
**If using Docker Desktop:**
|
||||
|
||||
1. **Enable Apple Virtualization Framework**: In Docker Desktop, go to:
|
||||
- Settings → General → Enable "Use the new Virtualization framework"
|
||||
|
||||
2. **Enable Rosetta for x86_64 Emulation**: In Docker Desktop, go to:
|
||||
- Settings → General → Enable "Use Rosetta for x86_64/amd64 emulation on Apple Silicon"
|
||||
|
||||
After enabling these options, restart Docker Desktop, then run `make run_stable` or `make run_latest` as usual.
|
||||
|
||||
To manually run SuperMQ, clone the repository and start all core services:
|
||||
|
||||
```bash
|
||||
docker compose -f docker/docker-compose.yaml --env-file docker/.env up
|
||||
```
|
||||
|
||||
Or use the [Makefile](Makefile) for a simpler command:
|
||||
|
||||
```bash
|
||||
make run
|
||||
```
|
||||
|
||||
For production deployments, check our [Kubernetes guide](https://docs.supermq.abstractmachines.fr/kubernetes). ⚙️
|
||||
|
||||
### Usage 📤📥
|
||||
|
||||
#### Using the CLI:
|
||||
**Using the CLI :**
|
||||
|
||||
```bash
|
||||
make cli
|
||||
@@ -73,7 +109,7 @@ make cli
|
||||
|
||||
This command retrieves the status of the SuperMQ server and outputs it to the console.
|
||||
|
||||
#### Using HTTP with Curl:
|
||||
**Using HTTP with Curl :**
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8080/status
|
||||
@@ -81,11 +117,11 @@ curl -X GET http://localhost:8080/status
|
||||
|
||||
This request fetches the server status over HTTP and provides a JSON response.
|
||||
|
||||
See our [CLI documentation](https://docs.supermq.abstractmachines.fr/cli) for more details.
|
||||
See our [CLI documentation](https://docs.supermq.absmach.eu/cli) for more details.
|
||||
|
||||
## Documentation 📚
|
||||
|
||||
The official documentation is hosted at [SuperMQ docs page](https://docs.supermq.abstractmachines.fr).
|
||||
The official documentation is hosted at [SuperMQ docs page](https://docs.supermq.absmach.eu/).
|
||||
|
||||
Documentation is auto-generated, check out the instructions in the [docs repository](https://github.com/absmach/supermq-docs).
|
||||
If you spot an error or a need for corrections, please let us know - or even better: send us a PR! 💌
|
||||
@@ -104,7 +140,7 @@ Join our community:
|
||||
|
||||
## Professional Support 💼
|
||||
|
||||
Need help deploying SuperMQ or integrating it into your system? Reach out to **[Abstract Machines](https://abstractmachines.fr/)** for professional support and guidance.
|
||||
Need help deploying SuperMQ or integrating it into your system? Reach out to **[Abstract Machines](https://absmach.eu/)** for professional support and guidance.
|
||||
|
||||
## License 📜
|
||||
|
||||
@@ -115,4 +151,3 @@ SuperMQ is open-source software licensed under the [Apache License 2.0](LICENSE)
|
||||
Special thanks to the amazing contributors who make SuperMQ possible. Check out the [MAINTAINERS](MAINTAINERS) file to see the team behind the magic.
|
||||
|
||||
Ready to build the future of messaging and event-driven systems? Let's get started! 🚀
|
||||
|
||||
|
||||
+252
-196
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.10
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.0
|
||||
// source: auth/v1/auth.proto
|
||||
|
||||
@@ -70,10 +70,10 @@ func (x *AuthNReq) GetToken() string {
|
||||
|
||||
type AuthNRes struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // token id
|
||||
UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // user id
|
||||
UserRole uint32 `protobuf:"varint,3,opt,name=user_role,json=userRole,proto3" json:"user_role,omitempty"` // user role
|
||||
Verified bool `protobuf:"varint,4,opt,name=verified,proto3" json:"verified,omitempty"` // verified user
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
UserRole uint32 `protobuf:"varint,3,opt,name=user_role,json=userRole,proto3" json:"user_role,omitempty"`
|
||||
Verified bool `protobuf:"varint,4,opt,name=verified,proto3" json:"verified,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -136,24 +136,209 @@ func (x *AuthNRes) GetVerified() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type AuthZReq struct {
|
||||
type PolicyReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` // Domain
|
||||
SubjectType string `protobuf:"bytes,2,opt,name=subject_type,json=subjectType,proto3" json:"subject_type,omitempty"` // Client or User
|
||||
SubjectKind string `protobuf:"bytes,3,opt,name=subject_kind,json=subjectKind,proto3" json:"subject_kind,omitempty"` // ID or Token
|
||||
SubjectRelation string `protobuf:"bytes,4,opt,name=subject_relation,json=subjectRelation,proto3" json:"subject_relation,omitempty"` // Subject relation
|
||||
Subject string `protobuf:"bytes,5,opt,name=subject,proto3" json:"subject,omitempty"` // Subject value (id or token, depending on kind)
|
||||
Relation string `protobuf:"bytes,6,opt,name=relation,proto3" json:"relation,omitempty"` // Relation to filter
|
||||
Permission string `protobuf:"bytes,7,opt,name=permission,proto3" json:"permission,omitempty"` // Action
|
||||
Object string `protobuf:"bytes,8,opt,name=object,proto3" json:"object,omitempty"` // Object ID
|
||||
ObjectType string `protobuf:"bytes,9,opt,name=object_type,json=objectType,proto3" json:"object_type,omitempty"` // Client, User, Group
|
||||
Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"`
|
||||
SubjectType string `protobuf:"bytes,2,opt,name=subject_type,json=subjectType,proto3" json:"subject_type,omitempty"`
|
||||
SubjectKind string `protobuf:"bytes,3,opt,name=subject_kind,json=subjectKind,proto3" json:"subject_kind,omitempty"`
|
||||
SubjectRelation string `protobuf:"bytes,4,opt,name=subject_relation,json=subjectRelation,proto3" json:"subject_relation,omitempty"`
|
||||
Subject string `protobuf:"bytes,5,opt,name=subject,proto3" json:"subject,omitempty"`
|
||||
Relation string `protobuf:"bytes,6,opt,name=relation,proto3" json:"relation,omitempty"`
|
||||
Permission string `protobuf:"bytes,7,opt,name=permission,proto3" json:"permission,omitempty"`
|
||||
Object string `protobuf:"bytes,8,opt,name=object,proto3" json:"object,omitempty"`
|
||||
ObjectType string `protobuf:"bytes,9,opt,name=object_type,json=objectType,proto3" json:"object_type,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PolicyReq) Reset() {
|
||||
*x = PolicyReq{}
|
||||
mi := &file_auth_v1_auth_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PolicyReq) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PolicyReq) ProtoMessage() {}
|
||||
|
||||
func (x *PolicyReq) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_v1_auth_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use PolicyReq.ProtoReflect.Descriptor instead.
|
||||
func (*PolicyReq) Descriptor() ([]byte, []int) {
|
||||
return file_auth_v1_auth_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *PolicyReq) GetDomain() string {
|
||||
if x != nil {
|
||||
return x.Domain
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PolicyReq) GetSubjectType() string {
|
||||
if x != nil {
|
||||
return x.SubjectType
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PolicyReq) GetSubjectKind() string {
|
||||
if x != nil {
|
||||
return x.SubjectKind
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PolicyReq) GetSubjectRelation() string {
|
||||
if x != nil {
|
||||
return x.SubjectRelation
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PolicyReq) GetSubject() string {
|
||||
if x != nil {
|
||||
return x.Subject
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PolicyReq) GetRelation() string {
|
||||
if x != nil {
|
||||
return x.Relation
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PolicyReq) GetPermission() string {
|
||||
if x != nil {
|
||||
return x.Permission
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PolicyReq) GetObject() string {
|
||||
if x != nil {
|
||||
return x.Object
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PolicyReq) GetObjectType() string {
|
||||
if x != nil {
|
||||
return x.ObjectType
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type PATReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
PatId string `protobuf:"bytes,1,opt,name=pat_id,json=patId,proto3" json:"pat_id,omitempty"`
|
||||
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
|
||||
Operation string `protobuf:"bytes,3,opt,name=operation,proto3" json:"operation,omitempty"`
|
||||
UserId string `protobuf:"bytes,4,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
EntityId string `protobuf:"bytes,5,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"`
|
||||
EntityType string `protobuf:"bytes,6,opt,name=entity_type,json=entityType,proto3" json:"entity_type,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PATReq) Reset() {
|
||||
*x = PATReq{}
|
||||
mi := &file_auth_v1_auth_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PATReq) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PATReq) ProtoMessage() {}
|
||||
|
||||
func (x *PATReq) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_v1_auth_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use PATReq.ProtoReflect.Descriptor instead.
|
||||
func (*PATReq) Descriptor() ([]byte, []int) {
|
||||
return file_auth_v1_auth_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *PATReq) GetPatId() string {
|
||||
if x != nil {
|
||||
return x.PatId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PATReq) GetDomain() string {
|
||||
if x != nil {
|
||||
return x.Domain
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PATReq) GetOperation() string {
|
||||
if x != nil {
|
||||
return x.Operation
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PATReq) GetUserId() string {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PATReq) GetEntityId() string {
|
||||
if x != nil {
|
||||
return x.EntityId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PATReq) GetEntityType() string {
|
||||
if x != nil {
|
||||
return x.EntityType
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type AuthZReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
PolicyReq *PolicyReq `protobuf:"bytes,1,opt,name=policy_req,json=policyReq,proto3" json:"policy_req,omitempty"`
|
||||
PatReq *PATReq `protobuf:"bytes,2,opt,name=pat_req,json=patReq,proto3,oneof" json:"pat_req,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AuthZReq) Reset() {
|
||||
*x = AuthZReq{}
|
||||
mi := &file_auth_v1_auth_proto_msgTypes[2]
|
||||
mi := &file_auth_v1_auth_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -165,7 +350,7 @@ func (x *AuthZReq) String() string {
|
||||
func (*AuthZReq) ProtoMessage() {}
|
||||
|
||||
func (x *AuthZReq) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_v1_auth_proto_msgTypes[2]
|
||||
mi := &file_auth_v1_auth_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -178,154 +363,21 @@ func (x *AuthZReq) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use AuthZReq.ProtoReflect.Descriptor instead.
|
||||
func (*AuthZReq) Descriptor() ([]byte, []int) {
|
||||
return file_auth_v1_auth_proto_rawDescGZIP(), []int{2}
|
||||
return file_auth_v1_auth_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *AuthZReq) GetDomain() string {
|
||||
func (x *AuthZReq) GetPolicyReq() *PolicyReq {
|
||||
if x != nil {
|
||||
return x.Domain
|
||||
return x.PolicyReq
|
||||
}
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AuthZReq) GetSubjectType() string {
|
||||
func (x *AuthZReq) GetPatReq() *PATReq {
|
||||
if x != nil {
|
||||
return x.SubjectType
|
||||
return x.PatReq
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthZReq) GetSubjectKind() string {
|
||||
if x != nil {
|
||||
return x.SubjectKind
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthZReq) GetSubjectRelation() string {
|
||||
if x != nil {
|
||||
return x.SubjectRelation
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthZReq) GetSubject() string {
|
||||
if x != nil {
|
||||
return x.Subject
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthZReq) GetRelation() string {
|
||||
if x != nil {
|
||||
return x.Relation
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthZReq) GetPermission() string {
|
||||
if x != nil {
|
||||
return x.Permission
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthZReq) GetObject() string {
|
||||
if x != nil {
|
||||
return x.Object
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthZReq) GetObjectType() string {
|
||||
if x != nil {
|
||||
return x.ObjectType
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type AuthZPatReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // User id
|
||||
PatId string `protobuf:"bytes,2,opt,name=pat_id,json=patId,proto3" json:"pat_id,omitempty"` // Pat id
|
||||
EntityType uint32 `protobuf:"varint,3,opt,name=entity_type,json=entityType,proto3" json:"entity_type,omitempty"` // Entity type
|
||||
OptionalDomainId string `protobuf:"bytes,4,opt,name=optional_domain_id,json=optionalDomainId,proto3" json:"optional_domain_id,omitempty"` // Optional domain id
|
||||
Operation uint32 `protobuf:"varint,6,opt,name=operation,proto3" json:"operation,omitempty"` // Operation
|
||||
EntityId string `protobuf:"bytes,7,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` // EntityID
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AuthZPatReq) Reset() {
|
||||
*x = AuthZPatReq{}
|
||||
mi := &file_auth_v1_auth_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuthZPatReq) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthZPatReq) ProtoMessage() {}
|
||||
|
||||
func (x *AuthZPatReq) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_v1_auth_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuthZPatReq.ProtoReflect.Descriptor instead.
|
||||
func (*AuthZPatReq) Descriptor() ([]byte, []int) {
|
||||
return file_auth_v1_auth_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *AuthZPatReq) GetUserId() string {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthZPatReq) GetPatId() string {
|
||||
if x != nil {
|
||||
return x.PatId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthZPatReq) GetEntityType() uint32 {
|
||||
if x != nil {
|
||||
return x.EntityType
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *AuthZPatReq) GetOptionalDomainId() string {
|
||||
if x != nil {
|
||||
return x.OptionalDomainId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthZPatReq) GetOperation() uint32 {
|
||||
if x != nil {
|
||||
return x.Operation
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *AuthZPatReq) GetEntityId() string {
|
||||
if x != nil {
|
||||
return x.EntityId
|
||||
}
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
type AuthZRes struct {
|
||||
@@ -338,7 +390,7 @@ type AuthZRes struct {
|
||||
|
||||
func (x *AuthZRes) Reset() {
|
||||
*x = AuthZRes{}
|
||||
mi := &file_auth_v1_auth_proto_msgTypes[4]
|
||||
mi := &file_auth_v1_auth_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -350,7 +402,7 @@ func (x *AuthZRes) String() string {
|
||||
func (*AuthZRes) ProtoMessage() {}
|
||||
|
||||
func (x *AuthZRes) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_v1_auth_proto_msgTypes[4]
|
||||
mi := &file_auth_v1_auth_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -363,7 +415,7 @@ func (x *AuthZRes) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use AuthZRes.ProtoReflect.Descriptor instead.
|
||||
func (*AuthZRes) Descriptor() ([]byte, []int) {
|
||||
return file_auth_v1_auth_proto_rawDescGZIP(), []int{4}
|
||||
return file_auth_v1_auth_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *AuthZRes) GetAuthorized() bool {
|
||||
@@ -391,8 +443,8 @@ const file_auth_v1_auth_proto_rawDesc = "" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x12\x17\n" +
|
||||
"\auser_id\x18\x02 \x01(\tR\x06userId\x12\x1b\n" +
|
||||
"\tuser_role\x18\x03 \x01(\rR\buserRole\x12\x1a\n" +
|
||||
"\bverified\x18\x04 \x01(\bR\bverified\"\xa2\x02\n" +
|
||||
"\bAuthZReq\x12\x16\n" +
|
||||
"\bverified\x18\x04 \x01(\bR\bverified\"\xa3\x02\n" +
|
||||
"\tPolicyReq\x12\x16\n" +
|
||||
"\x06domain\x18\x01 \x01(\tR\x06domain\x12!\n" +
|
||||
"\fsubject_type\x18\x02 \x01(\tR\vsubjectType\x12!\n" +
|
||||
"\fsubject_kind\x18\x03 \x01(\tR\vsubjectKind\x12)\n" +
|
||||
@@ -404,25 +456,29 @@ const file_auth_v1_auth_proto_rawDesc = "" +
|
||||
"permission\x12\x16\n" +
|
||||
"\x06object\x18\b \x01(\tR\x06object\x12\x1f\n" +
|
||||
"\vobject_type\x18\t \x01(\tR\n" +
|
||||
"objectType\"\xc7\x01\n" +
|
||||
"\vAuthZPatReq\x12\x17\n" +
|
||||
"\auser_id\x18\x01 \x01(\tR\x06userId\x12\x15\n" +
|
||||
"\x06pat_id\x18\x02 \x01(\tR\x05patId\x12\x1f\n" +
|
||||
"\ventity_type\x18\x03 \x01(\rR\n" +
|
||||
"entityType\x12,\n" +
|
||||
"\x12optional_domain_id\x18\x04 \x01(\tR\x10optionalDomainId\x12\x1c\n" +
|
||||
"\toperation\x18\x06 \x01(\rR\toperation\x12\x1b\n" +
|
||||
"\tentity_id\x18\a \x01(\tR\bentityId\":\n" +
|
||||
"objectType\"\xac\x01\n" +
|
||||
"\x06PATReq\x12\x15\n" +
|
||||
"\x06pat_id\x18\x01 \x01(\tR\x05patId\x12\x16\n" +
|
||||
"\x06domain\x18\x02 \x01(\tR\x06domain\x12\x1c\n" +
|
||||
"\toperation\x18\x03 \x01(\tR\toperation\x12\x17\n" +
|
||||
"\auser_id\x18\x04 \x01(\tR\x06userId\x12\x1b\n" +
|
||||
"\tentity_id\x18\x05 \x01(\tR\bentityId\x12\x1f\n" +
|
||||
"\ventity_type\x18\x06 \x01(\tR\n" +
|
||||
"entityType\"x\n" +
|
||||
"\bAuthZReq\x121\n" +
|
||||
"\n" +
|
||||
"policy_req\x18\x01 \x01(\v2\x12.auth.v1.PolicyReqR\tpolicyReq\x12-\n" +
|
||||
"\apat_req\x18\x02 \x01(\v2\x0f.auth.v1.PATReqH\x00R\x06patReq\x88\x01\x01B\n" +
|
||||
"\n" +
|
||||
"\b_pat_req\":\n" +
|
||||
"\bAuthZRes\x12\x1e\n" +
|
||||
"\n" +
|
||||
"authorized\x18\x01 \x01(\bR\n" +
|
||||
"authorized\x12\x0e\n" +
|
||||
"\x02id\x18\x02 \x01(\tR\x02id2\xf0\x01\n" +
|
||||
"\x02id\x18\x02 \x01(\tR\x02id2z\n" +
|
||||
"\vAuthService\x123\n" +
|
||||
"\tAuthorize\x12\x11.auth.v1.AuthZReq\x1a\x11.auth.v1.AuthZRes\"\x00\x129\n" +
|
||||
"\fAuthorizePAT\x12\x14.auth.v1.AuthZPatReq\x1a\x11.auth.v1.AuthZRes\"\x00\x126\n" +
|
||||
"\fAuthenticate\x12\x11.auth.v1.AuthNReq\x1a\x11.auth.v1.AuthNRes\"\x00\x129\n" +
|
||||
"\x0fAuthenticatePAT\x12\x11.auth.v1.AuthNReq\x1a\x11.auth.v1.AuthNRes\"\x00B-Z+github.com/absmach/supermq/api/grpc/auth/v1b\x06proto3"
|
||||
"\tAuthorize\x12\x11.auth.v1.AuthZReq\x1a\x11.auth.v1.AuthZRes\"\x00\x126\n" +
|
||||
"\fAuthenticate\x12\x11.auth.v1.AuthNReq\x1a\x11.auth.v1.AuthNRes\"\x00B-Z+github.com/absmach/supermq/api/grpc/auth/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_auth_v1_auth_proto_rawDescOnce sync.Once
|
||||
@@ -436,28 +492,27 @@ func file_auth_v1_auth_proto_rawDescGZIP() []byte {
|
||||
return file_auth_v1_auth_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_auth_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
|
||||
var file_auth_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||
var file_auth_v1_auth_proto_goTypes = []any{
|
||||
(*AuthNReq)(nil), // 0: auth.v1.AuthNReq
|
||||
(*AuthNRes)(nil), // 1: auth.v1.AuthNRes
|
||||
(*AuthZReq)(nil), // 2: auth.v1.AuthZReq
|
||||
(*AuthZPatReq)(nil), // 3: auth.v1.AuthZPatReq
|
||||
(*AuthZRes)(nil), // 4: auth.v1.AuthZRes
|
||||
(*AuthNReq)(nil), // 0: auth.v1.AuthNReq
|
||||
(*AuthNRes)(nil), // 1: auth.v1.AuthNRes
|
||||
(*PolicyReq)(nil), // 2: auth.v1.PolicyReq
|
||||
(*PATReq)(nil), // 3: auth.v1.PATReq
|
||||
(*AuthZReq)(nil), // 4: auth.v1.AuthZReq
|
||||
(*AuthZRes)(nil), // 5: auth.v1.AuthZRes
|
||||
}
|
||||
var file_auth_v1_auth_proto_depIdxs = []int32{
|
||||
2, // 0: auth.v1.AuthService.Authorize:input_type -> auth.v1.AuthZReq
|
||||
3, // 1: auth.v1.AuthService.AuthorizePAT:input_type -> auth.v1.AuthZPatReq
|
||||
0, // 2: auth.v1.AuthService.Authenticate:input_type -> auth.v1.AuthNReq
|
||||
0, // 3: auth.v1.AuthService.AuthenticatePAT:input_type -> auth.v1.AuthNReq
|
||||
4, // 4: auth.v1.AuthService.Authorize:output_type -> auth.v1.AuthZRes
|
||||
4, // 5: auth.v1.AuthService.AuthorizePAT:output_type -> auth.v1.AuthZRes
|
||||
1, // 6: auth.v1.AuthService.Authenticate:output_type -> auth.v1.AuthNRes
|
||||
1, // 7: auth.v1.AuthService.AuthenticatePAT:output_type -> auth.v1.AuthNRes
|
||||
4, // [4:8] is the sub-list for method output_type
|
||||
0, // [0:4] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
2, // 0: auth.v1.AuthZReq.policy_req:type_name -> auth.v1.PolicyReq
|
||||
3, // 1: auth.v1.AuthZReq.pat_req:type_name -> auth.v1.PATReq
|
||||
4, // 2: auth.v1.AuthService.Authorize:input_type -> auth.v1.AuthZReq
|
||||
0, // 3: auth.v1.AuthService.Authenticate:input_type -> auth.v1.AuthNReq
|
||||
5, // 4: auth.v1.AuthService.Authorize:output_type -> auth.v1.AuthZRes
|
||||
1, // 5: auth.v1.AuthService.Authenticate:output_type -> auth.v1.AuthNRes
|
||||
4, // [4:6] is the sub-list for method output_type
|
||||
2, // [2:4] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_auth_v1_auth_proto_init() }
|
||||
@@ -465,13 +520,14 @@ func file_auth_v1_auth_proto_init() {
|
||||
if File_auth_v1_auth_proto != nil {
|
||||
return
|
||||
}
|
||||
file_auth_v1_auth_proto_msgTypes[4].OneofWrappers = []any{}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_auth_v1_auth_proto_rawDesc), len(file_auth_v1_auth_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 5,
|
||||
NumMessages: 6,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: auth/v1/auth.proto
|
||||
|
||||
@@ -22,10 +22,8 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
AuthService_Authorize_FullMethodName = "/auth.v1.AuthService/Authorize"
|
||||
AuthService_AuthorizePAT_FullMethodName = "/auth.v1.AuthService/AuthorizePAT"
|
||||
AuthService_Authenticate_FullMethodName = "/auth.v1.AuthService/Authenticate"
|
||||
AuthService_AuthenticatePAT_FullMethodName = "/auth.v1.AuthService/AuthenticatePAT"
|
||||
AuthService_Authorize_FullMethodName = "/auth.v1.AuthService/Authorize"
|
||||
AuthService_Authenticate_FullMethodName = "/auth.v1.AuthService/Authenticate"
|
||||
)
|
||||
|
||||
// AuthServiceClient is the client API for AuthService service.
|
||||
@@ -36,9 +34,7 @@ const (
|
||||
// and authorization functionalities for SuperMQ services.
|
||||
type AuthServiceClient interface {
|
||||
Authorize(ctx context.Context, in *AuthZReq, opts ...grpc.CallOption) (*AuthZRes, error)
|
||||
AuthorizePAT(ctx context.Context, in *AuthZPatReq, opts ...grpc.CallOption) (*AuthZRes, error)
|
||||
Authenticate(ctx context.Context, in *AuthNReq, opts ...grpc.CallOption) (*AuthNRes, error)
|
||||
AuthenticatePAT(ctx context.Context, in *AuthNReq, opts ...grpc.CallOption) (*AuthNRes, error)
|
||||
}
|
||||
|
||||
type authServiceClient struct {
|
||||
@@ -59,16 +55,6 @@ func (c *authServiceClient) Authorize(ctx context.Context, in *AuthZReq, opts ..
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authServiceClient) AuthorizePAT(ctx context.Context, in *AuthZPatReq, opts ...grpc.CallOption) (*AuthZRes, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AuthZRes)
|
||||
err := c.cc.Invoke(ctx, AuthService_AuthorizePAT_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authServiceClient) Authenticate(ctx context.Context, in *AuthNReq, opts ...grpc.CallOption) (*AuthNRes, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AuthNRes)
|
||||
@@ -79,16 +65,6 @@ func (c *authServiceClient) Authenticate(ctx context.Context, in *AuthNReq, opts
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authServiceClient) AuthenticatePAT(ctx context.Context, in *AuthNReq, opts ...grpc.CallOption) (*AuthNRes, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AuthNRes)
|
||||
err := c.cc.Invoke(ctx, AuthService_AuthenticatePAT_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AuthServiceServer is the server API for AuthService service.
|
||||
// All implementations must embed UnimplementedAuthServiceServer
|
||||
// for forward compatibility.
|
||||
@@ -97,9 +73,7 @@ func (c *authServiceClient) AuthenticatePAT(ctx context.Context, in *AuthNReq, o
|
||||
// and authorization functionalities for SuperMQ services.
|
||||
type AuthServiceServer interface {
|
||||
Authorize(context.Context, *AuthZReq) (*AuthZRes, error)
|
||||
AuthorizePAT(context.Context, *AuthZPatReq) (*AuthZRes, error)
|
||||
Authenticate(context.Context, *AuthNReq) (*AuthNRes, error)
|
||||
AuthenticatePAT(context.Context, *AuthNReq) (*AuthNRes, error)
|
||||
mustEmbedUnimplementedAuthServiceServer()
|
||||
}
|
||||
|
||||
@@ -111,16 +85,10 @@ type AuthServiceServer interface {
|
||||
type UnimplementedAuthServiceServer struct{}
|
||||
|
||||
func (UnimplementedAuthServiceServer) Authorize(context.Context, *AuthZReq) (*AuthZRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Authorize not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) AuthorizePAT(context.Context, *AuthZPatReq) (*AuthZRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AuthorizePAT not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method Authorize not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) Authenticate(context.Context, *AuthNReq) (*AuthNRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Authenticate not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) AuthenticatePAT(context.Context, *AuthNReq) (*AuthNRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AuthenticatePAT not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method Authenticate not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {}
|
||||
func (UnimplementedAuthServiceServer) testEmbeddedByValue() {}
|
||||
@@ -133,7 +101,7 @@ type UnsafeAuthServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedAuthServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedAuthServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@@ -161,24 +129,6 @@ func _AuthService_Authorize_Handler(srv interface{}, ctx context.Context, dec fu
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AuthService_AuthorizePAT_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AuthZPatReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthServiceServer).AuthorizePAT(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AuthService_AuthorizePAT_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthServiceServer).AuthorizePAT(ctx, req.(*AuthZPatReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AuthService_Authenticate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AuthNReq)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -197,24 +147,6 @@ func _AuthService_Authenticate_Handler(srv interface{}, ctx context.Context, dec
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AuthService_AuthenticatePAT_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AuthNReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthServiceServer).AuthenticatePAT(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AuthService_AuthenticatePAT_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthServiceServer).AuthenticatePAT(ctx, req.(*AuthNReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -226,18 +158,10 @@ var AuthService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "Authorize",
|
||||
Handler: _AuthService_Authorize_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AuthorizePAT",
|
||||
Handler: _AuthService_AuthorizePAT_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Authenticate",
|
||||
Handler: _AuthService_Authenticate_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AuthenticatePAT",
|
||||
Handler: _AuthService_AuthenticatePAT_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "auth/v1/auth.proto",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.10
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.0
|
||||
// source: channels/v1/channels.proto
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: channels/v1/channels.proto
|
||||
|
||||
@@ -119,19 +119,19 @@ type ChannelsServiceServer interface {
|
||||
type UnimplementedChannelsServiceServer struct{}
|
||||
|
||||
func (UnimplementedChannelsServiceServer) Authorize(context.Context, *AuthzReq) (*AuthzRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Authorize not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method Authorize not implemented")
|
||||
}
|
||||
func (UnimplementedChannelsServiceServer) RemoveClientConnections(context.Context, *RemoveClientConnectionsReq) (*RemoveClientConnectionsRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveClientConnections not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RemoveClientConnections not implemented")
|
||||
}
|
||||
func (UnimplementedChannelsServiceServer) UnsetParentGroupFromChannels(context.Context, *UnsetParentGroupFromChannelsReq) (*UnsetParentGroupFromChannelsRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UnsetParentGroupFromChannels not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UnsetParentGroupFromChannels not implemented")
|
||||
}
|
||||
func (UnimplementedChannelsServiceServer) RetrieveEntity(context.Context, *v1.RetrieveEntityReq) (*v1.RetrieveEntityRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RetrieveEntity not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RetrieveEntity not implemented")
|
||||
}
|
||||
func (UnimplementedChannelsServiceServer) RetrieveIDByRoute(context.Context, *v1.RetrieveIDByRouteReq) (*v1.RetrieveEntityRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RetrieveIDByRoute not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RetrieveIDByRoute not implemented")
|
||||
}
|
||||
func (UnimplementedChannelsServiceServer) mustEmbedUnimplementedChannelsServiceServer() {}
|
||||
func (UnimplementedChannelsServiceServer) testEmbeddedByValue() {}
|
||||
@@ -144,7 +144,7 @@ type UnsafeChannelsServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterChannelsServiceServer(s grpc.ServiceRegistrar, srv ChannelsServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedChannelsServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedChannelsServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.10
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.0
|
||||
// source: clients/v1/clients.proto
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: clients/v1/clients.proto
|
||||
|
||||
@@ -153,25 +153,25 @@ type ClientsServiceServer interface {
|
||||
type UnimplementedClientsServiceServer struct{}
|
||||
|
||||
func (UnimplementedClientsServiceServer) Authenticate(context.Context, *AuthnReq) (*AuthnRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Authenticate not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method Authenticate not implemented")
|
||||
}
|
||||
func (UnimplementedClientsServiceServer) RetrieveEntity(context.Context, *v1.RetrieveEntityReq) (*v1.RetrieveEntityRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RetrieveEntity not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RetrieveEntity not implemented")
|
||||
}
|
||||
func (UnimplementedClientsServiceServer) RetrieveEntities(context.Context, *v1.RetrieveEntitiesReq) (*v1.RetrieveEntitiesRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RetrieveEntities not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RetrieveEntities not implemented")
|
||||
}
|
||||
func (UnimplementedClientsServiceServer) AddConnections(context.Context, *v1.AddConnectionsReq) (*v1.AddConnectionsRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AddConnections not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AddConnections not implemented")
|
||||
}
|
||||
func (UnimplementedClientsServiceServer) RemoveConnections(context.Context, *v1.RemoveConnectionsReq) (*v1.RemoveConnectionsRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveConnections not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RemoveConnections not implemented")
|
||||
}
|
||||
func (UnimplementedClientsServiceServer) RemoveChannelConnections(context.Context, *RemoveChannelConnectionsReq) (*RemoveChannelConnectionsRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveChannelConnections not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RemoveChannelConnections not implemented")
|
||||
}
|
||||
func (UnimplementedClientsServiceServer) UnsetParentGroupFromClient(context.Context, *UnsetParentGroupFromClientReq) (*UnsetParentGroupFromClientRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UnsetParentGroupFromClient not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UnsetParentGroupFromClient not implemented")
|
||||
}
|
||||
func (UnimplementedClientsServiceServer) mustEmbedUnimplementedClientsServiceServer() {}
|
||||
func (UnimplementedClientsServiceServer) testEmbeddedByValue() {}
|
||||
@@ -184,7 +184,7 @@ type UnsafeClientsServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterClientsServiceServer(s grpc.ServiceRegistrar, srv ClientsServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedClientsServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedClientsServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.10
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.0
|
||||
// source: common/v1/common.proto
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.10
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.0
|
||||
// source: domains/v1/domains.proto
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: domains/v1/domains.proto
|
||||
|
||||
@@ -99,13 +99,13 @@ type DomainsServiceServer interface {
|
||||
type UnimplementedDomainsServiceServer struct{}
|
||||
|
||||
func (UnimplementedDomainsServiceServer) DeleteUserFromDomains(context.Context, *DeleteUserReq) (*DeleteUserRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteUserFromDomains not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteUserFromDomains not implemented")
|
||||
}
|
||||
func (UnimplementedDomainsServiceServer) RetrieveStatus(context.Context, *v1.RetrieveEntityReq) (*v1.RetrieveEntityRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RetrieveStatus not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RetrieveStatus not implemented")
|
||||
}
|
||||
func (UnimplementedDomainsServiceServer) RetrieveIDByRoute(context.Context, *v1.RetrieveIDByRouteReq) (*v1.RetrieveEntityRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RetrieveIDByRoute not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RetrieveIDByRoute not implemented")
|
||||
}
|
||||
func (UnimplementedDomainsServiceServer) mustEmbedUnimplementedDomainsServiceServer() {}
|
||||
func (UnimplementedDomainsServiceServer) testEmbeddedByValue() {}
|
||||
@@ -118,7 +118,7 @@ type UnsafeDomainsServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterDomainsServiceServer(s grpc.ServiceRegistrar, srv DomainsServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedDomainsServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedDomainsServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.10
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.0
|
||||
// source: groups/v1/groups.proto
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: groups/v1/groups.proto
|
||||
|
||||
@@ -73,7 +73,7 @@ type GroupsServiceServer interface {
|
||||
type UnimplementedGroupsServiceServer struct{}
|
||||
|
||||
func (UnimplementedGroupsServiceServer) RetrieveEntity(context.Context, *v1.RetrieveEntityReq) (*v1.RetrieveEntityRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RetrieveEntity not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RetrieveEntity not implemented")
|
||||
}
|
||||
func (UnimplementedGroupsServiceServer) mustEmbedUnimplementedGroupsServiceServer() {}
|
||||
func (UnimplementedGroupsServiceServer) testEmbeddedByValue() {}
|
||||
@@ -86,7 +86,7 @@ type UnsafeGroupsServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterGroupsServiceServer(s grpc.ServiceRegistrar, srv GroupsServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedGroupsServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedGroupsServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
||||
+284
-24
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.10
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.0
|
||||
// source: token/v1/token.proto
|
||||
|
||||
@@ -30,6 +30,7 @@ type IssueReq struct {
|
||||
UserRole uint32 `protobuf:"varint,2,opt,name=user_role,json=userRole,proto3" json:"user_role,omitempty"`
|
||||
Type uint32 `protobuf:"varint,3,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Verified bool `protobuf:"varint,4,opt,name=verified,proto3" json:"verified,omitempty"`
|
||||
Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -92,6 +93,13 @@ func (x *IssueReq) GetVerified() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *IssueReq) GetDescription() string {
|
||||
if x != nil {
|
||||
return x.Description
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RefreshReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RefreshToken string `protobuf:"bytes,1,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"`
|
||||
@@ -144,6 +152,58 @@ func (x *RefreshReq) GetVerified() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type RevokeReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
TokenId string `protobuf:"bytes,1,opt,name=token_id,json=tokenId,proto3" json:"token_id,omitempty"`
|
||||
UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RevokeReq) Reset() {
|
||||
*x = RevokeReq{}
|
||||
mi := &file_token_v1_token_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RevokeReq) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RevokeReq) ProtoMessage() {}
|
||||
|
||||
func (x *RevokeReq) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_token_v1_token_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RevokeReq.ProtoReflect.Descriptor instead.
|
||||
func (*RevokeReq) Descriptor() ([]byte, []int) {
|
||||
return file_token_v1_token_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *RevokeReq) GetTokenId() string {
|
||||
if x != nil {
|
||||
return x.TokenId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RevokeReq) GetUserId() string {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// If a token is not carrying any information itself, the type
|
||||
// field can be used to determine how to validate the token.
|
||||
// Also, different tokens can be encoded in different ways.
|
||||
@@ -158,7 +218,7 @@ type Token struct {
|
||||
|
||||
func (x *Token) Reset() {
|
||||
*x = Token{}
|
||||
mi := &file_token_v1_token_proto_msgTypes[2]
|
||||
mi := &file_token_v1_token_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -170,7 +230,7 @@ func (x *Token) String() string {
|
||||
func (*Token) ProtoMessage() {}
|
||||
|
||||
func (x *Token) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_token_v1_token_proto_msgTypes[2]
|
||||
mi := &file_token_v1_token_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -183,7 +243,7 @@ func (x *Token) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use Token.ProtoReflect.Descriptor instead.
|
||||
func (*Token) Descriptor() ([]byte, []int) {
|
||||
return file_token_v1_token_proto_rawDescGZIP(), []int{2}
|
||||
return file_token_v1_token_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *Token) GetAccessToken() string {
|
||||
@@ -207,29 +267,219 @@ func (x *Token) GetAccessType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type RevokeRes struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RevokeRes) Reset() {
|
||||
*x = RevokeRes{}
|
||||
mi := &file_token_v1_token_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RevokeRes) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RevokeRes) ProtoMessage() {}
|
||||
|
||||
func (x *RevokeRes) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_token_v1_token_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RevokeRes.ProtoReflect.Descriptor instead.
|
||||
func (*RevokeRes) Descriptor() ([]byte, []int) {
|
||||
return file_token_v1_token_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
type ListUserRefreshTokensReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListUserRefreshTokensReq) Reset() {
|
||||
*x = ListUserRefreshTokensReq{}
|
||||
mi := &file_token_v1_token_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListUserRefreshTokensReq) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListUserRefreshTokensReq) ProtoMessage() {}
|
||||
|
||||
func (x *ListUserRefreshTokensReq) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_token_v1_token_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListUserRefreshTokensReq.ProtoReflect.Descriptor instead.
|
||||
func (*ListUserRefreshTokensReq) Descriptor() ([]byte, []int) {
|
||||
return file_token_v1_token_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *ListUserRefreshTokensReq) GetUserId() string {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ListUserRefreshTokensRes struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RefreshTokens []*RefreshToken `protobuf:"bytes,1,rep,name=refresh_tokens,json=refreshTokens,proto3" json:"refresh_tokens,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListUserRefreshTokensRes) Reset() {
|
||||
*x = ListUserRefreshTokensRes{}
|
||||
mi := &file_token_v1_token_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListUserRefreshTokensRes) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListUserRefreshTokensRes) ProtoMessage() {}
|
||||
|
||||
func (x *ListUserRefreshTokensRes) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_token_v1_token_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListUserRefreshTokensRes.ProtoReflect.Descriptor instead.
|
||||
func (*ListUserRefreshTokensRes) Descriptor() ([]byte, []int) {
|
||||
return file_token_v1_token_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *ListUserRefreshTokensRes) GetRefreshTokens() []*RefreshToken {
|
||||
if x != nil {
|
||||
return x.RefreshTokens
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RefreshToken struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RefreshToken) Reset() {
|
||||
*x = RefreshToken{}
|
||||
mi := &file_token_v1_token_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RefreshToken) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RefreshToken) ProtoMessage() {}
|
||||
|
||||
func (x *RefreshToken) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_token_v1_token_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RefreshToken.ProtoReflect.Descriptor instead.
|
||||
func (*RefreshToken) Descriptor() ([]byte, []int) {
|
||||
return file_token_v1_token_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *RefreshToken) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RefreshToken) GetDescription() string {
|
||||
if x != nil {
|
||||
return x.Description
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_token_v1_token_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_token_v1_token_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x14token/v1/token.proto\x12\btoken.v1\"p\n" +
|
||||
"\x14token/v1/token.proto\x12\btoken.v1\"\x92\x01\n" +
|
||||
"\bIssueReq\x12\x17\n" +
|
||||
"\auser_id\x18\x01 \x01(\tR\x06userId\x12\x1b\n" +
|
||||
"\tuser_role\x18\x02 \x01(\rR\buserRole\x12\x12\n" +
|
||||
"\x04type\x18\x03 \x01(\rR\x04type\x12\x1a\n" +
|
||||
"\bverified\x18\x04 \x01(\bR\bverified\"M\n" +
|
||||
"\bverified\x18\x04 \x01(\bR\bverified\x12 \n" +
|
||||
"\vdescription\x18\x05 \x01(\tR\vdescription\"M\n" +
|
||||
"\n" +
|
||||
"RefreshReq\x12#\n" +
|
||||
"\rrefresh_token\x18\x01 \x01(\tR\frefreshToken\x12\x1a\n" +
|
||||
"\bverified\x18\x02 \x01(\bR\bverified\"\x87\x01\n" +
|
||||
"\bverified\x18\x02 \x01(\bR\bverified\"?\n" +
|
||||
"\tRevokeReq\x12\x19\n" +
|
||||
"\btoken_id\x18\x01 \x01(\tR\atokenId\x12\x17\n" +
|
||||
"\auser_id\x18\x02 \x01(\tR\x06userId\"\x87\x01\n" +
|
||||
"\x05Token\x12!\n" +
|
||||
"\faccess_token\x18\x01 \x01(\tR\vaccessToken\x12(\n" +
|
||||
"\rrefresh_token\x18\x02 \x01(\tH\x00R\frefreshToken\x88\x01\x01\x12\x1f\n" +
|
||||
"\vaccess_type\x18\x03 \x01(\tR\n" +
|
||||
"accessTypeB\x10\n" +
|
||||
"\x0e_refresh_token2r\n" +
|
||||
"\x0e_refresh_token\"\v\n" +
|
||||
"\tRevokeRes\"3\n" +
|
||||
"\x18ListUserRefreshTokensReq\x12\x17\n" +
|
||||
"\auser_id\x18\x01 \x01(\tR\x06userId\"Y\n" +
|
||||
"\x18ListUserRefreshTokensRes\x12=\n" +
|
||||
"\x0erefresh_tokens\x18\x01 \x03(\v2\x16.token.v1.RefreshTokenR\rrefreshTokens\"@\n" +
|
||||
"\fRefreshToken\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x12 \n" +
|
||||
"\vdescription\x18\x02 \x01(\tR\vdescription2\x8b\x02\n" +
|
||||
"\fTokenService\x12.\n" +
|
||||
"\x05Issue\x12\x12.token.v1.IssueReq\x1a\x0f.token.v1.Token\"\x00\x122\n" +
|
||||
"\aRefresh\x12\x14.token.v1.RefreshReq\x1a\x0f.token.v1.Token\"\x00B.Z,github.com/absmach/supermq/api/grpc/token/v1b\x06proto3"
|
||||
"\aRefresh\x12\x14.token.v1.RefreshReq\x1a\x0f.token.v1.Token\"\x00\x124\n" +
|
||||
"\x06Revoke\x12\x13.token.v1.RevokeReq\x1a\x13.token.v1.RevokeRes\"\x00\x12a\n" +
|
||||
"\x15ListUserRefreshTokens\x12\".token.v1.ListUserRefreshTokensReq\x1a\".token.v1.ListUserRefreshTokensRes\"\x00B.Z,github.com/absmach/supermq/api/grpc/token/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_token_v1_token_proto_rawDescOnce sync.Once
|
||||
@@ -243,22 +493,32 @@ func file_token_v1_token_proto_rawDescGZIP() []byte {
|
||||
return file_token_v1_token_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_token_v1_token_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
||||
var file_token_v1_token_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
|
||||
var file_token_v1_token_proto_goTypes = []any{
|
||||
(*IssueReq)(nil), // 0: token.v1.IssueReq
|
||||
(*RefreshReq)(nil), // 1: token.v1.RefreshReq
|
||||
(*Token)(nil), // 2: token.v1.Token
|
||||
(*IssueReq)(nil), // 0: token.v1.IssueReq
|
||||
(*RefreshReq)(nil), // 1: token.v1.RefreshReq
|
||||
(*RevokeReq)(nil), // 2: token.v1.RevokeReq
|
||||
(*Token)(nil), // 3: token.v1.Token
|
||||
(*RevokeRes)(nil), // 4: token.v1.RevokeRes
|
||||
(*ListUserRefreshTokensReq)(nil), // 5: token.v1.ListUserRefreshTokensReq
|
||||
(*ListUserRefreshTokensRes)(nil), // 6: token.v1.ListUserRefreshTokensRes
|
||||
(*RefreshToken)(nil), // 7: token.v1.RefreshToken
|
||||
}
|
||||
var file_token_v1_token_proto_depIdxs = []int32{
|
||||
0, // 0: token.v1.TokenService.Issue:input_type -> token.v1.IssueReq
|
||||
1, // 1: token.v1.TokenService.Refresh:input_type -> token.v1.RefreshReq
|
||||
2, // 2: token.v1.TokenService.Issue:output_type -> token.v1.Token
|
||||
2, // 3: token.v1.TokenService.Refresh:output_type -> token.v1.Token
|
||||
2, // [2:4] is the sub-list for method output_type
|
||||
0, // [0:2] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
7, // 0: token.v1.ListUserRefreshTokensRes.refresh_tokens:type_name -> token.v1.RefreshToken
|
||||
0, // 1: token.v1.TokenService.Issue:input_type -> token.v1.IssueReq
|
||||
1, // 2: token.v1.TokenService.Refresh:input_type -> token.v1.RefreshReq
|
||||
2, // 3: token.v1.TokenService.Revoke:input_type -> token.v1.RevokeReq
|
||||
5, // 4: token.v1.TokenService.ListUserRefreshTokens:input_type -> token.v1.ListUserRefreshTokensReq
|
||||
3, // 5: token.v1.TokenService.Issue:output_type -> token.v1.Token
|
||||
3, // 6: token.v1.TokenService.Refresh:output_type -> token.v1.Token
|
||||
4, // 7: token.v1.TokenService.Revoke:output_type -> token.v1.RevokeRes
|
||||
6, // 8: token.v1.TokenService.ListUserRefreshTokens:output_type -> token.v1.ListUserRefreshTokensRes
|
||||
5, // [5:9] is the sub-list for method output_type
|
||||
1, // [1:5] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_token_v1_token_proto_init() }
|
||||
@@ -266,14 +526,14 @@ func file_token_v1_token_proto_init() {
|
||||
if File_token_v1_token_proto != nil {
|
||||
return
|
||||
}
|
||||
file_token_v1_token_proto_msgTypes[2].OneofWrappers = []any{}
|
||||
file_token_v1_token_proto_msgTypes[3].OneofWrappers = []any{}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_token_v1_token_proto_rawDesc), len(file_token_v1_token_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 3,
|
||||
NumMessages: 8,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: token/v1/token.proto
|
||||
|
||||
@@ -22,8 +22,10 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
TokenService_Issue_FullMethodName = "/token.v1.TokenService/Issue"
|
||||
TokenService_Refresh_FullMethodName = "/token.v1.TokenService/Refresh"
|
||||
TokenService_Issue_FullMethodName = "/token.v1.TokenService/Issue"
|
||||
TokenService_Refresh_FullMethodName = "/token.v1.TokenService/Refresh"
|
||||
TokenService_Revoke_FullMethodName = "/token.v1.TokenService/Revoke"
|
||||
TokenService_ListUserRefreshTokens_FullMethodName = "/token.v1.TokenService/ListUserRefreshTokens"
|
||||
)
|
||||
|
||||
// TokenServiceClient is the client API for TokenService service.
|
||||
@@ -32,6 +34,8 @@ const (
|
||||
type TokenServiceClient interface {
|
||||
Issue(ctx context.Context, in *IssueReq, opts ...grpc.CallOption) (*Token, error)
|
||||
Refresh(ctx context.Context, in *RefreshReq, opts ...grpc.CallOption) (*Token, error)
|
||||
Revoke(ctx context.Context, in *RevokeReq, opts ...grpc.CallOption) (*RevokeRes, error)
|
||||
ListUserRefreshTokens(ctx context.Context, in *ListUserRefreshTokensReq, opts ...grpc.CallOption) (*ListUserRefreshTokensRes, error)
|
||||
}
|
||||
|
||||
type tokenServiceClient struct {
|
||||
@@ -62,12 +66,34 @@ func (c *tokenServiceClient) Refresh(ctx context.Context, in *RefreshReq, opts .
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *tokenServiceClient) Revoke(ctx context.Context, in *RevokeReq, opts ...grpc.CallOption) (*RevokeRes, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RevokeRes)
|
||||
err := c.cc.Invoke(ctx, TokenService_Revoke_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *tokenServiceClient) ListUserRefreshTokens(ctx context.Context, in *ListUserRefreshTokensReq, opts ...grpc.CallOption) (*ListUserRefreshTokensRes, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListUserRefreshTokensRes)
|
||||
err := c.cc.Invoke(ctx, TokenService_ListUserRefreshTokens_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// TokenServiceServer is the server API for TokenService service.
|
||||
// All implementations must embed UnimplementedTokenServiceServer
|
||||
// for forward compatibility.
|
||||
type TokenServiceServer interface {
|
||||
Issue(context.Context, *IssueReq) (*Token, error)
|
||||
Refresh(context.Context, *RefreshReq) (*Token, error)
|
||||
Revoke(context.Context, *RevokeReq) (*RevokeRes, error)
|
||||
ListUserRefreshTokens(context.Context, *ListUserRefreshTokensReq) (*ListUserRefreshTokensRes, error)
|
||||
mustEmbedUnimplementedTokenServiceServer()
|
||||
}
|
||||
|
||||
@@ -79,10 +105,16 @@ type TokenServiceServer interface {
|
||||
type UnimplementedTokenServiceServer struct{}
|
||||
|
||||
func (UnimplementedTokenServiceServer) Issue(context.Context, *IssueReq) (*Token, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Issue not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method Issue not implemented")
|
||||
}
|
||||
func (UnimplementedTokenServiceServer) Refresh(context.Context, *RefreshReq) (*Token, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Refresh not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method Refresh not implemented")
|
||||
}
|
||||
func (UnimplementedTokenServiceServer) Revoke(context.Context, *RevokeReq) (*RevokeRes, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method Revoke not implemented")
|
||||
}
|
||||
func (UnimplementedTokenServiceServer) ListUserRefreshTokens(context.Context, *ListUserRefreshTokensReq) (*ListUserRefreshTokensRes, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListUserRefreshTokens not implemented")
|
||||
}
|
||||
func (UnimplementedTokenServiceServer) mustEmbedUnimplementedTokenServiceServer() {}
|
||||
func (UnimplementedTokenServiceServer) testEmbeddedByValue() {}
|
||||
@@ -95,7 +127,7 @@ type UnsafeTokenServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterTokenServiceServer(s grpc.ServiceRegistrar, srv TokenServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedTokenServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedTokenServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@@ -141,6 +173,42 @@ func _TokenService_Refresh_Handler(srv interface{}, ctx context.Context, dec fun
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _TokenService_Revoke_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RevokeReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(TokenServiceServer).Revoke(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: TokenService_Revoke_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(TokenServiceServer).Revoke(ctx, req.(*RevokeReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _TokenService_ListUserRefreshTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListUserRefreshTokensReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(TokenServiceServer).ListUserRefreshTokens(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: TokenService_ListUserRefreshTokens_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(TokenServiceServer).ListUserRefreshTokens(ctx, req.(*ListUserRefreshTokensReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// TokenService_ServiceDesc is the grpc.ServiceDesc for TokenService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -156,6 +224,14 @@ var TokenService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "Refresh",
|
||||
Handler: _TokenService_Refresh_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Revoke",
|
||||
Handler: _TokenService_Revoke_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListUserRefreshTokens",
|
||||
Handler: _TokenService_ListUserRefreshTokens_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "token/v1/token.proto",
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.0
|
||||
// source: users/v1/users.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
structpb "google.golang.org/protobuf/types/known/structpb"
|
||||
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type RetrieveUsersReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Ids []string `protobuf:"bytes,1,rep,name=ids,proto3" json:"ids,omitempty"`
|
||||
Offset uint64 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"`
|
||||
Limit uint64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RetrieveUsersReq) Reset() {
|
||||
*x = RetrieveUsersReq{}
|
||||
mi := &file_users_v1_users_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RetrieveUsersReq) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RetrieveUsersReq) ProtoMessage() {}
|
||||
|
||||
func (x *RetrieveUsersReq) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_users_v1_users_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RetrieveUsersReq.ProtoReflect.Descriptor instead.
|
||||
func (*RetrieveUsersReq) Descriptor() ([]byte, []int) {
|
||||
return file_users_v1_users_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *RetrieveUsersReq) GetIds() []string {
|
||||
if x != nil {
|
||||
return x.Ids
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *RetrieveUsersReq) GetOffset() uint64 {
|
||||
if x != nil {
|
||||
return x.Offset
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RetrieveUsersReq) GetLimit() uint64 {
|
||||
if x != nil {
|
||||
return x.Limit
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type RetrieveUsersRes struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Total uint64 `protobuf:"varint,1,opt,name=total,proto3" json:"total,omitempty"`
|
||||
Limit uint64 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"`
|
||||
Offset uint64 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"`
|
||||
Users []*User `protobuf:"bytes,4,rep,name=users,proto3" json:"users,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RetrieveUsersRes) Reset() {
|
||||
*x = RetrieveUsersRes{}
|
||||
mi := &file_users_v1_users_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RetrieveUsersRes) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RetrieveUsersRes) ProtoMessage() {}
|
||||
|
||||
func (x *RetrieveUsersRes) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_users_v1_users_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RetrieveUsersRes.ProtoReflect.Descriptor instead.
|
||||
func (*RetrieveUsersRes) Descriptor() ([]byte, []int) {
|
||||
return file_users_v1_users_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *RetrieveUsersRes) GetTotal() uint64 {
|
||||
if x != nil {
|
||||
return x.Total
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RetrieveUsersRes) GetLimit() uint64 {
|
||||
if x != nil {
|
||||
return x.Limit
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RetrieveUsersRes) GetOffset() uint64 {
|
||||
if x != nil {
|
||||
return x.Offset
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RetrieveUsersRes) GetUsers() []*User {
|
||||
if x != nil {
|
||||
return x.Users
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type User struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
FirstName string `protobuf:"bytes,2,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"`
|
||||
LastName string `protobuf:"bytes,3,opt,name=last_name,json=lastName,proto3" json:"last_name,omitempty"`
|
||||
Tags []string `protobuf:"bytes,4,rep,name=tags,proto3" json:"tags,omitempty"`
|
||||
Metadata *structpb.Struct `protobuf:"bytes,5,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
||||
PrivateMetadata *structpb.Struct `protobuf:"bytes,6,opt,name=private_metadata,json=privateMetadata,proto3" json:"private_metadata,omitempty"`
|
||||
Status uint32 `protobuf:"varint,7,opt,name=status,proto3" json:"status,omitempty"`
|
||||
Role uint32 `protobuf:"varint,8,opt,name=role,proto3" json:"role,omitempty"`
|
||||
ProfilePicture string `protobuf:"bytes,9,opt,name=profile_picture,json=profilePicture,proto3" json:"profile_picture,omitempty"`
|
||||
Username string `protobuf:"bytes,10,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Email string `protobuf:"bytes,11,opt,name=email,proto3" json:"email,omitempty"`
|
||||
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,13,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
|
||||
UpdatedBy string `protobuf:"bytes,14,opt,name=updated_by,json=updatedBy,proto3" json:"updated_by,omitempty"`
|
||||
VerifiedAt *timestamppb.Timestamp `protobuf:"bytes,15,opt,name=verified_at,json=verifiedAt,proto3" json:"verified_at,omitempty"`
|
||||
AuthProvider string `protobuf:"bytes,16,opt,name=auth_provider,json=authProvider,proto3" json:"auth_provider,omitempty"`
|
||||
Permissions []string `protobuf:"bytes,17,rep,name=permissions,proto3" json:"permissions,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *User) Reset() {
|
||||
*x = User{}
|
||||
mi := &file_users_v1_users_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *User) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*User) ProtoMessage() {}
|
||||
|
||||
func (x *User) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_users_v1_users_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use User.ProtoReflect.Descriptor instead.
|
||||
func (*User) Descriptor() ([]byte, []int) {
|
||||
return file_users_v1_users_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *User) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetFirstName() string {
|
||||
if x != nil {
|
||||
return x.FirstName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetLastName() string {
|
||||
if x != nil {
|
||||
return x.LastName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetTags() []string {
|
||||
if x != nil {
|
||||
return x.Tags
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *User) GetMetadata() *structpb.Struct {
|
||||
if x != nil {
|
||||
return x.Metadata
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *User) GetPrivateMetadata() *structpb.Struct {
|
||||
if x != nil {
|
||||
return x.PrivateMetadata
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *User) GetStatus() uint32 {
|
||||
if x != nil {
|
||||
return x.Status
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *User) GetRole() uint32 {
|
||||
if x != nil {
|
||||
return x.Role
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *User) GetProfilePicture() string {
|
||||
if x != nil {
|
||||
return x.ProfilePicture
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetUsername() string {
|
||||
if x != nil {
|
||||
return x.Username
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetEmail() string {
|
||||
if x != nil {
|
||||
return x.Email
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetCreatedAt() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.CreatedAt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *User) GetUpdatedAt() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.UpdatedAt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *User) GetUpdatedBy() string {
|
||||
if x != nil {
|
||||
return x.UpdatedBy
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetVerifiedAt() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.VerifiedAt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *User) GetAuthProvider() string {
|
||||
if x != nil {
|
||||
return x.AuthProvider
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *User) GetPermissions() []string {
|
||||
if x != nil {
|
||||
return x.Permissions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_users_v1_users_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_users_v1_users_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x14users/v1/users.proto\x12\busers.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"R\n" +
|
||||
"\x10RetrieveUsersReq\x12\x10\n" +
|
||||
"\x03ids\x18\x01 \x03(\tR\x03ids\x12\x16\n" +
|
||||
"\x06offset\x18\x02 \x01(\x04R\x06offset\x12\x14\n" +
|
||||
"\x05limit\x18\x03 \x01(\x04R\x05limit\"|\n" +
|
||||
"\x10RetrieveUsersRes\x12\x14\n" +
|
||||
"\x05total\x18\x01 \x01(\x04R\x05total\x12\x14\n" +
|
||||
"\x05limit\x18\x02 \x01(\x04R\x05limit\x12\x16\n" +
|
||||
"\x06offset\x18\x03 \x01(\x04R\x06offset\x12$\n" +
|
||||
"\x05users\x18\x04 \x03(\v2\x0e.users.v1.UserR\x05users\"\xff\x04\n" +
|
||||
"\x04User\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" +
|
||||
"\n" +
|
||||
"first_name\x18\x02 \x01(\tR\tfirstName\x12\x1b\n" +
|
||||
"\tlast_name\x18\x03 \x01(\tR\blastName\x12\x12\n" +
|
||||
"\x04tags\x18\x04 \x03(\tR\x04tags\x123\n" +
|
||||
"\bmetadata\x18\x05 \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12B\n" +
|
||||
"\x10private_metadata\x18\x06 \x01(\v2\x17.google.protobuf.StructR\x0fprivateMetadata\x12\x16\n" +
|
||||
"\x06status\x18\a \x01(\rR\x06status\x12\x12\n" +
|
||||
"\x04role\x18\b \x01(\rR\x04role\x12'\n" +
|
||||
"\x0fprofile_picture\x18\t \x01(\tR\x0eprofilePicture\x12\x1a\n" +
|
||||
"\busername\x18\n" +
|
||||
" \x01(\tR\busername\x12\x14\n" +
|
||||
"\x05email\x18\v \x01(\tR\x05email\x129\n" +
|
||||
"\n" +
|
||||
"created_at\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" +
|
||||
"\n" +
|
||||
"updated_at\x18\r \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x12\x1d\n" +
|
||||
"\n" +
|
||||
"updated_by\x18\x0e \x01(\tR\tupdatedBy\x12;\n" +
|
||||
"\vverified_at\x18\x0f \x01(\v2\x1a.google.protobuf.TimestampR\n" +
|
||||
"verifiedAt\x12#\n" +
|
||||
"\rauth_provider\x18\x10 \x01(\tR\fauthProvider\x12 \n" +
|
||||
"\vpermissions\x18\x11 \x03(\tR\vpermissions2Y\n" +
|
||||
"\fUsersService\x12I\n" +
|
||||
"\rRetrieveUsers\x12\x1a.users.v1.RetrieveUsersReq\x1a\x1a.users.v1.RetrieveUsersRes\"\x00B.Z,github.com/absmach/supermq/api/grpc/users/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_users_v1_users_proto_rawDescOnce sync.Once
|
||||
file_users_v1_users_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_users_v1_users_proto_rawDescGZIP() []byte {
|
||||
file_users_v1_users_proto_rawDescOnce.Do(func() {
|
||||
file_users_v1_users_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_users_v1_users_proto_rawDesc), len(file_users_v1_users_proto_rawDesc)))
|
||||
})
|
||||
return file_users_v1_users_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_users_v1_users_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
||||
var file_users_v1_users_proto_goTypes = []any{
|
||||
(*RetrieveUsersReq)(nil), // 0: users.v1.RetrieveUsersReq
|
||||
(*RetrieveUsersRes)(nil), // 1: users.v1.RetrieveUsersRes
|
||||
(*User)(nil), // 2: users.v1.User
|
||||
(*structpb.Struct)(nil), // 3: google.protobuf.Struct
|
||||
(*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp
|
||||
}
|
||||
var file_users_v1_users_proto_depIdxs = []int32{
|
||||
2, // 0: users.v1.RetrieveUsersRes.users:type_name -> users.v1.User
|
||||
3, // 1: users.v1.User.metadata:type_name -> google.protobuf.Struct
|
||||
3, // 2: users.v1.User.private_metadata:type_name -> google.protobuf.Struct
|
||||
4, // 3: users.v1.User.created_at:type_name -> google.protobuf.Timestamp
|
||||
4, // 4: users.v1.User.updated_at:type_name -> google.protobuf.Timestamp
|
||||
4, // 5: users.v1.User.verified_at:type_name -> google.protobuf.Timestamp
|
||||
0, // 6: users.v1.UsersService.RetrieveUsers:input_type -> users.v1.RetrieveUsersReq
|
||||
1, // 7: users.v1.UsersService.RetrieveUsers:output_type -> users.v1.RetrieveUsersRes
|
||||
7, // [7:8] is the sub-list for method output_type
|
||||
6, // [6:7] is the sub-list for method input_type
|
||||
6, // [6:6] is the sub-list for extension type_name
|
||||
6, // [6:6] is the sub-list for extension extendee
|
||||
0, // [0:6] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_users_v1_users_proto_init() }
|
||||
func file_users_v1_users_proto_init() {
|
||||
if File_users_v1_users_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_users_v1_users_proto_rawDesc), len(file_users_v1_users_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 3,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_users_v1_users_proto_goTypes,
|
||||
DependencyIndexes: file_users_v1_users_proto_depIdxs,
|
||||
MessageInfos: file_users_v1_users_proto_msgTypes,
|
||||
}.Build()
|
||||
File_users_v1_users_proto = out.File
|
||||
file_users_v1_users_proto_goTypes = nil
|
||||
file_users_v1_users_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: users/v1/users.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
UsersService_RetrieveUsers_FullMethodName = "/users.v1.UsersService/RetrieveUsers"
|
||||
)
|
||||
|
||||
// UsersServiceClient is the client API for UsersService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
//
|
||||
// UsersService exposes user-related functionality over gRPC.
|
||||
type UsersServiceClient interface {
|
||||
// RetrieveUsers fetches users for the provided IDs.
|
||||
RetrieveUsers(ctx context.Context, in *RetrieveUsersReq, opts ...grpc.CallOption) (*RetrieveUsersRes, error)
|
||||
}
|
||||
|
||||
type usersServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewUsersServiceClient(cc grpc.ClientConnInterface) UsersServiceClient {
|
||||
return &usersServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *usersServiceClient) RetrieveUsers(ctx context.Context, in *RetrieveUsersReq, opts ...grpc.CallOption) (*RetrieveUsersRes, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RetrieveUsersRes)
|
||||
err := c.cc.Invoke(ctx, UsersService_RetrieveUsers_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// UsersServiceServer is the server API for UsersService service.
|
||||
// All implementations must embed UnimplementedUsersServiceServer
|
||||
// for forward compatibility.
|
||||
//
|
||||
// UsersService exposes user-related functionality over gRPC.
|
||||
type UsersServiceServer interface {
|
||||
// RetrieveUsers fetches users for the provided IDs.
|
||||
RetrieveUsers(context.Context, *RetrieveUsersReq) (*RetrieveUsersRes, error)
|
||||
mustEmbedUnimplementedUsersServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedUsersServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedUsersServiceServer struct{}
|
||||
|
||||
func (UnimplementedUsersServiceServer) RetrieveUsers(context.Context, *RetrieveUsersReq) (*RetrieveUsersRes, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method RetrieveUsers not implemented")
|
||||
}
|
||||
func (UnimplementedUsersServiceServer) mustEmbedUnimplementedUsersServiceServer() {}
|
||||
func (UnimplementedUsersServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeUsersServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to UsersServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeUsersServiceServer interface {
|
||||
mustEmbedUnimplementedUsersServiceServer()
|
||||
}
|
||||
|
||||
func RegisterUsersServiceServer(s grpc.ServiceRegistrar, srv UsersServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedUsersServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&UsersService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _UsersService_RetrieveUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RetrieveUsersReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(UsersServiceServer).RetrieveUsers(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: UsersService_RetrieveUsers_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(UsersServiceServer).RetrieveUsers(ctx, req.(*RetrieveUsersReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// UsersService_ServiceDesc is the grpc.ServiceDesc for UsersService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var UsersService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "users.v1.UsersService",
|
||||
HandlerType: (*UsersServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "RetrieveUsers",
|
||||
Handler: _UsersService_RetrieveUsers_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "users/v1/users.proto",
|
||||
}
|
||||
+38
-139
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/absmach/supermq/clients"
|
||||
"github.com/absmach/supermq/groups"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/absmach/supermq/users"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
@@ -38,6 +37,7 @@ const (
|
||||
MetadataKey = "metadata"
|
||||
NameKey = "name"
|
||||
TagKey = "tag"
|
||||
TagsKey = "tags"
|
||||
StatusKey = "status"
|
||||
|
||||
ClientKey = "client"
|
||||
@@ -184,148 +184,47 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
return
|
||||
}
|
||||
|
||||
var wrapper error
|
||||
if errors.Contains(err, apiutil.ErrValidation) {
|
||||
wrapper, err = errors.Unwrap(err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case errors.Contains(err, errors.ErrTryAgain):
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
case errors.Contains(err, errors.ErrEmailAlreadyExists),
|
||||
errors.Contains(err, errors.ErrUsernameNotAvailable),
|
||||
errors.Contains(err, errors.ErrRouteNotAvailable),
|
||||
errors.Contains(err, errors.ErrChannelRouteNotAvailable),
|
||||
errors.Contains(err, errors.ErrDomainRouteNotAvailable),
|
||||
errors.Contains(err, svcerr.ErrExternalAuthProviderCouldNotChangePassword),
|
||||
errors.Contains(err, svcerr.ErrExternalAuthProviderCouldNotResetPassword),
|
||||
errors.Contains(err, svcerr.ErrExternalAuthProviderCouldNotUpdate):
|
||||
switch retErr := err.(type) {
|
||||
case *errors.RequestError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, svcerr.ErrAuthorization),
|
||||
errors.Contains(err, svcerr.ErrDomainAuthorization),
|
||||
errors.Contains(err, svcerr.ErrUnauthorizedPAT),
|
||||
errors.Contains(err, svcerr.ErrSuperAdminAction):
|
||||
err = unwrap(err)
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
|
||||
case errors.Contains(err, svcerr.ErrAuthentication),
|
||||
errors.Contains(err, apiutil.ErrBearerToken),
|
||||
errors.Contains(err, svcerr.ErrLogin),
|
||||
errors.Contains(err, apiutil.ErrUnsupportedTokenType):
|
||||
err = unwrap(err)
|
||||
if err := json.NewEncoder(w).Encode(retErr); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
case *errors.AuthNError:
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
case errors.Contains(err, svcerr.ErrMalformedEntity),
|
||||
errors.Contains(err, apiutil.ErrMalformedPolicy),
|
||||
errors.Contains(err, apiutil.ErrMissingSecret),
|
||||
errors.Contains(err, errors.ErrMalformedEntity),
|
||||
errors.Contains(err, apiutil.ErrMissingID),
|
||||
errors.Contains(err, apiutil.ErrInvalidVerification),
|
||||
errors.Contains(err, apiutil.ErrMissingName),
|
||||
errors.Contains(err, apiutil.ErrMissingEmail),
|
||||
errors.Contains(err, apiutil.ErrInvalidEmail),
|
||||
errors.Contains(err, apiutil.ErrMissingHost),
|
||||
errors.Contains(err, apiutil.ErrInvalidResetPass),
|
||||
errors.Contains(err, apiutil.ErrEmptyList),
|
||||
errors.Contains(err, apiutil.ErrMissingMemberKind),
|
||||
errors.Contains(err, apiutil.ErrMissingMemberType),
|
||||
errors.Contains(err, apiutil.ErrLimitSize),
|
||||
errors.Contains(err, apiutil.ErrBearerKey),
|
||||
errors.Contains(err, svcerr.ErrInvalidStatus),
|
||||
errors.Contains(err, apiutil.ErrNameSize),
|
||||
errors.Contains(err, apiutil.ErrInvalidIDFormat),
|
||||
errors.Contains(err, apiutil.ErrInvalidQueryParams),
|
||||
errors.Contains(err, apiutil.ErrMissingRelation),
|
||||
errors.Contains(err, apiutil.ErrValidation),
|
||||
errors.Contains(err, apiutil.ErrMissingPass),
|
||||
errors.Contains(err, apiutil.ErrMissingConfPass),
|
||||
errors.Contains(err, apiutil.ErrPasswordFormat),
|
||||
errors.Contains(err, svcerr.ErrInvalidRole),
|
||||
errors.Contains(err, svcerr.ErrInvalidPolicy),
|
||||
errors.Contains(err, apiutil.ErrInvitationState),
|
||||
errors.Contains(err, apiutil.ErrInvalidAPIKey),
|
||||
errors.Contains(err, svcerr.ErrViewEntity),
|
||||
errors.Contains(err, apiutil.ErrMissingCertData),
|
||||
errors.Contains(err, apiutil.ErrInvalidContact),
|
||||
errors.Contains(err, apiutil.ErrInvalidTopic),
|
||||
errors.Contains(err, apiutil.ErrInvalidCertData),
|
||||
errors.Contains(err, apiutil.ErrEmptyMessage),
|
||||
errors.Contains(err, apiutil.ErrInvalidLevel),
|
||||
errors.Contains(err, apiutil.ErrInvalidDirection),
|
||||
errors.Contains(err, apiutil.ErrInvalidEntityType),
|
||||
errors.Contains(err, apiutil.ErrMissingEntityType),
|
||||
errors.Contains(err, apiutil.ErrInvalidTimeFormat),
|
||||
errors.Contains(err, svcerr.ErrSearch),
|
||||
errors.Contains(err, apiutil.ErrEmptySearchQuery),
|
||||
errors.Contains(err, apiutil.ErrLenSearchQuery),
|
||||
errors.Contains(err, apiutil.ErrMissingDomainID),
|
||||
errors.Contains(err, apiutil.ErrMissingUserID),
|
||||
errors.Contains(err, apiutil.ErrMissingPATID),
|
||||
errors.Contains(err, apiutil.ErrMissingUsername),
|
||||
errors.Contains(err, apiutil.ErrMissingUsernameEmail),
|
||||
errors.Contains(err, apiutil.ErrMissingFirstName),
|
||||
errors.Contains(err, apiutil.ErrMissingLastName),
|
||||
errors.Contains(err, apiutil.ErrInvalidUsername),
|
||||
errors.Contains(err, apiutil.ErrMissingIdentity),
|
||||
errors.Contains(err, apiutil.ErrInvalidProfilePictureURL),
|
||||
errors.Contains(err, apiutil.ErrSelfParentingNotAllowed),
|
||||
errors.Contains(err, apiutil.ErrMissingChildrenGroupIDs),
|
||||
errors.Contains(err, apiutil.ErrMissingParentGroupID),
|
||||
errors.Contains(err, apiutil.ErrMissingConnectionType),
|
||||
errors.Contains(err, apiutil.ErrMissingRoleName),
|
||||
errors.Contains(err, apiutil.ErrMissingRoleID),
|
||||
errors.Contains(err, apiutil.ErrMissingPolicyEntityType),
|
||||
errors.Contains(err, apiutil.ErrMissingRoleMembers),
|
||||
errors.Contains(err, apiutil.ErrMissingDescription),
|
||||
errors.Contains(err, apiutil.ErrMissingEntityID),
|
||||
errors.Contains(err, apiutil.ErrInvalidRouteFormat),
|
||||
errors.Contains(err, svcerr.ErrRetainOneMember),
|
||||
errors.Contains(err, apiutil.ErrMissingRoute):
|
||||
err = unwrap(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
case errors.Contains(err, svcerr.ErrCreateEntity),
|
||||
errors.Contains(err, svcerr.ErrUpdateEntity),
|
||||
errors.Contains(err, svcerr.ErrRemoveEntity),
|
||||
errors.Contains(err, svcerr.ErrEnableClient),
|
||||
errors.Contains(err, svcerr.ErrEnableUser),
|
||||
errors.Contains(err, svcerr.ErrDisableUser):
|
||||
err = unwrap(err)
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
|
||||
case errors.Contains(err, svcerr.ErrNotFound):
|
||||
err = unwrap(err)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
||||
case errors.Contains(err, errors.ErrStatusAlreadyAssigned),
|
||||
errors.Contains(err, svcerr.ErrInvitationAlreadyRejected),
|
||||
errors.Contains(err, svcerr.ErrInvitationAlreadyAccepted),
|
||||
errors.Contains(err, svcerr.ErrConflict):
|
||||
err = unwrap(err)
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
|
||||
case errors.Contains(err, apiutil.ErrUnsupportedContentType):
|
||||
err = unwrap(err)
|
||||
if err := json.NewEncoder(w).Encode(retErr); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
case *errors.AuthZError:
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
if err := json.NewEncoder(w).Encode(retErr); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
case *errors.MediaTypeError:
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(retErr); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
case *errors.ServiceError:
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
if err := json.NewEncoder(w).Encode(retErr); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
case *errors.NotFoundError:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
if err := json.NewEncoder(w).Encode(retErr); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
case *errors.InternalError:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if wrapper != nil {
|
||||
err = errors.Wrap(wrapper, err)
|
||||
}
|
||||
|
||||
if errorVal, ok := err.(errors.Error); ok {
|
||||
if err := json.NewEncoder(w).Encode(errorVal); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func unwrap(err error) error {
|
||||
wrapper, err := errors.Unwrap(err)
|
||||
if wrapper != nil {
|
||||
return wrapper
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
+75
-92
@@ -218,121 +218,104 @@ func TestEncodeResponse(t *testing.T) {
|
||||
|
||||
func TestEncodeError(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
errs []error
|
||||
code int
|
||||
desc string
|
||||
err error
|
||||
code int
|
||||
hasBody bool
|
||||
checkError bool
|
||||
}{
|
||||
{
|
||||
desc: "BadRequest",
|
||||
errs: []error{
|
||||
apiutil.ErrMissingSecret,
|
||||
svcerr.ErrMalformedEntity,
|
||||
errors.ErrMalformedEntity,
|
||||
apiutil.ErrMissingID,
|
||||
apiutil.ErrEmptyList,
|
||||
apiutil.ErrMissingMemberType,
|
||||
apiutil.ErrMissingMemberKind,
|
||||
apiutil.ErrLimitSize,
|
||||
apiutil.ErrNameSize,
|
||||
svcerr.ErrViewEntity,
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
desc: "RequestError - Missing Secret",
|
||||
err: apiutil.ErrMissingSecret,
|
||||
code: http.StatusBadRequest,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "BadRequest with validation error",
|
||||
errs: []error{
|
||||
errors.Wrap(apiutil.ErrValidation, apiutil.ErrMissingSecret),
|
||||
errors.Wrap(apiutil.ErrValidation, svcerr.ErrMalformedEntity),
|
||||
errors.Wrap(apiutil.ErrValidation, errors.ErrMalformedEntity),
|
||||
errors.Wrap(apiutil.ErrValidation, apiutil.ErrMissingID),
|
||||
errors.Wrap(apiutil.ErrValidation, apiutil.ErrEmptyList),
|
||||
errors.Wrap(apiutil.ErrValidation, apiutil.ErrMissingMemberType),
|
||||
errors.Wrap(apiutil.ErrValidation, apiutil.ErrMissingMemberKind),
|
||||
errors.Wrap(apiutil.ErrValidation, apiutil.ErrLimitSize),
|
||||
errors.Wrap(apiutil.ErrValidation, apiutil.ErrNameSize),
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
desc: "RequestError - Missing ID",
|
||||
err: apiutil.ErrMissingID,
|
||||
code: http.StatusBadRequest,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "Unauthorized",
|
||||
errs: []error{
|
||||
svcerr.ErrAuthentication,
|
||||
svcerr.ErrAuthentication,
|
||||
apiutil.ErrBearerToken,
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
|
||||
{
|
||||
desc: "NotFound",
|
||||
errs: []error{
|
||||
svcerr.ErrNotFound,
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
desc: "RequestError - Empty List",
|
||||
err: apiutil.ErrEmptyList,
|
||||
code: http.StatusBadRequest,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "Conflict",
|
||||
errs: []error{
|
||||
svcerr.ErrConflict,
|
||||
svcerr.ErrConflict,
|
||||
},
|
||||
code: http.StatusConflict,
|
||||
desc: "RequestError - Conflict",
|
||||
err: svcerr.ErrConflict,
|
||||
code: http.StatusBadRequest,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "Forbidden",
|
||||
errs: []error{
|
||||
svcerr.ErrAuthorization,
|
||||
svcerr.ErrAuthorization,
|
||||
svcerr.ErrDomainAuthorization,
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
desc: "NotFoundError - Not Found",
|
||||
err: svcerr.ErrNotFound,
|
||||
code: http.StatusNotFound,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "UnsupportedMediaType",
|
||||
errs: []error{
|
||||
apiutil.ErrUnsupportedContentType,
|
||||
},
|
||||
code: http.StatusUnsupportedMediaType,
|
||||
desc: "AuthNError - Authentication Failed",
|
||||
err: svcerr.ErrAuthentication,
|
||||
code: http.StatusUnauthorized,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "StatusUnprocessableEntity",
|
||||
errs: []error{
|
||||
svcerr.ErrCreateEntity,
|
||||
svcerr.ErrUpdateEntity,
|
||||
svcerr.ErrRemoveEntity,
|
||||
},
|
||||
code: http.StatusUnprocessableEntity,
|
||||
desc: "AuthZError - Authorization Failed",
|
||||
err: svcerr.ErrAuthorization,
|
||||
code: http.StatusForbidden,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "InternalServerError",
|
||||
errs: []error{
|
||||
errors.New("test"),
|
||||
},
|
||||
code: http.StatusInternalServerError,
|
||||
desc: "AuthZError - Domain Authorization Failed",
|
||||
err: svcerr.ErrDomainAuthorization,
|
||||
code: http.StatusForbidden,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "MediaTypeError - Unsupported Content Type",
|
||||
err: apiutil.ErrUnsupportedContentType,
|
||||
code: http.StatusUnsupportedMediaType,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "ServiceError - Create Entity Failed",
|
||||
err: svcerr.ErrCreateEntity,
|
||||
code: http.StatusUnprocessableEntity,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "ServiceError - Update Entity Failed",
|
||||
err: svcerr.ErrUpdateEntity,
|
||||
code: http.StatusUnprocessableEntity,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "ServiceError - Remove Entity Failed",
|
||||
err: svcerr.ErrRemoveEntity,
|
||||
code: http.StatusUnprocessableEntity,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "InternalError",
|
||||
err: errors.NewInternalError(),
|
||||
code: http.StatusInternalServerError,
|
||||
hasBody: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.desc, func(t *testing.T) {
|
||||
responseWriter := newResponseWriter()
|
||||
for _, err := range c.errs {
|
||||
api.EncodeError(context.Background(), err, responseWriter)
|
||||
assert.Equal(t, c.code, responseWriter.StatusCode())
|
||||
|
||||
message := body{}
|
||||
jerr := json.Unmarshal(responseWriter.Body(), &message)
|
||||
assert.NoError(t, jerr)
|
||||
|
||||
var wrapper error
|
||||
switch errors.Contains(err, apiutil.ErrValidation) {
|
||||
case true:
|
||||
wrapper, err = errors.Unwrap(err)
|
||||
assert.Equal(t, err.Error(), message.Error)
|
||||
assert.Equal(t, wrapper.Error(), message.Message)
|
||||
case false:
|
||||
assert.Equal(t, err.Error(), message.Message)
|
||||
}
|
||||
api.EncodeError(context.Background(), c.err, responseWriter)
|
||||
assert.Equal(t, c.code, responseWriter.StatusCode())
|
||||
if !c.hasBody {
|
||||
return
|
||||
}
|
||||
message := body{}
|
||||
jerr := json.Unmarshal(responseWriter.Body(), &message)
|
||||
assert.NoError(t, jerr)
|
||||
assert.NotEmpty(t, message.Message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+91
-88
@@ -10,268 +10,271 @@ import "github.com/absmach/supermq/pkg/errors"
|
||||
// errors are logged twice.
|
||||
var (
|
||||
// ErrValidation indicates that an error was returned by the API.
|
||||
ErrValidation = errors.New("something went wrong with the request")
|
||||
ErrValidation = errors.NewRequestError("something went wrong with the request")
|
||||
|
||||
// ErrBearerToken indicates missing or invalid bearer user token.
|
||||
ErrBearerToken = errors.New("missing or invalid bearer user token")
|
||||
ErrBearerToken = errors.NewAuthNError("missing or invalid bearer user token")
|
||||
|
||||
// ErrBearerKey indicates missing or invalid bearer entity key.
|
||||
ErrBearerKey = errors.New("missing or invalid bearer entity key")
|
||||
ErrBearerKey = errors.NewAuthNError("missing or invalid bearer entity key")
|
||||
|
||||
// ErrMissingID indicates missing entity ID.
|
||||
ErrMissingID = errors.New("missing entity id")
|
||||
ErrMissingID = errors.NewRequestError("missing entity id")
|
||||
|
||||
// ErrMissingEntityID indicates missing entity ID.
|
||||
ErrMissingEntityID = errors.New("missing entity id")
|
||||
ErrMissingEntityID = errors.NewRequestError("missing entity id")
|
||||
|
||||
// ErrMissingClientID indicates missing client ID.
|
||||
ErrMissingClientID = errors.New("missing cient id")
|
||||
ErrMissingClientID = errors.NewRequestError("missing client id")
|
||||
|
||||
// ErrMissingChannelID indicates missing client ID.
|
||||
ErrMissingChannelID = errors.New("missing channel id")
|
||||
ErrMissingChannelID = errors.NewRequestError("missing channel id")
|
||||
|
||||
// ErrMissingConnectionType indicates missing connection tpye.
|
||||
ErrMissingConnectionType = errors.New("missing connection type")
|
||||
ErrMissingConnectionType = errors.NewRequestError("missing connection type")
|
||||
|
||||
// ErrMissingParentGroupID indicates missing parent group ID.
|
||||
ErrMissingParentGroupID = errors.New("missing parent group id")
|
||||
ErrMissingParentGroupID = errors.NewRequestError("missing parent group id")
|
||||
|
||||
// ErrMissingChildrenGroupIDs indicates missing children group IDs.
|
||||
ErrMissingChildrenGroupIDs = errors.New("missing children group ids")
|
||||
ErrMissingChildrenGroupIDs = errors.NewRequestError("missing children group ids")
|
||||
|
||||
// ErrSelfParentingNotAllowed indicates child id is same as parent id.
|
||||
ErrSelfParentingNotAllowed = errors.New("self parenting not allowed")
|
||||
ErrSelfParentingNotAllowed = errors.NewRequestError("self parenting not allowed")
|
||||
|
||||
// ErrInvalidChildGroupID indicates invalid child group ID.
|
||||
ErrInvalidChildGroupID = errors.New("invalid child group id")
|
||||
ErrInvalidChildGroupID = errors.NewRequestError("invalid child group id")
|
||||
|
||||
// ErrInvalidAuthKey indicates invalid auth key.
|
||||
ErrInvalidAuthKey = errors.New("invalid auth key")
|
||||
|
||||
// ErrInvalidIDFormat indicates an invalid ID format.
|
||||
ErrInvalidIDFormat = errors.New("invalid id format provided")
|
||||
ErrInvalidIDFormat = errors.NewRequestError("invalid id format provided")
|
||||
|
||||
// ErrNameSize indicates that name size exceeds the max.
|
||||
ErrNameSize = errors.New("invalid name size")
|
||||
ErrNameSize = errors.NewRequestError("invalid name size")
|
||||
|
||||
// ErrEmailSize indicates that email size exceeds the max.
|
||||
ErrEmailSize = errors.New("invalid email size")
|
||||
ErrEmailSize = errors.NewRequestError("invalid email size")
|
||||
|
||||
// ErrInvalidRole indicates that an invalid role.
|
||||
ErrInvalidRole = errors.New("invalid client role")
|
||||
ErrInvalidRole = errors.NewRequestError("invalid client role")
|
||||
|
||||
// ErrLimitSize indicates that an invalid limit.
|
||||
ErrLimitSize = errors.New("invalid limit size")
|
||||
ErrLimitSize = errors.NewRequestError("invalid limit size")
|
||||
|
||||
// ErrLevel indicates that an invalid level.
|
||||
ErrLevel = errors.New("invalid level")
|
||||
ErrLevel = errors.NewRequestError("invalid level")
|
||||
|
||||
// ErrOffsetSize indicates an invalid offset.
|
||||
ErrOffsetSize = errors.New("invalid offset size")
|
||||
ErrOffsetSize = errors.NewRequestError("invalid offset size")
|
||||
|
||||
// ErrInvalidOrder indicates an invalid list order.
|
||||
ErrInvalidOrder = errors.New("invalid list order provided")
|
||||
ErrInvalidOrder = errors.NewRequestError("invalid list order provided")
|
||||
|
||||
// ErrInvalidDirection indicates an invalid list direction.
|
||||
ErrInvalidDirection = errors.New("invalid list direction provided")
|
||||
ErrInvalidDirection = errors.NewRequestError("invalid list direction provided")
|
||||
|
||||
// ErrInvalidMemberKind indicates an invalid member kind.
|
||||
ErrInvalidMemberKind = errors.New("invalid member kind")
|
||||
ErrInvalidMemberKind = errors.NewRequestError("invalid member kind")
|
||||
|
||||
// ErrEmptyList indicates that entity data is empty.
|
||||
ErrEmptyList = errors.New("empty list provided")
|
||||
ErrEmptyList = errors.NewRequestError("empty list provided")
|
||||
|
||||
// ErrMissingRoleName indicates that role name is empty.
|
||||
ErrMissingRoleName = errors.New("empty role name")
|
||||
ErrMissingRoleName = errors.NewRequestError("empty role name")
|
||||
|
||||
// ErrMissingRoleID indicates that role id is empty.
|
||||
ErrMissingRoleID = errors.New("empty role id")
|
||||
ErrMissingRoleID = errors.NewRequestError("empty role id")
|
||||
|
||||
// ErrMissingRoleOperations indicates that role operations are empty.
|
||||
ErrMissingRoleOperations = errors.New("empty role operations")
|
||||
ErrMissingRoleOperations = errors.NewRequestError("empty role operations")
|
||||
|
||||
// ErrMissingRoleMembers indicates that role members are empty.
|
||||
ErrMissingRoleMembers = errors.New("empty role members")
|
||||
ErrMissingRoleMembers = errors.NewRequestError("empty role members")
|
||||
|
||||
// ErrMalformedPolicy indicates that policies are malformed.
|
||||
ErrMalformedPolicy = errors.New("malformed policy")
|
||||
ErrMalformedPolicy = errors.NewRequestError("malformed policy")
|
||||
|
||||
// ErrMissingPolicySub indicates that policies are subject.
|
||||
ErrMissingPolicySub = errors.New("malformed policy subject")
|
||||
ErrMissingPolicySub = errors.NewRequestError("malformed policy subject")
|
||||
|
||||
// ErrMissingPolicyObj indicates missing policies object.
|
||||
ErrMissingPolicyObj = errors.New("malformed policy object")
|
||||
ErrMissingPolicyObj = errors.NewRequestError("malformed policy object")
|
||||
|
||||
// ErrMalformedPolicyAct indicates missing policies action.
|
||||
ErrMalformedPolicyAct = errors.New("malformed policy action")
|
||||
ErrMalformedPolicyAct = errors.NewRequestError("malformed policy action")
|
||||
|
||||
// ErrMissingPolicyEntityType indicates missing policies entity type.
|
||||
ErrMissingPolicyEntityType = errors.New("missing policy entity type")
|
||||
ErrMissingPolicyEntityType = errors.NewRequestError("missing policy entity type")
|
||||
|
||||
// ErrMalformedPolicyPer indicates missing policies relation.
|
||||
ErrMalformedPolicyPer = errors.New("malformed policy permission")
|
||||
ErrMalformedPolicyPer = errors.NewRequestError("malformed policy permission")
|
||||
|
||||
// ErrMissingCertData indicates missing cert data (ttl).
|
||||
ErrMissingCertData = errors.New("missing certificate data")
|
||||
ErrMissingCertData = errors.NewRequestError("missing certificate data")
|
||||
|
||||
// ErrInvalidCertData indicates invalid cert data (ttl).
|
||||
ErrInvalidCertData = errors.New("invalid certificate data")
|
||||
ErrInvalidCertData = errors.NewRequestError("invalid certificate data")
|
||||
|
||||
// ErrInvalidTopic indicates an invalid subscription topic.
|
||||
ErrInvalidTopic = errors.New("invalid Subscription topic")
|
||||
ErrInvalidTopic = errors.NewRequestError("invalid Subscription topic")
|
||||
|
||||
// ErrInvalidContact indicates an invalid subscription contract.
|
||||
ErrInvalidContact = errors.New("invalid Subscription contact")
|
||||
ErrInvalidContact = errors.NewRequestError("invalid Subscription contact")
|
||||
|
||||
// ErrMissingEmail indicates missing email.
|
||||
ErrMissingEmail = errors.New("missing email")
|
||||
ErrMissingEmail = errors.NewRequestError("missing email")
|
||||
|
||||
// ErrInvalidEmail indicates missing email.
|
||||
ErrInvalidEmail = errors.New("invalid email")
|
||||
ErrInvalidEmail = errors.NewRequestError("invalid email")
|
||||
|
||||
// ErrMissingHost indicates missing host.
|
||||
ErrMissingHost = errors.New("missing host")
|
||||
ErrMissingHost = errors.NewRequestError("missing host")
|
||||
|
||||
// ErrMissingPass indicates missing password.
|
||||
ErrMissingPass = errors.New("missing password")
|
||||
ErrMissingPass = errors.NewRequestError("missing password")
|
||||
|
||||
// ErrMissingConfPass indicates missing conf password.
|
||||
ErrMissingConfPass = errors.New("missing conf password")
|
||||
ErrMissingConfPass = errors.NewRequestError("missing conf password")
|
||||
|
||||
// ErrInvalidResetPass indicates an invalid reset password.
|
||||
ErrInvalidResetPass = errors.New("invalid reset password")
|
||||
ErrInvalidResetPass = errors.NewRequestError("invalid reset password")
|
||||
|
||||
// ErrInvalidComparator indicates an invalid comparator.
|
||||
ErrInvalidComparator = errors.New("invalid comparator")
|
||||
ErrInvalidComparator = errors.NewRequestError("invalid comparator")
|
||||
|
||||
// ErrMissingMemberIDs indicates missing member ids.
|
||||
ErrMissingMemberIDs = errors.New("missing member ids")
|
||||
ErrMissingMemberIDs = errors.NewRequestError("missing member ids")
|
||||
|
||||
// ErrMissingMemberType indicates missing group member type.
|
||||
ErrMissingMemberType = errors.New("missing group member type")
|
||||
ErrMissingMemberType = errors.NewRequestError("missing group member type")
|
||||
|
||||
// ErrMissingMemberKind indicates missing group member kind.
|
||||
ErrMissingMemberKind = errors.New("missing group member kind")
|
||||
ErrMissingMemberKind = errors.NewRequestError("missing group member kind")
|
||||
|
||||
// ErrMissingRelation indicates missing relation.
|
||||
ErrMissingRelation = errors.New("missing relation")
|
||||
ErrMissingRelation = errors.NewRequestError("missing relation")
|
||||
|
||||
// ErrInvalidRelation indicates an invalid relation.
|
||||
ErrInvalidRelation = errors.New("invalid relation")
|
||||
ErrInvalidRelation = errors.NewRequestError("invalid relation")
|
||||
|
||||
// ErrInvalidAPIKey indicates an invalid API key type.
|
||||
ErrInvalidAPIKey = errors.New("invalid api key type")
|
||||
ErrInvalidAPIKey = errors.NewRequestError("invalid api key type")
|
||||
|
||||
// ErrInvitationState indicates an invalid invitation state.
|
||||
ErrInvitationState = errors.New("invalid invitation state")
|
||||
ErrInvitationState = errors.NewRequestError("invalid invitation state")
|
||||
|
||||
// ErrMissingIdentity indicates missing entity Identity.
|
||||
ErrMissingIdentity = errors.New("missing entity identity")
|
||||
ErrMissingIdentity = errors.NewRequestError("missing entity identity")
|
||||
|
||||
// ErrMissingSecret indicates missing secret.
|
||||
ErrMissingSecret = errors.New("missing secret")
|
||||
ErrMissingSecret = errors.NewRequestError("missing secret")
|
||||
|
||||
// ErrPasswordFormat indicates weak password.
|
||||
ErrPasswordFormat = errors.New("password does not meet the requirements")
|
||||
ErrPasswordFormat = errors.NewRequestError("password does not meet the requirements")
|
||||
|
||||
// ErrMissingName indicates missing identity name.
|
||||
ErrMissingName = errors.New("missing identity name")
|
||||
ErrMissingName = errors.NewRequestError("missing identity name")
|
||||
|
||||
// ErrMissingRoute indicates missing route.
|
||||
ErrMissingRoute = errors.New("missing route")
|
||||
ErrMissingRoute = errors.NewRequestError("missing route")
|
||||
|
||||
// ErrInvalidLevel indicates an invalid group level.
|
||||
ErrInvalidLevel = errors.New("invalid group level (should be between 0 and 5)")
|
||||
ErrInvalidLevel = errors.NewRequestError("invalid group level (should be between 0 and 5)")
|
||||
|
||||
// ErrNotFoundParam indicates that the parameter was not found in the query.
|
||||
ErrNotFoundParam = errors.New("parameter not found in the query")
|
||||
ErrNotFoundParam = errors.NewRequestError("parameter not found in the query")
|
||||
|
||||
// ErrInvalidQueryParams indicates invalid query parameters.
|
||||
ErrInvalidQueryParams = errors.New("invalid query parameters")
|
||||
ErrInvalidQueryParams = errors.NewRequestError("invalid query parameters")
|
||||
|
||||
// ErrInvalidVisibilityType indicates invalid visibility type.
|
||||
ErrInvalidVisibilityType = errors.New("invalid visibility type")
|
||||
ErrInvalidVisibilityType = errors.NewRequestError("invalid visibility type")
|
||||
|
||||
// ErrUnsupportedContentType indicates unacceptable or lack of Content-Type.
|
||||
ErrUnsupportedContentType = errors.New("unsupported content type")
|
||||
ErrUnsupportedContentType = errors.NewMediaTypeError("unsupported content type")
|
||||
|
||||
// ErrRollbackTx indicates failed to rollback transaction.
|
||||
ErrRollbackTx = errors.New("failed to rollback transaction")
|
||||
ErrRollbackTx = errors.NewRequestError("failed to rollback transaction")
|
||||
|
||||
// ErrInvalidAggregation indicates invalid aggregation value.
|
||||
ErrInvalidAggregation = errors.New("invalid aggregation value")
|
||||
ErrInvalidAggregation = errors.NewRequestError("invalid aggregation value")
|
||||
|
||||
// ErrInvalidInterval indicates invalid interval value.
|
||||
ErrInvalidInterval = errors.New("invalid interval value")
|
||||
ErrInvalidInterval = errors.NewRequestError("invalid interval value")
|
||||
|
||||
// ErrMissingFrom indicates missing from value.
|
||||
ErrMissingFrom = errors.New("missing from time value")
|
||||
ErrMissingFrom = errors.NewRequestError("missing from time value")
|
||||
|
||||
// ErrMissingTo indicates missing to value.
|
||||
ErrMissingTo = errors.New("missing to time value")
|
||||
ErrMissingTo = errors.NewRequestError("missing to time value")
|
||||
|
||||
// ErrEmptyMessage indicates empty message.
|
||||
ErrEmptyMessage = errors.New("empty message")
|
||||
ErrEmptyMessage = errors.NewRequestError("empty message")
|
||||
|
||||
// ErrMissingEntityType indicates missing entity type.
|
||||
ErrMissingEntityType = errors.New("missing entity type")
|
||||
ErrMissingEntityType = errors.NewRequestError("missing entity type")
|
||||
|
||||
// ErrInvalidEntityType indicates invalid entity type.
|
||||
ErrInvalidEntityType = errors.New("invalid entity type")
|
||||
ErrInvalidEntityType = errors.NewRequestError("invalid entity type")
|
||||
|
||||
// ErrInvalidTimeFormat indicates invalid time format i.e not unix time.
|
||||
ErrInvalidTimeFormat = errors.New("invalid time format use unix time")
|
||||
ErrInvalidTimeFormat = errors.NewRequestError("invalid time format use unix time")
|
||||
|
||||
// ErrEmptySearchQuery indicates search query should not be empty.
|
||||
ErrEmptySearchQuery = errors.New("search query must not be empty")
|
||||
ErrEmptySearchQuery = errors.NewRequestError("search query must not be empty")
|
||||
|
||||
// ErrLenSearchQuery indicates search query length.
|
||||
ErrLenSearchQuery = errors.New("search query must be at least 3 characters")
|
||||
ErrLenSearchQuery = errors.NewRequestError("search query must be at least 3 characters")
|
||||
|
||||
// ErrMissingDomainID indicates missing domainID.
|
||||
ErrMissingDomainID = errors.New("missing domainID")
|
||||
ErrMissingDomainID = errors.NewRequestError("missing domainID")
|
||||
|
||||
// ErrMissingUsername indicates missing user name.
|
||||
ErrMissingUsername = errors.New("missing username")
|
||||
ErrMissingUsername = errors.NewRequestError("missing username")
|
||||
|
||||
// ErrInvalidUsername indicates invalid user name.
|
||||
ErrInvalidUsername = errors.New("invalid username")
|
||||
ErrInvalidUsername = errors.NewRequestError("invalid username")
|
||||
|
||||
// ErrMissingFirstName indicates missing first name.
|
||||
ErrMissingFirstName = errors.New("missing first name")
|
||||
ErrMissingFirstName = errors.NewRequestError("missing first name")
|
||||
|
||||
// ErrMissingLastName indicates missing last name.
|
||||
ErrMissingLastName = errors.New("missing last name")
|
||||
ErrMissingLastName = errors.NewRequestError("missing last name")
|
||||
|
||||
// ErrInvalidProfilePictureURL indicates that the profile picture url is invalid.
|
||||
ErrInvalidProfilePictureURL = errors.New("invalid profile picture url")
|
||||
ErrInvalidProfilePictureURL = errors.NewRequestError("invalid profile picture url")
|
||||
|
||||
ErrMultipleEntitiesFilter = errors.New("multiple entities are provided in filter are not supported")
|
||||
ErrMultipleEntitiesFilter = errors.NewRequestError("multiple entities are provided in filter are not supported")
|
||||
|
||||
// ErrMissingDescription indicates missing description.
|
||||
ErrMissingDescription = errors.New("missing description")
|
||||
ErrMissingDescription = errors.NewRequestError("missing description")
|
||||
|
||||
// ErrUnsupportedTokenType indicates that this type of token is not supported.
|
||||
ErrUnsupportedTokenType = errors.New("unsupported content token type")
|
||||
ErrUnsupportedTokenType = errors.NewRequestError("unsupported content token type")
|
||||
|
||||
// ErrMissingUserID indicates missing user ID.
|
||||
ErrMissingUserID = errors.New("missing user id")
|
||||
ErrMissingUserID = errors.NewRequestError("missing user id")
|
||||
|
||||
// ErrMissingPATID indicates missing pat ID.
|
||||
ErrMissingPATID = errors.New("missing pat id")
|
||||
ErrMissingPATID = errors.NewRequestError("missing pat id")
|
||||
|
||||
// ErrInvalidNameFormat indicates invalid name format.
|
||||
ErrInvalidNameFormat = errors.New("invalid name format")
|
||||
ErrInvalidNameFormat = errors.NewRequestError("invalid name format")
|
||||
|
||||
// ErrInvalidRouteFormat indicates invalid route format.
|
||||
ErrInvalidRouteFormat = errors.New("invalid route format")
|
||||
ErrInvalidRouteFormat = errors.NewRequestError("invalid route format")
|
||||
|
||||
// ErrMissingUsernameEmail indicates missing user name / email.
|
||||
ErrMissingUsernameEmail = errors.New("missing username / email")
|
||||
ErrMissingUsernameEmail = errors.NewRequestError("missing username / email")
|
||||
|
||||
// ErrInvalidVerification indicates invalid email verification.
|
||||
ErrInvalidVerification = errors.New("invalid verification")
|
||||
ErrInvalidVerification = errors.NewRequestError("invalid verification")
|
||||
|
||||
// ErrEmailNotVerified indicates invalid email not verified.
|
||||
ErrEmailNotVerified = errors.New("email not verified")
|
||||
ErrEmailNotVerified = errors.NewRequestError("email not verified")
|
||||
|
||||
// ErrMalformedRequest indicates malformed request body.
|
||||
ErrMalformedRequestBody = errors.NewRequestError("request body is not a valid JSON, expecting a valid JSON")
|
||||
)
|
||||
|
||||
@@ -8,11 +8,16 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BearerPrefix represents the token prefix for Bearer authentication scheme.
|
||||
const BearerPrefix = "Bearer "
|
||||
const (
|
||||
// BearerPrefix represents the token prefix for Bearer authentication scheme.
|
||||
BearerPrefix = "Bearer "
|
||||
|
||||
// ClientPrefix represents the key prefix for Client authentication scheme.
|
||||
const ClientPrefix = "Client "
|
||||
// ClientPrefix represents the key prefix for Client authentication scheme.
|
||||
ClientPrefix = "Client "
|
||||
|
||||
// BasicAuthPrefix represents the prefix for Basic authentication scheme.
|
||||
BasicAuthPrefix = "Basic "
|
||||
)
|
||||
|
||||
// ExtractBearerToken returns value of the bearer token. If there is no bearer token - an empty value is returned.
|
||||
func ExtractBearerToken(r *http.Request) string {
|
||||
|
||||
@@ -9,7 +9,7 @@ info:
|
||||
contact:
|
||||
name: SuperMQ Team
|
||||
url: 'https://github.com/absmach/supermq'
|
||||
email: info@abstractmachines.fr
|
||||
email: info@absmach.eu
|
||||
description: |
|
||||
MQTT adapter provides an MQTT API for sending messages through the platform. MQTT adapter uses [mProxy](https://github.com/absmach/mproxy) for proxying traffic between client and MQTT broker.
|
||||
Additionally, the MQTT adapter and the message broker are replicating the traffic between brokers.
|
||||
|
||||
@@ -10,7 +10,7 @@ info:
|
||||
contact:
|
||||
name: SuperMQ Team
|
||||
url: 'https://github.com/absmach/supermq'
|
||||
email: info@abstractmachines.fr
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: 'https://github.com/absmach/supermq/blob/main/LICENSE'
|
||||
@@ -29,7 +29,7 @@ servers:
|
||||
default: localhost
|
||||
port:
|
||||
description: SuperMQ WebSocket Adapter port
|
||||
default: '8186'
|
||||
default: '8008'
|
||||
|
||||
channels:
|
||||
'm/{domainPrefix}/c/{channelPrefix}/{subtopic}':
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
This folder contains an OpenAPI specifications for SuperMQ API.
|
||||
|
||||
View specification in Swagger UI at [docs.api.supermq.abstractmachines.fr](https://docs.api.supermq.abstractmachines.fr)
|
||||
View specification in Swagger UI at [docs.api.supermq.absmach.eu](https://docs.api.supermq.absmach.eu)
|
||||
|
||||
+50
-39
@@ -9,7 +9,7 @@ info:
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
@@ -24,17 +24,17 @@ tags:
|
||||
description: Everything about your Keys.
|
||||
externalDocs:
|
||||
description: Find out more about keys
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
- name: PATs
|
||||
description: Everything about your Personal Access Tokens.
|
||||
externalDocs:
|
||||
description: Find out more about Personal Access Tokens
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
- name: Health
|
||||
description: Service health check endpoint.
|
||||
externalDocs:
|
||||
description: Find out more about health check
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
|
||||
paths:
|
||||
/keys:
|
||||
@@ -81,6 +81,8 @@ paths:
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Service can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
@@ -100,6 +102,8 @@ paths:
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Service can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
@@ -124,7 +128,7 @@ paths:
|
||||
description: Missing or invalid content type.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
|
||||
get:
|
||||
operationId: listPATs
|
||||
tags:
|
||||
@@ -144,7 +148,7 @@ paths:
|
||||
description: Missing or invalid access token provided.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
|
||||
delete:
|
||||
operationId: clearAllPATs
|
||||
tags:
|
||||
@@ -159,7 +163,7 @@ paths:
|
||||
description: Missing or invalid access token provided.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
|
||||
/pats/{patID}:
|
||||
get:
|
||||
operationId: retrievePAT
|
||||
@@ -181,7 +185,7 @@ paths:
|
||||
description: PAT not found.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
|
||||
delete:
|
||||
operationId: deletePAT
|
||||
tags:
|
||||
@@ -200,7 +204,7 @@ paths:
|
||||
description: PAT not found.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
|
||||
/pats/{patID}/name:
|
||||
patch:
|
||||
operationId: updatePATName
|
||||
@@ -226,7 +230,7 @@ paths:
|
||||
description: Missing or invalid content type.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
|
||||
/pats/{patID}/description:
|
||||
patch:
|
||||
operationId: updatePATDescription
|
||||
@@ -252,7 +256,7 @@ paths:
|
||||
description: Missing or invalid content type.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
|
||||
/pats/{patID}/secret/reset:
|
||||
patch:
|
||||
operationId: resetPATSecret
|
||||
@@ -278,7 +282,7 @@ paths:
|
||||
description: Missing or invalid content type.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
|
||||
/pats/{patID}/secret/revoke:
|
||||
patch:
|
||||
operationId: revokePATSecret
|
||||
@@ -298,7 +302,7 @@ paths:
|
||||
description: PAT not found.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
|
||||
/pats/{patID}/scope:
|
||||
get:
|
||||
operationId: listScopes
|
||||
@@ -322,7 +326,7 @@ paths:
|
||||
description: PAT not found.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
|
||||
delete:
|
||||
operationId: clearAllScopes
|
||||
tags:
|
||||
@@ -341,7 +345,7 @@ paths:
|
||||
description: PAT not found.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
|
||||
/pats/{patID}/scope/add:
|
||||
patch:
|
||||
operationId: addScope
|
||||
@@ -369,7 +373,7 @@ paths:
|
||||
description: Database cannot process the request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
|
||||
/pats/{patID}/scope/remove:
|
||||
patch:
|
||||
operationId: removeScope
|
||||
@@ -396,7 +400,6 @@ paths:
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
@@ -465,7 +468,7 @@ components:
|
||||
format: date-time
|
||||
example: "2019-11-26T13:31:52Z"
|
||||
description: Time when the PAT was revoked
|
||||
|
||||
|
||||
PATsPage:
|
||||
type: object
|
||||
properties:
|
||||
@@ -486,20 +489,10 @@ components:
|
||||
items:
|
||||
$ref: "#/components/schemas/PAT"
|
||||
description: List of Personal Access Tokens
|
||||
|
||||
|
||||
Scope:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
|
||||
description: Scope unique identifier
|
||||
pat_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "9118de62-c680-46b7-ad0a-21748a52833a"
|
||||
description: PAT ID this scope belongs to
|
||||
optional_domain_id:
|
||||
type: string
|
||||
format: uuid
|
||||
@@ -507,7 +500,8 @@ components:
|
||||
description: Optional domain ID for the scope
|
||||
entity_type:
|
||||
type: string
|
||||
enum: [groups, channels, clients, domains, users, dashboards, messages]
|
||||
enum:
|
||||
[groups, channels, clients, domains, users, dashboards, messages]
|
||||
example: "groups"
|
||||
description: Type of entity the scope applies to
|
||||
entity_id:
|
||||
@@ -516,10 +510,21 @@ components:
|
||||
description: ID of the entity the scope applies to. '*' means all entities of the specified type.
|
||||
operation:
|
||||
type: string
|
||||
enum: [create, read, list, update, delete, share, unshare, publish, subscribe]
|
||||
enum:
|
||||
[
|
||||
create,
|
||||
read,
|
||||
list,
|
||||
update,
|
||||
delete,
|
||||
share,
|
||||
unshare,
|
||||
publish,
|
||||
subscribe,
|
||||
]
|
||||
example: "read"
|
||||
description: Operation allowed by this scope
|
||||
|
||||
|
||||
ScopesPage:
|
||||
type: object
|
||||
properties:
|
||||
@@ -679,9 +684,11 @@ components:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- duration
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
example: "My PAT"
|
||||
description: Name of the Personal Access Token
|
||||
description:
|
||||
@@ -690,9 +697,10 @@ components:
|
||||
description: Description of the Personal Access Token
|
||||
duration:
|
||||
type: string
|
||||
pattern: "^[0-9]+(ns|us|µs|ms|s|m|h|d|w|y)$"
|
||||
example: "30d"
|
||||
description: Duration for which the PAT is valid. Format is a duration string (e.g. "30d", "24h", "1y").
|
||||
|
||||
|
||||
UpdatePATNameRequest:
|
||||
description: JSON-formatted document describing PAT name update request.
|
||||
required: true
|
||||
@@ -707,7 +715,7 @@ components:
|
||||
type: string
|
||||
example: "New PAT Name"
|
||||
description: New name for the Personal Access Token
|
||||
|
||||
|
||||
UpdatePATDescriptionRequest:
|
||||
description: JSON-formatted document describing PAT description update request.
|
||||
required: true
|
||||
@@ -722,7 +730,7 @@ components:
|
||||
type: string
|
||||
example: "New PAT Description"
|
||||
description: New description for the Personal Access Token
|
||||
|
||||
|
||||
ResetPATSecretRequest:
|
||||
description: JSON-formatted document describing PAT secret reset request.
|
||||
required: true
|
||||
@@ -730,12 +738,15 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- duration
|
||||
properties:
|
||||
duration:
|
||||
type: string
|
||||
pattern: "^[0-9]+(ns|us|µs|ms|s|m|h|d|w|y)$"
|
||||
example: "30d"
|
||||
description: Duration for which the new PAT secret is valid. Format is a duration string (e.g. "30d", "24h", "1y").
|
||||
|
||||
|
||||
AddScopeRequest:
|
||||
description: JSON-formatted document describing add scope request.
|
||||
required: true
|
||||
@@ -751,7 +762,7 @@ components:
|
||||
items:
|
||||
$ref: "#/components/schemas/Scope"
|
||||
description: List of scopes to add
|
||||
|
||||
|
||||
RemoveScopeRequest:
|
||||
description: JSON-formatted document describing remove scope request.
|
||||
required: true
|
||||
@@ -793,14 +804,14 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PAT"
|
||||
|
||||
|
||||
PATsPageRes:
|
||||
description: Page of Personal Access Tokens.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PATsPage"
|
||||
|
||||
|
||||
ScopesPageRes:
|
||||
description: Page of scopes.
|
||||
content:
|
||||
|
||||
@@ -9,7 +9,7 @@ info:
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
@@ -24,17 +24,17 @@ tags:
|
||||
description: CRUD operations for your channels
|
||||
externalDocs:
|
||||
description: Find out more about channels
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
- name: Connections
|
||||
description: All operations involving channel and client connections
|
||||
externalDocs:
|
||||
description: Find out more about channel and client connections
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
- name: Health
|
||||
description: Health check operations
|
||||
externalDocs:
|
||||
description: Find out more about health checks
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
|
||||
paths:
|
||||
/{domainID}/channels:
|
||||
@@ -89,6 +89,7 @@ paths:
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Order"
|
||||
- $ref: "#/components/parameters/Direction"
|
||||
- $ref: "#/components/parameters/Tags"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/Status"
|
||||
- $ref: "#/components/parameters/ChannelName"
|
||||
@@ -101,6 +102,8 @@ paths:
|
||||
- $ref: "#/components/parameters/Client"
|
||||
- $ref: "#/components/parameters/Group"
|
||||
- $ref: "#/components/parameters/User"
|
||||
- $ref: "#/components/parameters/CreatedFrom"
|
||||
- $ref: "#/components/parameters/CreatedTo"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/ChannelPageRes"
|
||||
@@ -656,9 +659,7 @@ components:
|
||||
example: 10
|
||||
description: Maximum number of items to return in one page.
|
||||
required:
|
||||
- channels
|
||||
- total
|
||||
- offset
|
||||
|
||||
ChannelUpdate:
|
||||
type: object
|
||||
@@ -705,7 +706,10 @@ components:
|
||||
type: array
|
||||
description: Connection types.
|
||||
items:
|
||||
example: publish
|
||||
type: string
|
||||
enum:
|
||||
- publish
|
||||
- subscribe
|
||||
|
||||
ChannelConnectionReqSchema:
|
||||
type: object
|
||||
@@ -719,7 +723,10 @@ components:
|
||||
type: array
|
||||
description: Connection types.
|
||||
items:
|
||||
example: publish
|
||||
type: string
|
||||
enum:
|
||||
- publish
|
||||
- subscribe
|
||||
|
||||
Error:
|
||||
type: object
|
||||
@@ -776,16 +783,12 @@ components:
|
||||
|
||||
Tags:
|
||||
name: tags
|
||||
description: Client tags.
|
||||
description: Channel tags. Multiple tags can be specified separated by comma for OR condition and plus for AND condition.
|
||||
in: query
|
||||
schema:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
type: string
|
||||
required: false
|
||||
example: ["yello", "orange"]
|
||||
example: "orange,yellow"
|
||||
|
||||
ChannelName:
|
||||
name: name
|
||||
@@ -831,7 +834,6 @@ components:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
minimum: 0
|
||||
required: false
|
||||
|
||||
Limit:
|
||||
@@ -844,7 +846,7 @@ components:
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
example: "100"
|
||||
example: 100
|
||||
|
||||
Offset:
|
||||
name: offset
|
||||
@@ -855,8 +857,8 @@ components:
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
example: "0"
|
||||
|
||||
example: 0
|
||||
|
||||
Order:
|
||||
name: order
|
||||
description: Field by which to order the results
|
||||
@@ -944,6 +946,26 @@ components:
|
||||
required: false
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
CreatedFrom:
|
||||
name: created_from
|
||||
description: Filter channels created from this date (inclusive).
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
required: false
|
||||
example: "2023-01-01T00:00:00Z"
|
||||
|
||||
CreatedTo:
|
||||
name: created_to
|
||||
description: Filter channels created up to this date (inclusive).
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
required: false
|
||||
example: "2023-12-31T23:59:59Z"
|
||||
|
||||
requestBodies:
|
||||
ChannelCreateReq:
|
||||
description: JSON-formatted document describing the new channel to be registered
|
||||
|
||||
+119
-21
@@ -9,7 +9,7 @@ info:
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
@@ -24,17 +24,17 @@ tags:
|
||||
description: CRUD operations for your clients
|
||||
externalDocs:
|
||||
description: Find out more about clients
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
- name: Roles
|
||||
description: All operations involving roles for clients
|
||||
externalDocs:
|
||||
description: Find out more about roles
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
- name: Health
|
||||
description: Health check operations
|
||||
externalDocs:
|
||||
description: Find out more about health checks
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
|
||||
paths:
|
||||
/{domainID}/clients:
|
||||
@@ -100,6 +100,8 @@ paths:
|
||||
- $ref: "#/components/parameters/ConnectionType"
|
||||
- $ref: "#/components/parameters/Group"
|
||||
- $ref: "#/components/parameters/User"
|
||||
- $ref: "#/components/parameters/CreatedFrom"
|
||||
- $ref: "#/components/parameters/CreatedTo"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
@@ -181,7 +183,7 @@ paths:
|
||||
|
||||
patch:
|
||||
operationId: updateClient
|
||||
summary: Updates name and metadata of the client.
|
||||
summary: Updates name, metadata, and private metadata of the client.
|
||||
description: |
|
||||
Update is performed by replacing the current resource data with values
|
||||
provided in a request payload. Note that the client's type and ID
|
||||
@@ -323,7 +325,7 @@ paths:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/ClientRes"
|
||||
$ref: "#/components/responses/DisabledClientRes"
|
||||
"400":
|
||||
description: Failed due to malformed client's ID.
|
||||
"401":
|
||||
@@ -943,10 +945,14 @@ components:
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
minimum: 8
|
||||
description: Free-form account secret used for acquiring auth token(s).
|
||||
private_metadata:
|
||||
type: object
|
||||
example: { "model": "example" }
|
||||
description: Arbitrary, object-encoded client's data private to the client.
|
||||
metadata:
|
||||
type: object
|
||||
example: { "model": "example" }
|
||||
description: Arbitrary, object-encoded client's data.
|
||||
description: Arbitrary, object-encoded client's data visible to other clients.
|
||||
status:
|
||||
type: string
|
||||
description: Client Status
|
||||
@@ -1001,10 +1007,14 @@ components:
|
||||
type: string
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Client secret password.
|
||||
private_metadata:
|
||||
type: object
|
||||
example: { "model": "example" }
|
||||
description: Arbitrary, object-encoded client's data private to the client.
|
||||
metadata:
|
||||
type: object
|
||||
example: { "model": "example" }
|
||||
description: Arbitrary, object-encoded client's data.
|
||||
description: Arbitrary, object-encoded client's data visible to other clients.
|
||||
status:
|
||||
type: string
|
||||
description: Client Status
|
||||
@@ -1058,10 +1068,14 @@ components:
|
||||
type: string
|
||||
example: ""
|
||||
description: Client secret password.
|
||||
private_metadata:
|
||||
type: object
|
||||
example: { "model": "example" }
|
||||
description: Arbitrary, object-encoded client's data private to the client.
|
||||
metadata:
|
||||
type: object
|
||||
example: { "model": "example" }
|
||||
description: Arbitrary, object-encoded client's data.
|
||||
description: Arbitrary, object-encoded client's data visible to other clients.
|
||||
status:
|
||||
type: string
|
||||
description: Client Status
|
||||
@@ -1113,9 +1127,14 @@ components:
|
||||
metadata:
|
||||
type: object
|
||||
example: { "role": "general" }
|
||||
description: Arbitrary, object-encoded client's data.
|
||||
description: Arbitrary, object-encoded client's data visible to other clients.
|
||||
private_metadata:
|
||||
type: object
|
||||
example: { "role": "general" }
|
||||
description: Arbitrary, object-encoded client's data private to the client.
|
||||
required:
|
||||
- name
|
||||
- private_metadata
|
||||
- metadata
|
||||
|
||||
ClientTags:
|
||||
@@ -1130,6 +1149,63 @@ components:
|
||||
items:
|
||||
type: string
|
||||
|
||||
DisabledClient:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Client unique identifier.
|
||||
name:
|
||||
type: string
|
||||
example: clientName
|
||||
description: Client name.
|
||||
tags:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
example: ["tag1", "tag2"]
|
||||
description: Client tags.
|
||||
domain_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: ID of the domain to which client belongs.
|
||||
credentials:
|
||||
type: object
|
||||
properties:
|
||||
identity:
|
||||
type: string
|
||||
example: clientIDentity
|
||||
description: Client Identity for example email address.
|
||||
secret:
|
||||
type: string
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Client secret password.
|
||||
metadata:
|
||||
type: object
|
||||
example: { "model": "example" }
|
||||
description: Arbitrary, object-encoded client's data visible to other clients.
|
||||
status:
|
||||
type: string
|
||||
description: Client Status
|
||||
format: string
|
||||
example: disabled
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the channel was created.
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the channel was created.
|
||||
xml:
|
||||
name: client
|
||||
|
||||
ClientSecret:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1208,16 +1284,12 @@ components:
|
||||
|
||||
Tags:
|
||||
name: tags
|
||||
description: Client tags.
|
||||
description: Clients tags. Multiple tags can be specified separated by comma for OR condition and plus for AND condition.
|
||||
in: query
|
||||
schema:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
type: string
|
||||
required: false
|
||||
example: ["yello", "orange"]
|
||||
example: "orange,yellow"
|
||||
|
||||
Metadata:
|
||||
name: metadata
|
||||
@@ -1225,7 +1297,6 @@ components:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
minimum: 0
|
||||
required: false
|
||||
|
||||
Limit:
|
||||
@@ -1238,7 +1309,7 @@ components:
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
example: "100"
|
||||
example: 100
|
||||
|
||||
Offset:
|
||||
name: offset
|
||||
@@ -1249,7 +1320,7 @@ components:
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
example: "0"
|
||||
example: 0
|
||||
|
||||
Order:
|
||||
name: order
|
||||
@@ -1349,6 +1420,26 @@ components:
|
||||
minLength: 36
|
||||
required: false
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
CreatedFrom:
|
||||
name: created_from
|
||||
description: Filter clients created from this date (inclusive).
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
required: false
|
||||
example: "2023-01-01T00:00:00Z"
|
||||
|
||||
CreatedTo:
|
||||
name: created_to
|
||||
description: Filter clients created up to this date (inclusive).
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
required: false
|
||||
example: "2023-12-31T23:59:59Z"
|
||||
|
||||
requestBodies:
|
||||
ClientCreateReq:
|
||||
@@ -1370,7 +1461,7 @@ components:
|
||||
$ref: "#/components/schemas/ClientReqObj"
|
||||
|
||||
ClientUpdateReq:
|
||||
description: JSON-formated document describing the metadata and name of client to be update
|
||||
description: JSON-formated document describing the metadata, private metadata and name of client to be update
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
@@ -1447,6 +1538,13 @@ components:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Client"
|
||||
|
||||
DisabledClientRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DisabledClient"
|
||||
|
||||
ClientPageRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
|
||||
@@ -9,7 +9,7 @@ info:
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
@@ -24,22 +24,22 @@ tags:
|
||||
description: CRUD operations for your domains
|
||||
externalDocs:
|
||||
description: Find out more about domains
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
- name: Roles
|
||||
description: All operations involving roles for domains
|
||||
externalDocs:
|
||||
description: Find out more about roles
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
- name: Invitations
|
||||
description: All operations involving invitations for domains
|
||||
externalDocs:
|
||||
description: Find out more about Invitations
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
- name: Health
|
||||
description: Service health check endpoint.
|
||||
externalDocs:
|
||||
description: Find out more about health check
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
|
||||
paths:
|
||||
/domains:
|
||||
@@ -76,6 +76,7 @@ paths:
|
||||
- $ref: "#/components/parameters/Order"
|
||||
- $ref: "#/components/parameters/Direction"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/Tags"
|
||||
- $ref: "#/components/parameters/Status"
|
||||
- $ref: "#/components/parameters/DomainName"
|
||||
- $ref: "./schemas/roles.yaml#/components/parameters/ActionsQuery"
|
||||
@@ -83,6 +84,8 @@ paths:
|
||||
- $ref: "./schemas/roles.yaml#/components/parameters/RoleNameQuery"
|
||||
- $ref: "#/components/parameters/AccessType"
|
||||
- $ref: "#/components/parameters/OnlyTotal"
|
||||
- $ref: "#/components/parameters/CreatedFrom"
|
||||
- $ref: "#/components/parameters/CreatedTo"
|
||||
tags:
|
||||
- Domains
|
||||
security:
|
||||
@@ -1179,6 +1182,14 @@ components:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
Tags:
|
||||
name: tags
|
||||
description: Domain tags. Multiple tags can be specified separated by comma for OR condition and plus for AND condition.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
example: "orange,yellow"
|
||||
Type:
|
||||
name: type
|
||||
description: The type of the API Key.
|
||||
@@ -1282,6 +1293,26 @@ components:
|
||||
default: false
|
||||
required: false
|
||||
|
||||
CreatedFrom:
|
||||
name: created_from
|
||||
description: Filter domains created from this date (inclusive).
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
required: false
|
||||
example: "2023-01-01T00:00:00Z"
|
||||
|
||||
CreatedTo:
|
||||
name: created_to
|
||||
description: Filter domains created up to this date (inclusive).
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
required: false
|
||||
example: "2023-12-31T23:59:59Z"
|
||||
|
||||
requestBodies:
|
||||
DomainCreateReq:
|
||||
description: JSON-formatted document describing the new domain to be registered
|
||||
|
||||
+63
-30
@@ -9,7 +9,7 @@ info:
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
@@ -24,17 +24,17 @@ tags:
|
||||
description: CRUD operations for your groups
|
||||
externalDocs:
|
||||
description: Find out more about users groups
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
- name: Roles
|
||||
description: All operations involving roles for groups
|
||||
externalDocs:
|
||||
description: Find out more about roles
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
- name: Health
|
||||
description: Health check operations
|
||||
externalDocs:
|
||||
description: Find out more about health checks
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
|
||||
paths:
|
||||
/{domainID}/groups:
|
||||
@@ -92,16 +92,19 @@ paths:
|
||||
- $ref: "#/components/parameters/DirectionOrder"
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Tree"
|
||||
- $ref: "#/components/parameters/Tags"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
- $ref: "#/components/parameters/GroupName"
|
||||
- $ref: "#/components/parameters/RootGroup"
|
||||
- $ref: "#/components/parameters/Status"
|
||||
- $ref: "#/components/parameters/ID"
|
||||
- $ref: "#/components/parameters/ID"
|
||||
- $ref: "./schemas/roles.yaml#/components/parameters/ActionsQuery"
|
||||
- $ref: "./schemas/roles.yaml#/components/parameters/RoleIDQuery"
|
||||
- $ref: "./schemas/roles.yaml#/components/parameters/RoleNameQuery"
|
||||
- $ref: "#/components/parameters/AccessType"
|
||||
- $ref: "#/components/parameters/OnlyTotal"
|
||||
- $ref: "#/components/parameters/OnlyTotal"
|
||||
- $ref: "#/components/parameters/CreatedFrom"
|
||||
- $ref: "#/components/parameters/CreatedTo"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/GroupPageRes"
|
||||
@@ -328,6 +331,8 @@ paths:
|
||||
- $ref: "#/components/parameters/Level"
|
||||
- $ref: "#/components/parameters/Tree"
|
||||
- $ref: "#/components/parameters/Direction"
|
||||
- $ref: "#/components/parameters/CreatedFrom"
|
||||
- $ref: "#/components/parameters/CreatedTo"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/GroupsHierarchyPageRes"
|
||||
@@ -362,6 +367,11 @@ paths:
|
||||
responses:
|
||||
"200":
|
||||
description: Parent group set.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
links: {}
|
||||
"400":
|
||||
description: Failed due to malformed group's ID.
|
||||
"401":
|
||||
@@ -392,6 +402,11 @@ paths:
|
||||
responses:
|
||||
"200":
|
||||
description: Parent group removed.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
links: {}
|
||||
"400":
|
||||
description: Failed due to malformed group's ID.
|
||||
"401":
|
||||
@@ -1377,16 +1392,12 @@ components:
|
||||
|
||||
Tags:
|
||||
name: tags
|
||||
description: User tags.
|
||||
description: Group tags. Multiple tags can be specified separated by comma for OR condition and plus for AND condition.
|
||||
in: query
|
||||
schema:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
type: string
|
||||
required: false
|
||||
example: ["yello", "orange"]
|
||||
example: "orange,yellow"
|
||||
|
||||
GroupName:
|
||||
name: name
|
||||
@@ -1398,7 +1409,7 @@ components:
|
||||
example: "groupName"
|
||||
|
||||
GroupDescription:
|
||||
name: name
|
||||
name: description
|
||||
description: Group's description.
|
||||
in: query
|
||||
schema:
|
||||
@@ -1513,7 +1524,6 @@ components:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
minimum: 0
|
||||
required: false
|
||||
|
||||
Limit:
|
||||
@@ -1526,7 +1536,7 @@ components:
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
example: "100"
|
||||
example: 100
|
||||
|
||||
Offset:
|
||||
name: offset
|
||||
@@ -1537,9 +1547,9 @@ components:
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
example: "0"
|
||||
|
||||
Order:
|
||||
example: 0
|
||||
|
||||
Order:
|
||||
name: order
|
||||
description: Field by which to order the results
|
||||
in: query
|
||||
@@ -1547,8 +1557,8 @@ components:
|
||||
type: string
|
||||
required: false
|
||||
example: created_at
|
||||
|
||||
DirectionOrder:
|
||||
|
||||
DirectionOrder:
|
||||
name: order
|
||||
description: Direction of ordering the results.
|
||||
in: query
|
||||
@@ -1560,7 +1570,7 @@ components:
|
||||
required: false
|
||||
example: desc
|
||||
|
||||
ID:
|
||||
ID:
|
||||
name: id
|
||||
description: List groups with the given ID.
|
||||
in: query
|
||||
@@ -1569,7 +1579,7 @@ components:
|
||||
format: uuid
|
||||
required: false
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
|
||||
AccessType:
|
||||
name: access_type
|
||||
description: Type of access the user has on the group.
|
||||
@@ -1583,8 +1593,8 @@ components:
|
||||
- indirect_group
|
||||
required: false
|
||||
example: direct
|
||||
|
||||
OnlyTotal:
|
||||
|
||||
OnlyTotal:
|
||||
name: only_total
|
||||
description: If true, the response will contain only the total number of groups that match the query parameters.
|
||||
in: query
|
||||
@@ -1592,7 +1602,27 @@ components:
|
||||
type: boolean
|
||||
default: false
|
||||
required: false
|
||||
|
||||
|
||||
CreatedFrom:
|
||||
name: created_from
|
||||
description: Filter groups created from this date (inclusive).
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
required: false
|
||||
example: "2023-01-01T00:00:00Z"
|
||||
|
||||
CreatedTo:
|
||||
name: created_to
|
||||
description: Filter groups created up to this date (inclusive).
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
required: false
|
||||
example: "2023-12-31T23:59:59Z"
|
||||
|
||||
User:
|
||||
name: user
|
||||
description: If provided lists groups associated with a user with the provided ID. Only available for admin users.
|
||||
@@ -1663,10 +1693,10 @@ components:
|
||||
operationId: getGroup
|
||||
parameters:
|
||||
groupID: $response.body#/id
|
||||
get_children:
|
||||
operationId: listChildrenGroups
|
||||
parameters:
|
||||
groupID: $response.body#/id
|
||||
# get_children:
|
||||
# operationId: listChildrenGroups
|
||||
# parameters:
|
||||
# groupID: $response.body#/id
|
||||
update:
|
||||
operationId: updateGroup
|
||||
parameters:
|
||||
@@ -1710,6 +1740,7 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupsPage"
|
||||
links: {}
|
||||
|
||||
GroupsHierarchyPageRes:
|
||||
description: Group hierarchy retrieved.
|
||||
@@ -1717,6 +1748,7 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GroupsHierarchyPage"
|
||||
links: {}
|
||||
|
||||
MembersPageRes:
|
||||
description: Group members retrieved.
|
||||
@@ -1724,6 +1756,7 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MembersPage"
|
||||
links: {}
|
||||
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
|
||||
@@ -9,7 +9,7 @@ info:
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
@@ -24,7 +24,7 @@ tags:
|
||||
description: Everything about your Messages
|
||||
externalDocs:
|
||||
description: Find out more about messages
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
|
||||
paths:
|
||||
/m/{domainPrefix}/c/{channelPrefix}:
|
||||
@@ -47,6 +47,8 @@ paths:
|
||||
description: Message discarded due to its malformed content.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Access denied to the requested resource.
|
||||
"404":
|
||||
description: Message discarded due to invalid channel id.
|
||||
"415":
|
||||
|
||||
@@ -75,7 +75,7 @@ paths:
|
||||
This includes connection status, messages sent/received, and other metrics.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/domain_id"
|
||||
- $ref: '#/components/parameters/client_id'
|
||||
- $ref: "#/components/parameters/client_id"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
@@ -319,7 +319,7 @@ components:
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
example: "0"
|
||||
example: 0
|
||||
|
||||
limit:
|
||||
name: limit
|
||||
@@ -328,10 +328,10 @@ components:
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
example: "10"
|
||||
example: 10
|
||||
|
||||
operation:
|
||||
name: operation
|
||||
@@ -365,7 +365,7 @@ components:
|
||||
description: Start date in unix time.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
type: integer
|
||||
format: int64
|
||||
required: false
|
||||
example: 1966777289
|
||||
@@ -375,7 +375,7 @@ components:
|
||||
description: End date in unix time.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
type: integer
|
||||
format: int64
|
||||
required: false
|
||||
example: 1966777289
|
||||
|
||||
@@ -1,431 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: SuperMQ twins service
|
||||
description: |
|
||||
HTTP API for managing digital twins and their states.
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.18.0
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9018
|
||||
- url: https://localhost:9018
|
||||
|
||||
tags:
|
||||
- name: twins
|
||||
description: Everything about your Twins
|
||||
externalDocs:
|
||||
description: Find out more about twins
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
|
||||
paths:
|
||||
/twins:
|
||||
post:
|
||||
operationId: createTwin
|
||||
summary: Adds new twin
|
||||
description: |
|
||||
Adds new twin to the list of twins owned by user identified using
|
||||
the provided access token.
|
||||
tags:
|
||||
- twins
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/TwinReq"
|
||||
responses:
|
||||
"201":
|
||||
$ref: "#/components/responses/TwinCreateRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
get:
|
||||
operationId: getTwins
|
||||
summary: Retrieves twins
|
||||
description: |
|
||||
Retrieves a list of twins. Due to performance concerns, data
|
||||
is retrieved in subsets.
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
- $ref: "#/components/parameters/Metadata"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/TwinsPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/twins/{twinID}:
|
||||
get:
|
||||
operationId: getTwin
|
||||
summary: Retrieves twin info
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TwinID"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/TwinRes"
|
||||
"400":
|
||||
description: Failed due to malformed twin's ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: Twin does not exist.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
operationId: updateTwin
|
||||
summary: Updates twin info
|
||||
description: |
|
||||
Update is performed by replacing the current resource data with values
|
||||
provided in a request payload. Note that the twin's ID cannot be changed.
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TwinID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/TwinReq"
|
||||
responses:
|
||||
"200":
|
||||
description: Twin updated.
|
||||
"400":
|
||||
description: Failed due to malformed twin's ID or malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: Twin does not exist.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
operationId: removeTwin
|
||||
summary: Removes a twin
|
||||
description: Removes a twin.
|
||||
tags:
|
||||
- twins
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TwinID"
|
||||
responses:
|
||||
"204":
|
||||
description: Twin removed.
|
||||
"400":
|
||||
description: Failed due to malformed twin's ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided
|
||||
"404":
|
||||
description: Twin does not exist.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/states/{twinID}:
|
||||
get:
|
||||
operationId: getStates
|
||||
summary: Retrieves states of twin with id twinID
|
||||
description: |
|
||||
Retrieves a list of states. Due to performance concerns, data
|
||||
is retrieved in subsets.
|
||||
tags:
|
||||
- states
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TwinID"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/StatesPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: Twin does not exist.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
parameters:
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: Twin name
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
Metadata:
|
||||
name: metadata
|
||||
description: |
|
||||
Metadata filter. Filtering is performed matching the parameter with
|
||||
metadata on top level. Parameter is json.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
minimum: 0
|
||||
required: false
|
||||
TwinID:
|
||||
name: twinID
|
||||
description: Unique twin identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
minimum: 1
|
||||
required: true
|
||||
|
||||
schemas:
|
||||
Attribute:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Name of the attribute.
|
||||
channel:
|
||||
type: string
|
||||
description: SuperMQ channel used by attribute.
|
||||
subtopic:
|
||||
type: string
|
||||
description: Subtopic used by attribute.
|
||||
persist_state:
|
||||
type: boolean
|
||||
description: Trigger state creation based on the attribute.
|
||||
Definition:
|
||||
type: object
|
||||
properties:
|
||||
delta:
|
||||
type: number
|
||||
description: Minimal time delay before new state creation.
|
||||
attributes:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
$ref: "#/components/schemas/Attribute"
|
||||
TwinReqObj:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Free-form twin name.
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded twin's data.
|
||||
definition:
|
||||
$ref: "#/components/schemas/Definition"
|
||||
TwinResObj:
|
||||
type: object
|
||||
properties:
|
||||
owner:
|
||||
type: string
|
||||
description: Email address of SuperMQ user that owns twin.
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique twin identifier generated by the service.
|
||||
name:
|
||||
type: string
|
||||
description: Free-form twin name.
|
||||
revision:
|
||||
type: number
|
||||
description: Oridnal revision number of twin.
|
||||
created:
|
||||
type: string
|
||||
format: date
|
||||
description: Twin creation date and time.
|
||||
updated:
|
||||
type: string
|
||||
format: date
|
||||
description: Twin update date and time.
|
||||
definitions:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
$ref: "#/components/schemas/Definition"
|
||||
metadata:
|
||||
type: object
|
||||
description: Arbitrary, object-encoded twin's data.
|
||||
TwinsPage:
|
||||
type: object
|
||||
properties:
|
||||
twins:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
$ref: "#/components/schemas/TwinResObj"
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
required:
|
||||
- twins
|
||||
State:
|
||||
type: object
|
||||
properties:
|
||||
twin_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: ID of twin state belongs to.
|
||||
id:
|
||||
type: number
|
||||
description: State position in a time row of states.
|
||||
created:
|
||||
type: string
|
||||
format: date
|
||||
description: State creation date.
|
||||
payload:
|
||||
type: object
|
||||
description: Object-encoded states's payload.
|
||||
StatesPage:
|
||||
type: object
|
||||
properties:
|
||||
states:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
$ref: "#/components/schemas/State"
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
required:
|
||||
- states
|
||||
|
||||
requestBodies:
|
||||
TwinReq:
|
||||
description: JSON-formatted document describing the twin to create or update.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TwinReqObj"
|
||||
required: true
|
||||
|
||||
responses:
|
||||
TwinCreateRes:
|
||||
description: Created twin's relative URL (i.e. /twins/{twinID}).
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
TwinRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TwinResObj"
|
||||
links:
|
||||
update:
|
||||
operationId: updateTwin
|
||||
parameters:
|
||||
twinID: $response.body#/id
|
||||
delete:
|
||||
operationId: removeTwin
|
||||
parameters:
|
||||
twinID: $response.body#/id
|
||||
states:
|
||||
operationId: getStates
|
||||
parameters:
|
||||
twinID: $response.body#/id
|
||||
TwinsPageRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TwinsPage"
|
||||
StatesPageRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/StatesPage"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yaml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
+169
-35
@@ -9,7 +9,7 @@ info:
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
@@ -24,12 +24,12 @@ tags:
|
||||
description: Everything about your Users
|
||||
externalDocs:
|
||||
description: Find out more about users
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
- name: Health
|
||||
description: Health check operations
|
||||
externalDocs:
|
||||
description: Find out more about health checks
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
url: https://docs.supermq.absmach.eu/
|
||||
|
||||
paths:
|
||||
/users:
|
||||
@@ -83,8 +83,10 @@ paths:
|
||||
- $ref: "#/components/parameters/LastName"
|
||||
- $ref: "#/components/parameters/Username"
|
||||
- $ref: "#/components/parameters/Email"
|
||||
- $ref: "#/components/parameters/Tag"
|
||||
- $ref: "#/components/parameters/Tags"
|
||||
- $ref: "#/components/parameters/OnlyTotal"
|
||||
- $ref: "#/components/parameters/CreatedFrom"
|
||||
- $ref: "#/components/parameters/CreatedTo"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
@@ -116,7 +118,7 @@ paths:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/UserRes"
|
||||
$ref: "#/components/responses/UserProfileRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
@@ -154,10 +156,10 @@ paths:
|
||||
|
||||
patch:
|
||||
operationId: updateUser
|
||||
summary: Updates first, last name and metadata of the user.
|
||||
summary: Updates first, last name, metadata, and private metadata of the user.
|
||||
description: |
|
||||
Updates name and metadata of the user with provided ID. Name and metadata
|
||||
is updated using authorization token and the new received info.
|
||||
Updates name, metadata and private metadata of the user with provided ID. Name, metadata
|
||||
and private metadata are updated using authorization token and the new received info.
|
||||
tags:
|
||||
- Users
|
||||
parameters:
|
||||
@@ -461,7 +463,7 @@ paths:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/UserRes"
|
||||
$ref: "#/components/responses/UserProfileRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
@@ -569,7 +571,7 @@ paths:
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/IssueTokenReq"
|
||||
responses:
|
||||
"200":
|
||||
"201":
|
||||
$ref: "#/components/responses/TokenRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
@@ -610,6 +612,57 @@ paths:
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/users/tokens/revoke:
|
||||
post:
|
||||
operationId: revokeRefreshToken
|
||||
summary: Revoke Refresh Token
|
||||
description: |
|
||||
Revokes a specific refresh token by its ID. This invalidates the
|
||||
refresh token so it can no longer be used to obtain new access tokens.
|
||||
tags:
|
||||
- Users
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/RevokeRefreshTokenReq"
|
||||
responses:
|
||||
"204":
|
||||
description: Refresh token revoked successfully.
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/users/tokens/refresh-tokens:
|
||||
get:
|
||||
operationId: listActiveRefreshTokens
|
||||
summary: List Active Refresh Tokens
|
||||
description: |
|
||||
Lists all active refresh token sessions for the currently authenticated user.
|
||||
tags:
|
||||
- Users
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/RefreshTokensPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/users/send-verification:
|
||||
post:
|
||||
operationId: sendVerification
|
||||
@@ -706,7 +759,7 @@ components:
|
||||
type: string
|
||||
format: password
|
||||
example: password
|
||||
minimum: 8
|
||||
minLength: 8
|
||||
description: Free-form account secret used for acquiring auth token(s).
|
||||
required:
|
||||
- username
|
||||
@@ -714,7 +767,11 @@ components:
|
||||
metadata:
|
||||
type: object
|
||||
example: { "domain": "example.com" }
|
||||
description: Arbitrary, object-encoded user's data.
|
||||
description: Arbitrary, object-encoded user's data visible to other users.
|
||||
private_metadata:
|
||||
type: object
|
||||
example: { "domain": "example.com" }
|
||||
description: Arbitrary, object-encoded user's data private to the user.
|
||||
profile_picture:
|
||||
type: string
|
||||
example: "https://example.com/profile.jpg"
|
||||
@@ -770,7 +827,11 @@ components:
|
||||
metadata:
|
||||
type: object
|
||||
example: { "address": "example" }
|
||||
description: Arbitrary, object-encoded user's data.
|
||||
description: Arbitrary, object-encoded user's data visible to other users.
|
||||
private_metadata:
|
||||
type: object
|
||||
example: { "address": "example" }
|
||||
description: Arbitrary, object-encoded user's data private to the user.
|
||||
profile_picture:
|
||||
type: string
|
||||
example: "https://example.com/profile.jpg"
|
||||
@@ -830,12 +891,12 @@ components:
|
||||
secret:
|
||||
type: string
|
||||
example: password
|
||||
minimum: 8
|
||||
minLength: 8
|
||||
description: User secret password.
|
||||
metadata:
|
||||
type: object
|
||||
example: { "role": "general" }
|
||||
description: Arbitrary, object-encoded user's data.
|
||||
description: Arbitrary, object-encoded user's data visible to other users.
|
||||
status:
|
||||
type: string
|
||||
description: User Status
|
||||
@@ -894,11 +955,11 @@ components:
|
||||
metadata:
|
||||
type: object
|
||||
example: { "role": "general" }
|
||||
description: Arbitrary, object-encoded user's data.
|
||||
required:
|
||||
- first_name
|
||||
- last_name
|
||||
- metadata
|
||||
description: Arbitrary, object-encoded user's data visible to other users.
|
||||
private_metadata:
|
||||
type: object
|
||||
example: { "role": "general" }
|
||||
description: Arbitrary, object-encoded user's data private to the user.
|
||||
|
||||
UserTags:
|
||||
type: object
|
||||
@@ -908,7 +969,6 @@ components:
|
||||
example: ["yello", "orange"]
|
||||
description: User tags.
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
|
||||
@@ -938,12 +998,12 @@ components:
|
||||
old_secret:
|
||||
type: string
|
||||
example: oldpassword
|
||||
minimum: 8
|
||||
minLength: 8
|
||||
description: Old user secret password.
|
||||
new_secret:
|
||||
type: string
|
||||
example: newpassword
|
||||
minimum: 8
|
||||
minLength: 8
|
||||
description: New user secret password.
|
||||
required:
|
||||
- old_secret
|
||||
@@ -1029,12 +1089,36 @@ components:
|
||||
password:
|
||||
type: string
|
||||
example: password
|
||||
minimum: 8
|
||||
minLength: 8
|
||||
description: User secret password.
|
||||
required:
|
||||
- username
|
||||
- password
|
||||
|
||||
RefreshToken:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "bb7edb32-2eac-4aad-aebe-ed96fe073879"
|
||||
description: Unique identifier of the refresh token.
|
||||
description:
|
||||
type: string
|
||||
example: "Chrome browser session"
|
||||
description: Description of the refresh token session.
|
||||
|
||||
RefreshTokensPage:
|
||||
type: object
|
||||
properties:
|
||||
refresh_tokens:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/RefreshToken"
|
||||
description: List of active refresh tokens.
|
||||
required:
|
||||
- refresh_tokens
|
||||
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1138,14 +1222,14 @@ components:
|
||||
required: false
|
||||
example: enabled
|
||||
|
||||
Tag:
|
||||
name: tag
|
||||
description: User tag.
|
||||
Tags:
|
||||
name: tags
|
||||
description: User tags. Multiple tags can be specified separated by comma for OR condition and plus for AND condition.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
example: "orange"
|
||||
example: "orange,yellow"
|
||||
|
||||
GroupName:
|
||||
name: name
|
||||
@@ -1278,7 +1362,7 @@ components:
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
example: "100"
|
||||
example: 100
|
||||
|
||||
Offset:
|
||||
name: offset
|
||||
@@ -1289,7 +1373,7 @@ components:
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
example: "0"
|
||||
example: 0
|
||||
|
||||
Order:
|
||||
name: order
|
||||
@@ -1321,6 +1405,26 @@ components:
|
||||
default: false
|
||||
required: false
|
||||
|
||||
CreatedFrom:
|
||||
name: created_from
|
||||
description: Filter users created from this date (inclusive).
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
required: false
|
||||
example: "2023-01-01T00:00:00Z"
|
||||
|
||||
CreatedTo:
|
||||
name: created_to
|
||||
description: Filter users created up to this date (inclusive).
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
required: false
|
||||
example: "2023-12-31T23:59:59Z"
|
||||
|
||||
VerificationToken:
|
||||
name: token
|
||||
description: Verification token.
|
||||
@@ -1339,7 +1443,7 @@ components:
|
||||
$ref: "#/components/schemas/UserReqObj"
|
||||
|
||||
UserUpdateReq:
|
||||
description: JSON-formated document describing the metadata and name of user to be update
|
||||
description: JSON-formated document describing the name, metadata and private_metadata of user to be update
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
@@ -1444,18 +1548,34 @@ components:
|
||||
format: password
|
||||
description: New password.
|
||||
example: "12345678"
|
||||
minimum: 8
|
||||
minLength: 8
|
||||
confirm_password:
|
||||
type: string
|
||||
format: password
|
||||
description: New confirmation password.
|
||||
example: "12345678"
|
||||
minimum: 8
|
||||
minLength: 8
|
||||
token:
|
||||
type: string
|
||||
format: jwt
|
||||
description: Reset token generated and sent in email.
|
||||
|
||||
RevokeRefreshTokenReq:
|
||||
description: JSON-formatted document describing the refresh token to revoke.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
token_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "bb7edb32-2eac-4aad-aebe-ed96fe073879"
|
||||
description: The unique identifier of the refresh token to revoke.
|
||||
required:
|
||||
- token_id
|
||||
|
||||
PasswordChange:
|
||||
description: Password change data. User can change its password.
|
||||
required: true
|
||||
@@ -1467,11 +1587,11 @@ components:
|
||||
password:
|
||||
type: string
|
||||
format: password
|
||||
minimum: 8
|
||||
minLength: 8
|
||||
description: New password.
|
||||
old_password:
|
||||
type: string
|
||||
minimum: 8
|
||||
minLength: 8
|
||||
format: password
|
||||
description: Old password.
|
||||
|
||||
@@ -1533,6 +1653,13 @@ components:
|
||||
schema:
|
||||
$ref: "#/components/schemas/User"
|
||||
|
||||
UserProfileRes:
|
||||
description: Data retrieved for current user profile.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/User"
|
||||
|
||||
UserPageRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
@@ -1560,6 +1687,13 @@ components:
|
||||
example: access
|
||||
description: User access token type.
|
||||
|
||||
RefreshTokensPageRes:
|
||||
description: List of active refresh tokens for the authenticated user.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/RefreshTokensPage"
|
||||
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
|
||||
+49
-49
@@ -40,7 +40,7 @@ The following actions are supported:
|
||||
|
||||
## Domains
|
||||
|
||||
Domains are used to group users and clients. Each domain has a unique route that is associated with the domain. Domains are used to group users and their entities.
|
||||
Domains are used to group users and clients. Each domain has a unique `route` that is associated with the domain. Domains are used to group users and their entities.
|
||||
|
||||
Domain consists of the following fields:
|
||||
|
||||
@@ -59,50 +59,50 @@ Domain consists of the following fields:
|
||||
|
||||
The service is configured using the environment variables presented in the following table. Note that any unset variables will be replaced with their default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ----------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------ |
|
||||
| SMQ_AUTH_LOG_LEVEL | Log level for the Auth service (debug, info, warn, error) | info |
|
||||
| SMQ_AUTH_DB_HOST | Database host address | localhost |
|
||||
| SMQ_AUTH_DB_PORT | Database host port | 5432 |
|
||||
| SMQ_AUTH_DB_USER | Database user | supermq |
|
||||
| SMQ_AUTH_DB_PASSWORD | Database password | supermq |
|
||||
| SMQ_AUTH_DB_NAME | Name of the database used by the service | auth |
|
||||
| SMQ_AUTH_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| SMQ_AUTH_DB_SSL_CERT | Path to the PEM encoded certificate file | "" |
|
||||
| SMQ_AUTH_DB_SSL_KEY | Path to the PEM encoded key file | "" |
|
||||
| SMQ_AUTH_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | "" |
|
||||
| SMQ_AUTH_HTTP_HOST | Auth service HTTP host | "" |
|
||||
| SMQ_AUTH_HTTP_PORT | Auth service HTTP port | 8189 |
|
||||
| SMQ_AUTH_HTTP_SERVER_CERT | Path to the PEM encoded HTTP server certificate file | "" |
|
||||
| SMQ_AUTH_HTTP_SERVER_KEY | Path to the PEM encoded HTTP server key file | "" |
|
||||
| SMQ_AUTH_GRPC_HOST | Auth service gRPC host | "" |
|
||||
| SMQ_AUTH_GRPC_PORT | Auth service gRPC port | 8181 |
|
||||
| SMQ_AUTH_GRPC_SERVER_CERT | Path to the PEM encoded gRPC server certificate file | "" |
|
||||
| SMQ_AUTH_GRPC_SERVER_KEY | Path to the PEM encoded gRPC server key file | "" |
|
||||
| SMQ_AUTH_GRPC_SERVER_CA_CERTS | Path to the PEM encoded gRPC server CA certificate file | "" |
|
||||
| SMQ_AUTH_GRPC_CLIENT_CA_CERTS | Path to the PEM encoded gRPC client CA certificate file | "" |
|
||||
| SMQ_AUTH_SECRET_KEY | String used for signing tokens | secret |
|
||||
| SMQ_AUTH_ACCESS_TOKEN_DURATION | The access token expiration period | 1h |
|
||||
| SMQ_AUTH_REFRESH_TOKEN_DURATION | The refresh token expiration period | 24h |
|
||||
| SMQ_AUTH_INVITATION_DURATION | The invitation token expiration period | 168h |
|
||||
| SMQ_AUTH_CACHE_URL | Redis URL for caching PAT scopes | redis://localhost:6379/0 |
|
||||
| SMQ_AUTH_CACHE_KEY_DURATION | Duration for which PAT scope cache keys are valid | 10m |
|
||||
| SMQ_SPICEDB_HOST | SpiceDB host address | localhost |
|
||||
| SMQ_SPICEDB_PORT | SpiceDB host port | 50051 |
|
||||
| SMQ_SPICEDB_PRE_SHARED_KEY | SpiceDB pre-shared key | 12345678 |
|
||||
| SMQ_SPICEDB_SCHEMA_FILE | Path to SpiceDB schema file | ./docker/spicedb/schema.zed |
|
||||
| SMQ_JAEGER_URL | Jaeger server URL | <http://jaeger:4318/v1/traces> |
|
||||
| SMQ_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 |
|
||||
| SMQ_SEND_TELEMETRY | Send telemetry to supermq call home server | true |
|
||||
| SMQ_ADAPTER_INSTANCE_ID | Adapter instance ID | "" |
|
||||
| SMQ_CALLOUT_URLS | Comma-separated list of callout URLs | "" |
|
||||
| SMQ_CALLOUT_METHOD | Callout method | POST |
|
||||
| SMQ_CALLOUT_TLS_VERIFICATION | Enable TLS verification for callouts | true |
|
||||
| SMQ_CALLOUT_TIMEOUT | Callout timeout | 10s |
|
||||
| SMQ_CALLOUT_CA_CERT | Path to CA certificate file | "" |
|
||||
| SMQ_CALLOUT_CERT | Path to client certificate file | "" |
|
||||
| SMQ_CALLOUT_KEY | Path to client key file | "" |
|
||||
| SMQ_CALLOUT_OPERATIONS | Invoke callout if the authorization permission matches any of the given permissions. | "" |
|
||||
| Variable | Description | Default |
|
||||
| :--- | :--- | :--- |
|
||||
| `SMQ_AUTH_LOG_LEVEL` | Log level for the Auth service (debug, info, warn, error) | info |
|
||||
| `SMQ_AUTH_DB_HOST` | Database host address | localhost |
|
||||
| `SMQ_AUTH_DB_PORT` | Database host port | 5432 |
|
||||
| `SMQ_AUTH_DB_USER` | Database user | supermq |
|
||||
| `SMQ_AUTH_DB_PASSWORD` | Database password | supermq |
|
||||
| `SMQ_AUTH_DB_NAME` | Name of the database used by the service | auth |
|
||||
| `SMQ_AUTH_DB_SSL_MODE` | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| `SMQ_AUTH_DB_SSL_CERT` | Path to the PEM encoded certificate file | "" |
|
||||
| `SMQ_AUTH_DB_SSL_KEY` | Path to the PEM encoded key file | "" |
|
||||
| `SMQ_AUTH_DB_SSL_ROOT_CERT` | Path to the PEM encoded root certificate file | "" |
|
||||
| `SMQ_AUTH_HTTP_HOST` | Auth service HTTP host | "" |
|
||||
| `SMQ_AUTH_HTTP_PORT` | Auth service HTTP port | 8189 |
|
||||
| `SMQ_AUTH_HTTP_SERVER_CERT` | Path to the PEM encoded HTTP server certificate file | "" |
|
||||
| `SMQ_AUTH_HTTP_SERVER_KEY` | Path to the PEM encoded HTTP server key file | "" |
|
||||
| `SMQ_AUTH_GRPC_HOST` | Auth service gRPC host | "" |
|
||||
| `SMQ_AUTH_GRPC_PORT` | Auth service gRPC port | 8181 |
|
||||
| `SMQ_AUTH_GRPC_SERVER_CERT` | Path to the PEM encoded gRPC server certificate file | "" |
|
||||
| `SMQ_AUTH_GRPC_SERVER_KEY` | Path to the PEM encoded gRPC server key file | "" |
|
||||
| `SMQ_AUTH_GRPC_SERVER_CA_CERTS` | Path to the PEM encoded gRPC server CA certificate file | "" |
|
||||
| `SMQ_AUTH_GRPC_CLIENT_CA_CERTS` | Path to the PEM encoded gRPC client CA certificate file | "" |
|
||||
| `SMQ_AUTH_SECRET_KEY` | String used for signing tokens | secret |
|
||||
| `SMQ_AUTH_ACCESS_TOKEN_DURATION` | The access token expiration period | 1h |
|
||||
| `SMQ_AUTH_REFRESH_TOKEN_DURATION` | The refresh token expiration period | 24h |
|
||||
| `SMQ_AUTH_INVITATION_DURATION` | The invitation token expiration period | 168h |
|
||||
| `SMQ_AUTH_CACHE_URL` | Redis URL for caching PAT scopes | redis://localhost:6379/0 |
|
||||
| `SMQ_AUTH_CACHE_KEY_DURATION` | Duration for which PAT scope cache keys are valid | 10m |
|
||||
| `SMQ_SPICEDB_HOST` | SpiceDB host address | localhost |
|
||||
| `SMQ_SPICEDB_PORT` | SpiceDB host port | 50051 |
|
||||
| `SMQ_SPICEDB_PRE_SHARED_KEY` | SpiceDB pre-shared key | 12345678 |
|
||||
| `SMQ_SPICEDB_SCHEMA_FILE` | Path to SpiceDB schema file | ./docker/spicedb/schema.zed |
|
||||
| `SMQ_JAEGER_URL` | Jaeger server URL | <http://jaeger:4318/v1/traces> |
|
||||
| `SMQ_JAEGER_TRACE_RATIO` | Jaeger sampling ratio | 1.0 |
|
||||
| `SMQ_SEND_TELEMETRY` | Send telemetry to supermq call home server | true |
|
||||
| `SMQ_ADAPTER_INSTANCE_ID` | Adapter instance ID | "" |
|
||||
| `SMQ_CALLOUT_URLS` | Comma-separated list of callout URLs | "" |
|
||||
| `SMQ_CALLOUT_METHOD` | Callout method | POST |
|
||||
| `SMQ_CALLOUT_TLS_VERIFICATION` | Enable TLS verification for callouts | true |
|
||||
| `SMQ_CALLOUT_TIMEOUT` | Callout timeout | 10s |
|
||||
| `SMQ_CALLOUT_CA_CERT` | Path to CA certificate file | "" |
|
||||
| `SMQ_CALLOUT_CERT` | Path to client certificate file | "" |
|
||||
| `SMQ_CALLOUT_KEY` | Path to client key file | "" |
|
||||
| `SMQ_CALLOUT_OPERATIONS` | Invoke callout if the authorization permission matches any of the given permissions. | "" |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -183,7 +183,7 @@ PATs in SuperMQ are designed with the following features:
|
||||
|
||||
A PAT consists of three parts separated by underscores:
|
||||
|
||||
```
|
||||
```bash
|
||||
pat_<encoded-user-and-pat-id>_<random-string>
|
||||
```
|
||||
|
||||
@@ -318,7 +318,7 @@ curl --location --request PATCH 'http://localhost:9001/pats/a2500226-95dc-4285-8
|
||||
|
||||
When making API requests, include the PAT in the Authorization header:
|
||||
|
||||
```
|
||||
```bash
|
||||
Authorization: Bearer pat_<encoded-user-and-pat-id>_<random-string>
|
||||
```
|
||||
|
||||
@@ -444,6 +444,6 @@ When a PAT is used for authentication:
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out the [API documentation](https://docs.api.supermq.abstractmachines.fr/?urls.primaryName=auth.yaml).
|
||||
For more information about service capabilities and its usage, please check out the [API documentation](https://docs.api.supermq.absmach.eu/?urls.primaryName=auth.yaml).
|
||||
|
||||
[doc]: https://docs.supermq.abstractmachines.fr
|
||||
[doc]: https://docs.supermq.absmach.eu/
|
||||
|
||||
@@ -18,11 +18,9 @@ import (
|
||||
const authSvcName = "auth.v1.AuthService"
|
||||
|
||||
type authGrpcClient struct {
|
||||
authenticate endpoint.Endpoint
|
||||
authenticatePAT endpoint.Endpoint
|
||||
authorize endpoint.Endpoint
|
||||
authorizePAT endpoint.Endpoint
|
||||
timeout time.Duration
|
||||
authenticate endpoint.Endpoint
|
||||
authorize endpoint.Endpoint
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
var _ grpcAuthV1.AuthServiceClient = (*authGrpcClient)(nil)
|
||||
@@ -38,14 +36,6 @@ func NewAuthClient(conn *grpc.ClientConn, timeout time.Duration) grpcAuthV1.Auth
|
||||
decodeIdentifyResponse,
|
||||
grpcAuthV1.AuthNRes{},
|
||||
).Endpoint(),
|
||||
authenticatePAT: kitgrpc.NewClient(
|
||||
conn,
|
||||
authSvcName,
|
||||
"AuthenticatePAT",
|
||||
encodeIdentifyRequest,
|
||||
decodeIdentifyPATResponse,
|
||||
grpcAuthV1.AuthNRes{},
|
||||
).Endpoint(),
|
||||
authorize: kitgrpc.NewClient(
|
||||
conn,
|
||||
authSvcName,
|
||||
@@ -54,14 +44,6 @@ func NewAuthClient(conn *grpc.ClientConn, timeout time.Duration) grpcAuthV1.Auth
|
||||
decodeAuthorizeResponse,
|
||||
grpcAuthV1.AuthZRes{},
|
||||
).Endpoint(),
|
||||
authorizePAT: kitgrpc.NewClient(
|
||||
conn,
|
||||
authSvcName,
|
||||
"AuthorizePAT",
|
||||
encodeAuthorizePATRequest,
|
||||
decodeAuthorizeResponse,
|
||||
grpcAuthV1.AuthZRes{},
|
||||
).Endpoint(),
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
@@ -88,37 +70,39 @@ func decodeIdentifyResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
return authenticateRes{id: res.GetId(), userID: res.GetUserId(), userRole: auth.Role(res.UserRole), verified: res.GetVerified()}, nil
|
||||
}
|
||||
|
||||
func (client authGrpcClient) AuthenticatePAT(ctx context.Context, token *grpcAuthV1.AuthNReq, _ ...grpc.CallOption) (*grpcAuthV1.AuthNRes, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.authenticatePAT(ctx, authenticateReq{token: token.GetToken()})
|
||||
if err != nil {
|
||||
return &grpcAuthV1.AuthNRes{}, grpcapi.DecodeError(err)
|
||||
}
|
||||
ir := res.(authenticateRes)
|
||||
return &grpcAuthV1.AuthNRes{Id: ir.id, UserId: ir.userID, UserRole: uint32(ir.userRole)}, nil
|
||||
}
|
||||
|
||||
func decodeIdentifyPATResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
res := grpcRes.(*grpcAuthV1.AuthNRes)
|
||||
return authenticateRes{id: res.GetId(), userID: res.GetUserId(), userRole: auth.Role(res.UserRole)}, nil
|
||||
}
|
||||
|
||||
func (client authGrpcClient) Authorize(ctx context.Context, req *grpcAuthV1.AuthZReq, _ ...grpc.CallOption) (r *grpcAuthV1.AuthZRes, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.authorize(ctx, authReq{
|
||||
Domain: req.GetDomain(),
|
||||
SubjectType: req.GetSubjectType(),
|
||||
Subject: req.GetSubject(),
|
||||
SubjectKind: req.GetSubjectKind(),
|
||||
Relation: req.GetRelation(),
|
||||
Permission: req.GetPermission(),
|
||||
ObjectType: req.GetObjectType(),
|
||||
Object: req.GetObject(),
|
||||
})
|
||||
var authReqData authReq
|
||||
|
||||
if req != nil {
|
||||
policyReq := req.GetPolicyReq()
|
||||
patReq := req.GetPatReq()
|
||||
|
||||
if policyReq != nil {
|
||||
authReqData = authReq{
|
||||
Domain: policyReq.GetDomain(),
|
||||
SubjectType: policyReq.GetSubjectType(),
|
||||
Subject: policyReq.GetSubject(),
|
||||
SubjectKind: policyReq.GetSubjectKind(),
|
||||
Relation: policyReq.GetRelation(),
|
||||
Permission: policyReq.GetPermission(),
|
||||
ObjectType: policyReq.GetObjectType(),
|
||||
Object: policyReq.GetObject(),
|
||||
}
|
||||
}
|
||||
|
||||
if patReq != nil {
|
||||
authReqData.UserID = patReq.GetUserId()
|
||||
authReqData.PatID = patReq.GetPatId()
|
||||
authReqData.EntityType = patReq.GetEntityType()
|
||||
authReqData.Operation = patReq.GetOperation()
|
||||
authReqData.EntityID = patReq.GetEntityId()
|
||||
}
|
||||
}
|
||||
|
||||
res, err := client.authorize(ctx, authReqData)
|
||||
if err != nil {
|
||||
return &grpcAuthV1.AuthZRes{}, grpcapi.DecodeError(err)
|
||||
}
|
||||
@@ -134,46 +118,30 @@ func decodeAuthorizeResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
|
||||
func encodeAuthorizeRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(authReq)
|
||||
return &grpcAuthV1.AuthZReq{
|
||||
Domain: req.Domain,
|
||||
SubjectType: req.SubjectType,
|
||||
Subject: req.Subject,
|
||||
SubjectKind: req.SubjectKind,
|
||||
Relation: req.Relation,
|
||||
Permission: req.Permission,
|
||||
ObjectType: req.ObjectType,
|
||||
Object: req.Object,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client authGrpcClient) AuthorizePAT(ctx context.Context, req *grpcAuthV1.AuthZPatReq, _ ...grpc.CallOption) (r *grpcAuthV1.AuthZRes, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.authorizePAT(ctx, authPATReq{
|
||||
userID: req.GetUserId(),
|
||||
patID: req.GetPatId(),
|
||||
entityType: auth.EntityType(req.GetEntityType()),
|
||||
optionalDomainID: req.GetOptionalDomainId(),
|
||||
operation: auth.Operation(req.GetOperation()),
|
||||
entityID: req.GetEntityId(),
|
||||
})
|
||||
if err != nil {
|
||||
return &grpcAuthV1.AuthZRes{}, grpcapi.DecodeError(err)
|
||||
authZReq := &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Domain: req.Domain,
|
||||
SubjectType: req.SubjectType,
|
||||
Subject: req.Subject,
|
||||
SubjectKind: req.SubjectKind,
|
||||
Relation: req.Relation,
|
||||
Permission: req.Permission,
|
||||
ObjectType: req.ObjectType,
|
||||
Object: req.Object,
|
||||
},
|
||||
}
|
||||
|
||||
ar := res.(authorizeRes)
|
||||
return &grpcAuthV1.AuthZRes{Authorized: ar.authorized, Id: ar.id}, nil
|
||||
}
|
||||
if req.PatID != "" {
|
||||
authZReq.PatReq = &grpcAuthV1.PATReq{
|
||||
PatId: req.PatID,
|
||||
Domain: req.Domain,
|
||||
Operation: req.Operation,
|
||||
UserId: req.UserID,
|
||||
EntityId: req.EntityID,
|
||||
EntityType: req.EntityType,
|
||||
}
|
||||
}
|
||||
|
||||
func encodeAuthorizePATRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(authPATReq)
|
||||
return &grpcAuthV1.AuthZPatReq{
|
||||
UserId: req.userID,
|
||||
PatId: req.patID,
|
||||
EntityType: uint32(req.entityType),
|
||||
OptionalDomainId: req.optionalDomainID,
|
||||
Operation: uint32(req.operation),
|
||||
EntityId: req.entityID,
|
||||
}, nil
|
||||
return authZReq, nil
|
||||
}
|
||||
|
||||
@@ -23,23 +23,7 @@ func authenticateEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return authenticateRes{}, err
|
||||
}
|
||||
|
||||
return authenticateRes{userID: key.Subject, userRole: key.Role, verified: key.Verified}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func authenticatePATEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(authenticateReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return authenticateRes{}, err
|
||||
}
|
||||
|
||||
pat, err := svc.IdentifyPAT(ctx, req.token)
|
||||
if err != nil {
|
||||
return authenticateRes{}, err
|
||||
}
|
||||
|
||||
return authenticateRes{id: pat.ID, userID: pat.User, userRole: pat.Role}, nil
|
||||
return authenticateRes{id: key.ID, userID: key.Subject, userRole: key.Role, verified: key.Verified}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +34,23 @@ func authorizeEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
if err := req.validate(); err != nil {
|
||||
return authorizeRes{}, err
|
||||
}
|
||||
|
||||
var pat *auth.PATAuthz
|
||||
if req.PatID != "" {
|
||||
entityType, err := auth.ParseEntityType(req.EntityType)
|
||||
if err != nil {
|
||||
return authorizeRes{authorized: false}, err
|
||||
}
|
||||
pat = &auth.PATAuthz{
|
||||
PatID: req.PatID,
|
||||
UserID: req.UserID,
|
||||
EntityType: entityType,
|
||||
EntityID: req.EntityID,
|
||||
Operation: req.Operation,
|
||||
Domain: req.Domain,
|
||||
}
|
||||
}
|
||||
|
||||
err := svc.Authorize(ctx, policies.Policy{
|
||||
Domain: req.Domain,
|
||||
SubjectType: req.SubjectType,
|
||||
@@ -59,22 +60,7 @@ func authorizeEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
Permission: req.Permission,
|
||||
ObjectType: req.ObjectType,
|
||||
Object: req.Object,
|
||||
})
|
||||
if err != nil {
|
||||
return authorizeRes{authorized: false}, err
|
||||
}
|
||||
return authorizeRes{authorized: true}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func authorizePATEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(authPATReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return authorizeRes{}, err
|
||||
}
|
||||
err := svc.AuthorizePAT(ctx, req.userID, req.patID, req.entityType, req.optionalDomainID, req.operation, req.entityID)
|
||||
}, pat)
|
||||
if err != nil {
|
||||
return authorizeRes{authorized: false}, err
|
||||
}
|
||||
|
||||
+175
-191
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/absmach/supermq/internal/testsutil"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/grpc"
|
||||
@@ -25,25 +26,14 @@ import (
|
||||
|
||||
const (
|
||||
port = 8081
|
||||
secret = "secret"
|
||||
email = "test@example.com"
|
||||
id = "testID"
|
||||
clientsType = "clients"
|
||||
usersType = "users"
|
||||
description = "Description"
|
||||
groupName = "smqx"
|
||||
adminPermission = "admin"
|
||||
|
||||
authoritiesObj = "authorities"
|
||||
memberRelation = "member"
|
||||
loginDuration = 30 * time.Minute
|
||||
refreshDuration = 24 * time.Hour
|
||||
invalidDuration = 7 * 24 * time.Hour
|
||||
validToken = "valid"
|
||||
inValidToken = "invalid"
|
||||
validPATToken = "valid"
|
||||
inValidPATToken = "invalid"
|
||||
validPolicy = "valid"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -73,6 +63,7 @@ func TestIdentify(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
key auth.Key
|
||||
idt *grpcAuthV1.AuthNRes
|
||||
svcErr error
|
||||
err error
|
||||
@@ -80,12 +71,14 @@ func TestIdentify(t *testing.T) {
|
||||
{
|
||||
desc: "authenticate user with valid user token",
|
||||
token: validToken,
|
||||
key: auth.Key{ID: "", Subject: id, Role: auth.UserRole},
|
||||
idt: &grpcAuthV1.AuthNRes{UserId: id, UserRole: uint32(auth.UserRole)},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authenticate user with invalid user token",
|
||||
token: "invalid",
|
||||
key: auth.Key{},
|
||||
idt: &grpcAuthV1.AuthNRes{},
|
||||
svcErr: svcerr.ErrAuthentication,
|
||||
err: svcerr.ErrAuthentication,
|
||||
@@ -96,11 +89,26 @@ func TestIdentify(t *testing.T) {
|
||||
idt: &grpcAuthV1.AuthNRes{},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "authenticate user with valid PAT token",
|
||||
token: "pat_" + validPATToken,
|
||||
key: auth.Key{ID: id, Type: auth.PersonalAccessToken, Subject: clientID, Role: auth.UserRole},
|
||||
idt: &grpcAuthV1.AuthNRes{Id: id, UserId: clientID, UserRole: uint32(auth.UserRole)},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authenticate user with invalid PAT token",
|
||||
token: "pat_invalid",
|
||||
key: auth.Key{},
|
||||
idt: &grpcAuthV1.AuthNRes{},
|
||||
svcErr: svcerr.ErrAuthentication,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
svcCall := svc.On("Identify", mock.Anything, mock.Anything).Return(auth.Key{Subject: id, Role: auth.UserRole}, tc.svcErr)
|
||||
svcCall := svc.On("Identify", mock.Anything, tc.token).Return(tc.key, tc.svcErr)
|
||||
idt, err := grpcClient.Authenticate(context.Background(), &grpcAuthV1.AuthNReq{Token: tc.token})
|
||||
if idt != nil {
|
||||
assert.Equal(t, tc.idt, idt, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.idt, idt))
|
||||
@@ -129,12 +137,14 @@ func TestAuthorize(t *testing.T) {
|
||||
desc: "authorize user with authorized token",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: true},
|
||||
err: nil,
|
||||
@@ -143,12 +153,14 @@ func TestAuthorize(t *testing.T) {
|
||||
desc: "authorize user with unauthorized token",
|
||||
token: inValidToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: svcerr.ErrAuthorization,
|
||||
@@ -157,12 +169,14 @@ func TestAuthorize(t *testing.T) {
|
||||
desc: "authorize user with empty subject",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
Subject: "",
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: "",
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPolicySub,
|
||||
@@ -171,12 +185,14 @@ func TestAuthorize(t *testing.T) {
|
||||
desc: "authorize user with empty subject type",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
Subject: id,
|
||||
SubjectType: "",
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: "",
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPolicySub,
|
||||
@@ -185,12 +201,14 @@ func TestAuthorize(t *testing.T) {
|
||||
desc: "authorize user with empty object",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: "",
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: "",
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPolicyObj,
|
||||
@@ -199,12 +217,14 @@ func TestAuthorize(t *testing.T) {
|
||||
desc: "authorize user with empty object type",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: "",
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: "",
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPolicyObj,
|
||||
@@ -213,20 +233,120 @@ func TestAuthorize(t *testing.T) {
|
||||
desc: "authorize user with empty permission",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: "",
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: "",
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMalformedPolicyPer,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with valid PAT token",
|
||||
token: validPATToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Permission: policies.ViewPermission,
|
||||
ObjectType: policies.ClientType,
|
||||
Domain: domainID,
|
||||
Object: clientID,
|
||||
},
|
||||
PatReq: &grpcAuthV1.PATReq{
|
||||
PatId: id,
|
||||
Domain: domainID,
|
||||
Operation: "view",
|
||||
UserId: id,
|
||||
EntityId: clientID,
|
||||
EntityType: auth.ClientsScopeStr,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: true},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with unauthorized PAT token",
|
||||
token: inValidToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Permission: policies.ViewPermission,
|
||||
ObjectType: policies.ClientType,
|
||||
Domain: domainID,
|
||||
Object: clientID,
|
||||
},
|
||||
PatReq: &grpcAuthV1.PATReq{
|
||||
PatId: id,
|
||||
Domain: domainID,
|
||||
Operation: "view",
|
||||
UserId: id,
|
||||
EntityId: clientID,
|
||||
EntityType: auth.ClientsScopeStr,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: svcerr.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "authorize PAT with missing user id",
|
||||
token: validPATToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Permission: policies.ViewPermission,
|
||||
ObjectType: policies.ClientType,
|
||||
Domain: domainID,
|
||||
Object: clientID,
|
||||
},
|
||||
PatReq: &grpcAuthV1.PATReq{
|
||||
PatId: id,
|
||||
Domain: domainID,
|
||||
Operation: "view",
|
||||
EntityId: clientID,
|
||||
EntityType: auth.ClientsScopeStr,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingUserID,
|
||||
},
|
||||
{
|
||||
desc: "authorize PAT with missing entity id",
|
||||
token: validPATToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Permission: policies.ViewPermission,
|
||||
ObjectType: policies.ClientType,
|
||||
Domain: domainID,
|
||||
Object: clientID,
|
||||
},
|
||||
PatReq: &grpcAuthV1.PATReq{
|
||||
PatId: id,
|
||||
Domain: domainID,
|
||||
Operation: "view",
|
||||
UserId: id,
|
||||
EntityType: auth.ClientsScopeStr,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
svcCall := svc.On("Authorize", mock.Anything, mock.Anything).Return(tc.err)
|
||||
svcCall := svc.On("Authorize", mock.Anything, mock.Anything, mock.Anything).Return(tc.err)
|
||||
ar, err := grpcClient.Authorize(context.Background(), tc.authRequest)
|
||||
if ar != nil {
|
||||
assert.Equal(t, tc.authResponse, ar, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.authResponse, ar))
|
||||
@@ -236,139 +356,3 @@ func TestAuthorize(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentifyPAT(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
defer conn.Close()
|
||||
grpcClient := grpcapi.NewAuthClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
idt *grpcAuthV1.AuthNRes
|
||||
svcErr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "authenticate user with valid user token",
|
||||
token: validToken,
|
||||
idt: &grpcAuthV1.AuthNRes{Id: id, UserId: clientID},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authenticate user with invalid user token",
|
||||
token: "invalid",
|
||||
idt: &grpcAuthV1.AuthNRes{},
|
||||
svcErr: svcerr.ErrAuthentication,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "authenticate user with empty token",
|
||||
token: "",
|
||||
idt: &grpcAuthV1.AuthNRes{},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
svcCall := svc.On("IdentifyPAT", mock.Anything, tc.token).Return(auth.PAT{ID: id, User: clientID, IssuedAt: time.Now()}, tc.svcErr)
|
||||
idt, err := grpcClient.AuthenticatePAT(context.Background(), &grpcAuthV1.AuthNReq{Token: tc.token})
|
||||
if idt != nil {
|
||||
assert.Equal(t, tc.idt, idt, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.idt, idt))
|
||||
}
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizePAT(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
defer conn.Close()
|
||||
|
||||
grpcClient := grpcapi.NewAuthClient(conn, time.Second)
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
authRequest *grpcAuthV1.AuthZPatReq
|
||||
authResponse *grpcAuthV1.AuthZRes
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "authorize user with authorized token",
|
||||
token: validPATToken,
|
||||
authRequest: &grpcAuthV1.AuthZPatReq{
|
||||
UserId: id,
|
||||
PatId: id,
|
||||
EntityType: uint32(auth.ClientsType),
|
||||
OptionalDomainId: domainID,
|
||||
Operation: uint32(auth.CreateOp),
|
||||
EntityId: clientID,
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: true},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with unauthorized token",
|
||||
token: inValidPATToken,
|
||||
authRequest: &grpcAuthV1.AuthZPatReq{
|
||||
UserId: id,
|
||||
PatId: id,
|
||||
EntityType: uint32(auth.ClientsType),
|
||||
OptionalDomainId: domainID,
|
||||
Operation: uint32(auth.CreateOp),
|
||||
EntityId: clientID,
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: svcerr.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with missing user id",
|
||||
token: validPATToken,
|
||||
authRequest: &grpcAuthV1.AuthZPatReq{
|
||||
PatId: id,
|
||||
EntityType: uint32(auth.ClientsType),
|
||||
OptionalDomainId: domainID,
|
||||
Operation: uint32(auth.CreateOp),
|
||||
EntityId: clientID,
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingUserID,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with missing pat id",
|
||||
token: validPATToken,
|
||||
authRequest: &grpcAuthV1.AuthZPatReq{
|
||||
UserId: id,
|
||||
EntityType: uint32(auth.ClientsType),
|
||||
OptionalDomainId: domainID,
|
||||
Operation: uint32(auth.CreateOp),
|
||||
EntityId: clientID,
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPATID,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
svcCall := svc.On("AuthorizePAT",
|
||||
mock.Anything,
|
||||
tc.authRequest.UserId,
|
||||
tc.authRequest.PatId,
|
||||
mock.Anything,
|
||||
tc.authRequest.OptionalDomainId,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything).Return(tc.err)
|
||||
ar, err := grpcClient.AuthorizePAT(context.Background(), tc.authRequest)
|
||||
if ar != nil {
|
||||
assert.Equal(t, tc.authResponse, ar, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.authResponse, ar))
|
||||
}
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package auth
|
||||
|
||||
import (
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/auth"
|
||||
)
|
||||
|
||||
type authenticateReq struct {
|
||||
@@ -33,9 +32,28 @@ type authReq struct {
|
||||
Permission string
|
||||
ObjectType string
|
||||
Object string
|
||||
|
||||
// PAT authorization fields
|
||||
UserID string
|
||||
PatID string
|
||||
EntityType string
|
||||
Operation string
|
||||
EntityID string
|
||||
}
|
||||
|
||||
func (req authReq) validate() error {
|
||||
if req.PatID != "" {
|
||||
if req.UserID == "" {
|
||||
return apiutil.ErrMissingUserID
|
||||
}
|
||||
if req.EntityID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
if req.EntityType == "" {
|
||||
return apiutil.ErrMissingPolicyObj
|
||||
}
|
||||
}
|
||||
|
||||
if req.Subject == "" || req.SubjectType == "" {
|
||||
return apiutil.ErrMissingPolicySub
|
||||
}
|
||||
@@ -50,22 +68,3 @@ func (req authReq) validate() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type authPATReq struct {
|
||||
userID string
|
||||
patID string
|
||||
entityType auth.EntityType
|
||||
optionalDomainID string
|
||||
operation auth.Operation
|
||||
entityID string
|
||||
}
|
||||
|
||||
func (req authPATReq) validate() error {
|
||||
if req.userID == "" {
|
||||
return apiutil.ErrMissingUserID
|
||||
}
|
||||
if req.patID == "" {
|
||||
return apiutil.ErrMissingPATID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,10 +16,8 @@ var _ grpcAuthV1.AuthServiceServer = (*authGrpcServer)(nil)
|
||||
|
||||
type authGrpcServer struct {
|
||||
grpcAuthV1.UnimplementedAuthServiceServer
|
||||
authorize kitgrpc.Handler
|
||||
authenticate kitgrpc.Handler
|
||||
authenticatePAT kitgrpc.Handler
|
||||
authorizePAT kitgrpc.Handler
|
||||
authorize kitgrpc.Handler
|
||||
authenticate kitgrpc.Handler
|
||||
}
|
||||
|
||||
// NewAuthServer returns new AuthnServiceServer instance.
|
||||
@@ -36,18 +34,6 @@ func NewAuthServer(svc auth.Service) grpcAuthV1.AuthServiceServer {
|
||||
decodeAuthenticateRequest,
|
||||
encodeAuthenticateResponse,
|
||||
),
|
||||
|
||||
authenticatePAT: kitgrpc.NewServer(
|
||||
(authenticatePATEndpoint(svc)),
|
||||
decodeAuthenticateRequest,
|
||||
encodeAuthenticatePATResponse,
|
||||
),
|
||||
|
||||
authorizePAT: kitgrpc.NewServer(
|
||||
(authorizePATEndpoint(svc)),
|
||||
decodeAuthorizePATRequest,
|
||||
encodeAuthorizeResponse,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,14 +45,6 @@ func (s *authGrpcServer) Authenticate(ctx context.Context, req *grpcAuthV1.AuthN
|
||||
return res.(*grpcAuthV1.AuthNRes), nil
|
||||
}
|
||||
|
||||
func (s *authGrpcServer) AuthenticatePAT(ctx context.Context, req *grpcAuthV1.AuthNReq) (*grpcAuthV1.AuthNRes, error) {
|
||||
_, res, err := s.authenticatePAT.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, grpcapi.EncodeError(err)
|
||||
}
|
||||
return res.(*grpcAuthV1.AuthNRes), nil
|
||||
}
|
||||
|
||||
func (s *authGrpcServer) Authorize(ctx context.Context, req *grpcAuthV1.AuthZReq) (*grpcAuthV1.AuthZRes, error) {
|
||||
_, res, err := s.authorize.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
@@ -85,46 +63,42 @@ func encodeAuthenticateResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
return &grpcAuthV1.AuthNRes{Id: res.id, UserId: res.userID, UserRole: uint32(res.userRole), Verified: res.verified}, nil
|
||||
}
|
||||
|
||||
func encodeAuthenticatePATResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
res := grpcRes.(authenticateRes)
|
||||
return &grpcAuthV1.AuthNRes{Id: res.id, UserId: res.userID, UserRole: uint32(res.userRole)}, nil
|
||||
}
|
||||
|
||||
func decodeAuthorizeRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(*grpcAuthV1.AuthZReq)
|
||||
return authReq{
|
||||
Domain: req.GetDomain(),
|
||||
SubjectType: req.GetSubjectType(),
|
||||
SubjectKind: req.GetSubjectKind(),
|
||||
Subject: req.GetSubject(),
|
||||
Relation: req.GetRelation(),
|
||||
Permission: req.GetPermission(),
|
||||
ObjectType: req.GetObjectType(),
|
||||
Object: req.GetObject(),
|
||||
}, nil
|
||||
if req == nil {
|
||||
return authReq{}, nil
|
||||
}
|
||||
|
||||
policyReq := req.GetPolicyReq()
|
||||
patReq := req.GetPatReq()
|
||||
|
||||
if policyReq == nil {
|
||||
return authReq{}, nil
|
||||
}
|
||||
|
||||
authRequest := authReq{
|
||||
Domain: policyReq.GetDomain(),
|
||||
SubjectType: policyReq.GetSubjectType(),
|
||||
SubjectKind: policyReq.GetSubjectKind(),
|
||||
Subject: policyReq.GetSubject(),
|
||||
Relation: policyReq.GetRelation(),
|
||||
Permission: policyReq.GetPermission(),
|
||||
ObjectType: policyReq.GetObjectType(),
|
||||
Object: policyReq.GetObject(),
|
||||
}
|
||||
|
||||
if patReq != nil {
|
||||
authRequest.UserID = patReq.GetUserId()
|
||||
authRequest.PatID = patReq.GetPatId()
|
||||
authRequest.EntityType = patReq.GetEntityType()
|
||||
authRequest.Operation = patReq.GetOperation()
|
||||
authRequest.EntityID = patReq.GetEntityId()
|
||||
}
|
||||
|
||||
return authRequest, nil
|
||||
}
|
||||
|
||||
func encodeAuthorizeResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
res := grpcRes.(authorizeRes)
|
||||
return &grpcAuthV1.AuthZRes{Authorized: res.authorized, Id: res.id}, nil
|
||||
}
|
||||
|
||||
func decodeAuthorizePATRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(*grpcAuthV1.AuthZPatReq)
|
||||
return authPATReq{
|
||||
userID: req.GetUserId(),
|
||||
patID: req.GetPatId(),
|
||||
entityType: auth.EntityType(req.GetEntityType()),
|
||||
optionalDomainID: req.GetOptionalDomainId(),
|
||||
operation: auth.Operation(req.GetOperation()),
|
||||
entityID: req.GetEntityId(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *authGrpcServer) AuthorizePAT(ctx context.Context, req *grpcAuthV1.AuthZPatReq) (*grpcAuthV1.AuthZRes, error) {
|
||||
_, res, err := s.authorizePAT.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, grpcapi.EncodeError(err)
|
||||
}
|
||||
return res.(*grpcAuthV1.AuthZRes), nil
|
||||
}
|
||||
|
||||
@@ -18,14 +18,16 @@ import (
|
||||
const tokenSvcName = "token.v1.TokenService"
|
||||
|
||||
type tokenGrpcClient struct {
|
||||
issue endpoint.Endpoint
|
||||
refresh endpoint.Endpoint
|
||||
timeout time.Duration
|
||||
issue endpoint.Endpoint
|
||||
refresh endpoint.Endpoint
|
||||
revoke endpoint.Endpoint
|
||||
listUserRefreshTokens endpoint.Endpoint
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
var _ grpcTokenV1.TokenServiceClient = (*tokenGrpcClient)(nil)
|
||||
|
||||
// NewAuthClient returns new auth gRPC client instance.
|
||||
// NewTokenClient returns new token gRPC client instance.
|
||||
func NewTokenClient(conn *grpc.ClientConn, timeout time.Duration) grpcTokenV1.TokenServiceClient {
|
||||
return &tokenGrpcClient{
|
||||
issue: kitgrpc.NewClient(
|
||||
@@ -44,6 +46,22 @@ func NewTokenClient(conn *grpc.ClientConn, timeout time.Duration) grpcTokenV1.To
|
||||
decodeRefreshResponse,
|
||||
grpcTokenV1.Token{},
|
||||
).Endpoint(),
|
||||
revoke: kitgrpc.NewClient(
|
||||
conn,
|
||||
tokenSvcName,
|
||||
"Revoke",
|
||||
encodeRevokeRequest,
|
||||
decodeRevokeResponse,
|
||||
grpcTokenV1.RevokeRes{},
|
||||
).Endpoint(),
|
||||
listUserRefreshTokens: kitgrpc.NewClient(
|
||||
conn,
|
||||
tokenSvcName,
|
||||
"ListUserRefreshTokens",
|
||||
encodeListUserRefreshTokensRequest,
|
||||
decodeListUserRefreshTokensResponse,
|
||||
grpcTokenV1.ListUserRefreshTokensRes{},
|
||||
).Endpoint(),
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
@@ -53,10 +71,11 @@ func (client tokenGrpcClient) Issue(ctx context.Context, req *grpcTokenV1.IssueR
|
||||
defer cancel()
|
||||
|
||||
res, err := client.issue(ctx, issueReq{
|
||||
userID: req.GetUserId(),
|
||||
userRole: auth.Role(req.GetUserRole()),
|
||||
keyType: auth.KeyType(req.GetType()),
|
||||
verified: req.GetVerified(),
|
||||
userID: req.GetUserId(),
|
||||
userRole: auth.Role(req.GetUserRole()),
|
||||
keyType: auth.KeyType(req.GetType()),
|
||||
verified: req.GetVerified(),
|
||||
description: req.GetDescription(),
|
||||
})
|
||||
if err != nil {
|
||||
return &grpcTokenV1.Token{}, grpcapi.DecodeError(err)
|
||||
@@ -67,10 +86,11 @@ func (client tokenGrpcClient) Issue(ctx context.Context, req *grpcTokenV1.IssueR
|
||||
func encodeIssueRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(issueReq)
|
||||
return &grpcTokenV1.IssueReq{
|
||||
UserId: req.userID,
|
||||
UserRole: uint32(req.userRole),
|
||||
Type: uint32(req.keyType),
|
||||
Verified: req.verified,
|
||||
UserId: req.userID,
|
||||
UserRole: uint32(req.userRole),
|
||||
Type: uint32(req.keyType),
|
||||
Verified: req.verified,
|
||||
Description: req.description,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -97,3 +117,43 @@ func encodeRefreshRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
func decodeRefreshResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
return grpcRes, nil
|
||||
}
|
||||
|
||||
func (client tokenGrpcClient) Revoke(ctx context.Context, req *grpcTokenV1.RevokeReq, _ ...grpc.CallOption) (*grpcTokenV1.RevokeRes, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.revoke(ctx, revokeReq{userID: req.GetUserId(), tokenID: req.GetTokenId()})
|
||||
if err != nil {
|
||||
return &grpcTokenV1.RevokeRes{}, grpcapi.DecodeError(err)
|
||||
}
|
||||
return res.(*grpcTokenV1.RevokeRes), nil
|
||||
}
|
||||
|
||||
func encodeRevokeRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(revokeReq)
|
||||
return &grpcTokenV1.RevokeReq{UserId: req.userID, TokenId: req.tokenID}, nil
|
||||
}
|
||||
|
||||
func decodeRevokeResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
return grpcRes, nil
|
||||
}
|
||||
|
||||
func (client tokenGrpcClient) ListUserRefreshTokens(ctx context.Context, req *grpcTokenV1.ListUserRefreshTokensReq, _ ...grpc.CallOption) (*grpcTokenV1.ListUserRefreshTokensRes, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, client.timeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.listUserRefreshTokens(ctx, listUserRefreshTokensReq{userID: req.GetUserId()})
|
||||
if err != nil {
|
||||
return &grpcTokenV1.ListUserRefreshTokensRes{}, grpcapi.DecodeError(err)
|
||||
}
|
||||
return res.(*grpcTokenV1.ListUserRefreshTokensRes), nil
|
||||
}
|
||||
|
||||
func encodeListUserRefreshTokensRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(listUserRefreshTokensReq)
|
||||
return &grpcTokenV1.ListUserRefreshTokensReq{UserId: req.userID}, nil
|
||||
}
|
||||
|
||||
func decodeListUserRefreshTokensResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
return grpcRes, nil
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@ func issueEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
key := auth.Key{
|
||||
Type: req.keyType,
|
||||
Subject: req.userID,
|
||||
Role: req.userRole,
|
||||
Verified: req.verified,
|
||||
Type: req.keyType,
|
||||
Subject: req.userID,
|
||||
Role: req.userRole,
|
||||
Verified: req.verified,
|
||||
Description: req.description,
|
||||
}
|
||||
tkn, err := svc.Issue(ctx, "", key)
|
||||
if err != nil {
|
||||
@@ -56,3 +57,34 @@ func refreshEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
|
||||
func revokeEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(revokeReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := svc.RevokeToken(ctx, req.userID, req.tokenID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listUserRefreshTokensEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(listUserRefreshTokensReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return listUserRefreshTokensRes{}, err
|
||||
}
|
||||
|
||||
refreshTokens, err := svc.ListUserRefreshTokens(ctx, req.userID)
|
||||
if err != nil {
|
||||
return listUserRefreshTokensRes{}, err
|
||||
}
|
||||
|
||||
return listUserRefreshTokensRes{refreshTokens: refreshTokens}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,24 +24,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
port = 8082
|
||||
secret = "secret"
|
||||
email = "test@example.com"
|
||||
id = "testID"
|
||||
clientsType = "clients"
|
||||
usersType = "users"
|
||||
description = "Description"
|
||||
groupName = "smqx"
|
||||
adminPermission = "admin"
|
||||
|
||||
authoritiesObj = "authorities"
|
||||
memberRelation = "member"
|
||||
loginDuration = 30 * time.Minute
|
||||
refreshDuration = 24 * time.Hour
|
||||
invalidDuration = 7 * 24 * time.Hour
|
||||
validToken = "valid"
|
||||
inValidToken = "invalid"
|
||||
validPolicy = "valid"
|
||||
port = 8082
|
||||
validToken = "valid"
|
||||
inValidToken = "invalid"
|
||||
invalidID = "invalid"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -63,9 +49,9 @@ func startGRPCServer(svc auth.Service, port int) *grpc.Server {
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
defer conn.Close()
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
defer conn.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
@@ -127,9 +113,9 @@ func TestIssue(t *testing.T) {
|
||||
|
||||
func TestRefresh(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
defer conn.Close()
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
defer conn.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
@@ -161,9 +147,99 @@ func TestRefresh(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("Issue", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.issueResponse, tc.err)
|
||||
svcCall := svc.On("Issue", mock.Anything, mock.Anything, mock.Anything).Return(tc.issueResponse, tc.err)
|
||||
_, err := grpcClient.Refresh(context.Background(), &grpcTokenV1.RefreshReq{RefreshToken: tc.token})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
defer conn.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "revoke token with valid id",
|
||||
id: validID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "revoke token with invalid id",
|
||||
id: invalidID,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "revoke token with empty id",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "revoke already revoked token",
|
||||
id: validID,
|
||||
err: svcerr.ErrConflict,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("RevokeToken", mock.Anything, mock.Anything, tc.id).Return(tc.err)
|
||||
_, err := grpcClient.Revoke(context.Background(), &grpcTokenV1.RevokeReq{TokenId: tc.id})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUserRefreshTokens(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
defer conn.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
userID string
|
||||
listResponse []auth.TokenInfo
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "list tokens for user with valid id",
|
||||
userID: validID,
|
||||
listResponse: []auth.TokenInfo{
|
||||
{ID: testsutil.GenerateUUID(&testing.T{}), Description: "Token 1"},
|
||||
{ID: testsutil.GenerateUUID(&testing.T{}), Description: "Token 2"},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list tokens for user with empty list",
|
||||
userID: validID,
|
||||
listResponse: []auth.TokenInfo{},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list tokens with invalid user id",
|
||||
userID: invalidID,
|
||||
listResponse: nil,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "list tokens with empty user id",
|
||||
userID: "",
|
||||
listResponse: nil,
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("ListUserRefreshTokens", mock.Anything, tc.userID).Return(tc.listResponse, tc.err)
|
||||
_, err := grpcClient.ListUserRefreshTokens(context.Background(), &grpcTokenV1.ListUserRefreshTokensReq{UserId: tc.userID})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@ import (
|
||||
)
|
||||
|
||||
type issueReq struct {
|
||||
userID string
|
||||
userRole auth.Role
|
||||
keyType auth.KeyType
|
||||
verified bool
|
||||
userID string
|
||||
userRole auth.Role
|
||||
keyType auth.KeyType
|
||||
verified bool
|
||||
description string
|
||||
}
|
||||
|
||||
func (req issueReq) validate() error {
|
||||
@@ -38,3 +39,28 @@ func (req refreshReq) validate() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type revokeReq struct {
|
||||
userID string
|
||||
tokenID string
|
||||
}
|
||||
|
||||
func (req revokeReq) validate() error {
|
||||
if req.tokenID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listUserRefreshTokensReq struct {
|
||||
userID string
|
||||
}
|
||||
|
||||
func (req listUserRefreshTokensReq) validate() error {
|
||||
if req.userID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,8 +3,14 @@
|
||||
|
||||
package token
|
||||
|
||||
import "github.com/absmach/supermq/auth"
|
||||
|
||||
type issueRes struct {
|
||||
accessToken string
|
||||
refreshToken string
|
||||
accessType string
|
||||
}
|
||||
|
||||
type listUserRefreshTokensRes struct {
|
||||
refreshTokens []auth.TokenInfo
|
||||
}
|
||||
|
||||
@@ -16,11 +16,13 @@ var _ grpcTokenV1.TokenServiceServer = (*tokenGrpcServer)(nil)
|
||||
|
||||
type tokenGrpcServer struct {
|
||||
grpcTokenV1.UnimplementedTokenServiceServer
|
||||
issue kitgrpc.Handler
|
||||
refresh kitgrpc.Handler
|
||||
issue kitgrpc.Handler
|
||||
refresh kitgrpc.Handler
|
||||
revoke kitgrpc.Handler
|
||||
listUserRefreshTokens kitgrpc.Handler
|
||||
}
|
||||
|
||||
// NewAuthServer returns new AuthnServiceServer instance.
|
||||
// NewTokenServer returns new TokenServiceServer instance.
|
||||
func NewTokenServer(svc auth.Service) grpcTokenV1.TokenServiceServer {
|
||||
return &tokenGrpcServer{
|
||||
issue: kitgrpc.NewServer(
|
||||
@@ -33,6 +35,16 @@ func NewTokenServer(svc auth.Service) grpcTokenV1.TokenServiceServer {
|
||||
decodeRefreshRequest,
|
||||
encodeIssueResponse,
|
||||
),
|
||||
revoke: kitgrpc.NewServer(
|
||||
(revokeEndpoint(svc)),
|
||||
decodeRevokeRequest,
|
||||
encodeRevokeResponse,
|
||||
),
|
||||
listUserRefreshTokens: kitgrpc.NewServer(
|
||||
(listUserRefreshTokensEndpoint(svc)),
|
||||
decodeListUserRefreshTokensRequest,
|
||||
encodeListUserRefreshTokensResponse,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +67,11 @@ func (s *tokenGrpcServer) Refresh(ctx context.Context, req *grpcTokenV1.RefreshR
|
||||
func decodeIssueRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(*grpcTokenV1.IssueReq)
|
||||
return issueReq{
|
||||
userID: req.GetUserId(),
|
||||
userRole: auth.Role(req.GetUserRole()),
|
||||
keyType: auth.KeyType(req.GetType()),
|
||||
verified: req.Verified,
|
||||
userID: req.GetUserId(),
|
||||
userRole: auth.Role(req.GetUserRole()),
|
||||
keyType: auth.KeyType(req.GetType()),
|
||||
verified: req.Verified,
|
||||
description: req.GetDescription(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -76,3 +89,49 @@ func encodeIssueResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
AccessType: res.accessType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *tokenGrpcServer) Revoke(ctx context.Context, req *grpcTokenV1.RevokeReq) (*grpcTokenV1.RevokeRes, error) {
|
||||
_, res, err := s.revoke.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, grpcapi.EncodeError(err)
|
||||
}
|
||||
return res.(*grpcTokenV1.RevokeRes), nil
|
||||
}
|
||||
|
||||
func decodeRevokeRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(*grpcTokenV1.RevokeReq)
|
||||
return revokeReq{userID: req.GetUserId(), tokenID: req.GetTokenId()}, nil
|
||||
}
|
||||
|
||||
func encodeRevokeResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
return &grpcTokenV1.RevokeRes{}, nil
|
||||
}
|
||||
|
||||
func (s *tokenGrpcServer) ListUserRefreshTokens(ctx context.Context, req *grpcTokenV1.ListUserRefreshTokensReq) (*grpcTokenV1.ListUserRefreshTokensRes, error) {
|
||||
_, res, err := s.listUserRefreshTokens.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, grpcapi.EncodeError(err)
|
||||
}
|
||||
return res.(*grpcTokenV1.ListUserRefreshTokensRes), nil
|
||||
}
|
||||
|
||||
func decodeListUserRefreshTokensRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
req := grpcReq.(*grpcTokenV1.ListUserRefreshTokensReq)
|
||||
return listUserRefreshTokensReq{userID: req.GetUserId()}, nil
|
||||
}
|
||||
|
||||
func encodeListUserRefreshTokensResponse(_ context.Context, grpcRes any) (any, error) {
|
||||
res := grpcRes.(listUserRefreshTokensRes)
|
||||
|
||||
refreshTokens := make([]*grpcTokenV1.RefreshToken, len(res.refreshTokens))
|
||||
for i, refreshToken := range res.refreshTokens {
|
||||
refreshTokens[i] = &grpcTokenV1.RefreshToken{
|
||||
Id: refreshToken.ID,
|
||||
Description: refreshToken.Description,
|
||||
}
|
||||
}
|
||||
|
||||
return &grpcTokenV1.ListUserRefreshTokensRes{
|
||||
RefreshTokens: refreshTokens,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -85,3 +85,15 @@ func revokeEndpoint(svc auth.Service) endpoint.Endpoint {
|
||||
return revokeKeyRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveJWKSEndpoint(svc auth.Service, jwksCacheMaxAge, jwksCacheStaleWhileRevalidate int) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
jwks := svc.RetrieveJWKS()
|
||||
|
||||
return retrieveJWKSRes{
|
||||
Keys: jwks,
|
||||
CacheMaxAge: jwksCacheMaxAge,
|
||||
CacheStaleWhileRevalidate: jwksCacheStaleWhileRevalidate,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package keys_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -17,30 +16,23 @@ import (
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/auth"
|
||||
httpapi "github.com/absmach/supermq/auth/api/http"
|
||||
"github.com/absmach/supermq/auth/jwt"
|
||||
"github.com/absmach/supermq/auth/mocks"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
policymocks "github.com/absmach/supermq/pkg/policies/mocks"
|
||||
"github.com/absmach/supermq/pkg/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
const (
|
||||
secret = "secret"
|
||||
contentType = "application/json"
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
email = "user@example.com"
|
||||
loginDuration = 30 * time.Minute
|
||||
refreshDuration = 24 * time.Hour
|
||||
invalidDuration = 7 * 24 * time.Hour
|
||||
accessToken = "valid token"
|
||||
)
|
||||
|
||||
var (
|
||||
krepo *mocks.KeyRepository
|
||||
pEvaluator *policymocks.Evaluator
|
||||
)
|
||||
var Token = auth.Token{
|
||||
AccessToken: accessToken,
|
||||
}
|
||||
|
||||
type issueRequest struct {
|
||||
Duration time.Duration `json:"duration,omitempty"`
|
||||
@@ -72,22 +64,11 @@ func (tr testRequest) make() (*http.Response, error) {
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newService() auth.Service {
|
||||
krepo = new(mocks.KeyRepository)
|
||||
pRepo := new(mocks.PATSRepository)
|
||||
cache := new(mocks.Cache)
|
||||
hash := new(mocks.Hasher)
|
||||
idProvider := uuid.NewMock()
|
||||
pService := new(policymocks.Service)
|
||||
pEvaluator = new(policymocks.Evaluator)
|
||||
t := jwt.New([]byte(secret))
|
||||
func newServer() (*httptest.Server, *mocks.Service) {
|
||||
svc := new(mocks.Service)
|
||||
mux := httpapi.MakeHandler(svc, smqlog.NewMock(), "", 900, 60)
|
||||
|
||||
return auth.New(krepo, pRepo, cache, hash, idProvider, t, pEvaluator, pService, loginDuration, refreshDuration, invalidDuration)
|
||||
}
|
||||
|
||||
func newServer(svc auth.Service) *httptest.Server {
|
||||
mux := httpapi.MakeHandler(svc, smqlog.NewMock(), "")
|
||||
return httptest.NewServer(mux)
|
||||
return httptest.NewServer(mux), svc
|
||||
}
|
||||
|
||||
func toJSON(data any) string {
|
||||
@@ -99,13 +80,7 @@ func toJSON(data any) string {
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
svc := newService()
|
||||
policyCall := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(nil)
|
||||
token, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, Role: auth.UserRole, IssuedAt: time.Now(), Subject: id})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
policyCall.Unset()
|
||||
|
||||
ts := newServer(svc)
|
||||
ts, svc := newServer()
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
@@ -119,6 +94,8 @@ func TestIssue(t *testing.T) {
|
||||
ct string
|
||||
token string
|
||||
status int
|
||||
svcRes auth.Token
|
||||
svcErr error
|
||||
}{
|
||||
{
|
||||
desc: "issue login key with empty token",
|
||||
@@ -131,28 +108,30 @@ func TestIssue(t *testing.T) {
|
||||
desc: "issue API key",
|
||||
req: toJSON(ak),
|
||||
ct: contentType,
|
||||
token: token.AccessToken,
|
||||
token: accessToken,
|
||||
status: http.StatusCreated,
|
||||
svcRes: Token,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
req: toJSON(rk),
|
||||
ct: contentType,
|
||||
token: token.AccessToken,
|
||||
token: accessToken,
|
||||
status: http.StatusCreated,
|
||||
svcRes: Token,
|
||||
},
|
||||
{
|
||||
desc: "issue login key wrong content type",
|
||||
req: toJSON(lk),
|
||||
ct: "",
|
||||
token: token.AccessToken,
|
||||
token: accessToken,
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key wrong content type",
|
||||
req: toJSON(rk),
|
||||
ct: "",
|
||||
token: token.AccessToken,
|
||||
token: accessToken,
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
@@ -160,6 +139,7 @@ func TestIssue(t *testing.T) {
|
||||
req: toJSON(ak),
|
||||
ct: contentType,
|
||||
token: "wrong",
|
||||
svcErr: svcerr.ErrAuthentication,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
@@ -167,27 +147,28 @@ func TestIssue(t *testing.T) {
|
||||
req: toJSON(rk),
|
||||
ct: contentType,
|
||||
token: "",
|
||||
svcErr: svcerr.ErrAuthentication,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid request",
|
||||
req: "{",
|
||||
ct: contentType,
|
||||
token: token.AccessToken,
|
||||
token: accessToken,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid JSON",
|
||||
req: "{invalid}",
|
||||
ct: contentType,
|
||||
token: token.AccessToken,
|
||||
token: accessToken,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "issue key with invalid JSON content",
|
||||
req: `{"Type":{"key":"AccessToken"}}`,
|
||||
ct: contentType,
|
||||
token: token.AccessToken,
|
||||
token: accessToken,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
@@ -201,30 +182,16 @@ func TestIssue(t *testing.T) {
|
||||
token: tc.token,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
repoCall := krepo.On("Save", mock.Anything, mock.Anything).Return("", nil)
|
||||
policyCall := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(nil)
|
||||
svcCall := svc.On("Issue", mock.Anything, tc.token, mock.Anything).Return(tc.svcRes, tc.svcErr)
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
repoCall.Unset()
|
||||
policyCall.Unset()
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieve(t *testing.T) {
|
||||
svc := newService()
|
||||
policyCall := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(nil)
|
||||
token, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, Role: auth.UserRole, IssuedAt: time.Now(), Subject: id})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), Subject: id}
|
||||
|
||||
repoCall := krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, nil)
|
||||
k, err := svc.Issue(context.Background(), token.AccessToken, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
repoCall.Unset()
|
||||
policyCall.Unset()
|
||||
|
||||
ts := newServer(svc)
|
||||
ts, svc := newServer()
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
@@ -234,12 +201,13 @@ func TestRetrieve(t *testing.T) {
|
||||
token string
|
||||
key auth.Key
|
||||
status int
|
||||
err error
|
||||
svcRes auth.Key
|
||||
svcErr error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve an existing key",
|
||||
id: k.AccessToken,
|
||||
token: token.AccessToken,
|
||||
id: id,
|
||||
token: accessToken,
|
||||
key: auth.Key{
|
||||
Subject: id,
|
||||
Type: auth.AccessKey,
|
||||
@@ -247,28 +215,28 @@ func TestRetrieve(t *testing.T) {
|
||||
ExpiresAt: time.Now().Add(refreshDuration),
|
||||
},
|
||||
status: http.StatusOK,
|
||||
err: nil,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a non-existing key",
|
||||
id: "non-existing",
|
||||
token: token.AccessToken,
|
||||
status: http.StatusBadRequest,
|
||||
err: svcerr.ErrNotFound,
|
||||
token: accessToken,
|
||||
status: http.StatusNotFound,
|
||||
svcErr: svcerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a key with an invalid token",
|
||||
id: k.AccessToken,
|
||||
id: accessToken,
|
||||
token: "wrong",
|
||||
status: http.StatusUnauthorized,
|
||||
err: svcerr.ErrAuthentication,
|
||||
svcErr: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a key with an empty token",
|
||||
token: "",
|
||||
id: k.AccessToken,
|
||||
id: accessToken,
|
||||
status: http.StatusUnauthorized,
|
||||
err: svcerr.ErrAuthentication,
|
||||
svcErr: svcerr.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -279,30 +247,16 @@ func TestRetrieve(t *testing.T) {
|
||||
url: fmt.Sprintf("%s/keys/%s", ts.URL, tc.id),
|
||||
token: tc.token,
|
||||
}
|
||||
policyCall := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(nil)
|
||||
repoCall := krepo.On("Retrieve", mock.Anything, mock.Anything, mock.Anything).Return(tc.key, tc.err)
|
||||
svcCall := svc.On("RetrieveKey", mock.Anything, tc.token, tc.id).Return(tc.svcRes, tc.svcErr)
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
repoCall.Unset()
|
||||
policyCall.Unset()
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
svc := newService()
|
||||
policyCall := pEvaluator.On("CheckPolicy", mock.Anything, mock.Anything).Return(nil)
|
||||
token, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, Role: auth.UserRole, IssuedAt: time.Now(), Subject: id})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
key := auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), Subject: id}
|
||||
|
||||
repoCall := krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, nil)
|
||||
k, err := svc.Issue(context.Background(), token.AccessToken, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
repoCall.Unset()
|
||||
policyCall.Unset()
|
||||
|
||||
ts := newServer(svc)
|
||||
ts, svc := newServer()
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
@@ -311,29 +265,33 @@ func TestRevoke(t *testing.T) {
|
||||
id string
|
||||
token string
|
||||
status int
|
||||
svcErr error
|
||||
}{
|
||||
{
|
||||
desc: "revoke an existing key",
|
||||
id: k.AccessToken,
|
||||
token: token.AccessToken,
|
||||
id: id,
|
||||
token: accessToken,
|
||||
status: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
desc: "revoke a non-existing key",
|
||||
id: "non-existing",
|
||||
token: token.AccessToken,
|
||||
status: http.StatusNoContent,
|
||||
token: accessToken,
|
||||
svcErr: svcerr.ErrNotFound,
|
||||
status: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
desc: "revoke key with invalid token",
|
||||
id: k.AccessToken,
|
||||
id: id,
|
||||
token: "wrong",
|
||||
svcErr: svcerr.ErrAuthentication,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "revoke key with empty token",
|
||||
id: k.AccessToken,
|
||||
id: id,
|
||||
token: "",
|
||||
svcErr: svcerr.ErrAuthentication,
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
@@ -345,10 +303,57 @@ func TestRevoke(t *testing.T) {
|
||||
url: fmt.Sprintf("%s/keys/%s", ts.URL, tc.id),
|
||||
token: tc.token,
|
||||
}
|
||||
repoCall := krepo.On("Remove", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
svcCall := svc.On("Revoke", mock.Anything, tc.token, tc.id).Return(tc.svcErr)
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
repoCall.Unset()
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveJWKS(t *testing.T) {
|
||||
ts, svc := newServer()
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
svcRes []auth.PublicKeyInfo
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "retrieve JWKS with keys",
|
||||
svcRes: []auth.PublicKeyInfo{newPublicKeyInfo(), newPublicKeyInfo()},
|
||||
status: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "retrieve empty JWKS",
|
||||
svcRes: []auth.PublicKeyInfo{},
|
||||
status: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/keys/.well-known/jwks.json", ts.URL),
|
||||
}
|
||||
svcCall := svc.On("RetrieveJWKS").Return(tc.svcRes)
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func newPublicKeyInfo() auth.PublicKeyInfo {
|
||||
return auth.PublicKeyInfo{
|
||||
KeyID: "test-key-id",
|
||||
KeyType: "OKP",
|
||||
Algorithm: "EdDSA",
|
||||
Use: "sig",
|
||||
Curve: "Ed25519",
|
||||
X: "base64url-encoded-public-key",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,3 +46,5 @@ func (req keyReq) validate() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type jwksReq struct{}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
package keys
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -14,6 +16,8 @@ import (
|
||||
var (
|
||||
_ supermq.Response = (*issueKeyRes)(nil)
|
||||
_ supermq.Response = (*revokeKeyRes)(nil)
|
||||
_ supermq.Response = (*retrieveKeyRes)(nil)
|
||||
_ supermq.Response = (*retrieveJWKSRes)(nil)
|
||||
)
|
||||
|
||||
type issueKeyRes struct {
|
||||
@@ -69,3 +73,34 @@ func (res revokeKeyRes) Headers() map[string]string {
|
||||
func (res revokeKeyRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type retrieveJWKSRes struct {
|
||||
Keys []auth.PublicKeyInfo `json:"-"`
|
||||
CacheMaxAge int `json:"-"`
|
||||
CacheStaleWhileRevalidate int `json:"-"`
|
||||
}
|
||||
|
||||
func (res retrieveJWKSRes) MarshalJSON() ([]byte, error) {
|
||||
type jwksResponse struct {
|
||||
Keys []auth.PublicKeyInfo `json:"keys"`
|
||||
}
|
||||
|
||||
return json.Marshal(jwksResponse{Keys: res.Keys})
|
||||
}
|
||||
|
||||
func (res retrieveJWKSRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res retrieveJWKSRes) Headers() map[string]string {
|
||||
cacheControl := fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d", res.CacheMaxAge, res.CacheStaleWhileRevalidate)
|
||||
headers := map[string]string{
|
||||
"Cache-Control": cacheControl,
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
func (res retrieveJWKSRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
const contentType = "application/json"
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc auth.Service, mux *chi.Mux, logger *slog.Logger) *chi.Mux {
|
||||
func MakeHandler(svc auth.Service, mux *chi.Mux, logger *slog.Logger, jwksCacheMaxAge, jwksCacheStaleWhileRevalidate int) *chi.Mux {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
|
||||
}
|
||||
@@ -46,6 +46,13 @@ func MakeHandler(svc auth.Service, mux *chi.Mux, logger *slog.Logger) *chi.Mux {
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
).ServeHTTP)
|
||||
|
||||
r.Get("/.well-known/jwks.json", kithttp.NewServer(
|
||||
retrieveJWKSEndpoint(svc, jwksCacheMaxAge, jwksCacheStaleWhileRevalidate),
|
||||
decodeJWKSReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
).ServeHTTP)
|
||||
})
|
||||
return mux
|
||||
}
|
||||
@@ -57,7 +64,7 @@ func decodeIssue(_ context.Context, r *http.Request) (any, error) {
|
||||
|
||||
req := issueKeyReq{token: apiutil.ExtractBearerToken(r)}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -70,3 +77,8 @@ func decodeKeyReq(_ context.Context, r *http.Request) (any, error) {
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeJWKSReq(_ context.Context, _ *http.Request) (any, error) {
|
||||
req := jwksReq{}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,724 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pats
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var valid = "valid"
|
||||
|
||||
func TestCreatePatReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req createPatReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: createPatReq{
|
||||
token: valid,
|
||||
Name: "test-pat",
|
||||
Description: "test description",
|
||||
Duration: 24 * time.Hour,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: createPatReq{
|
||||
token: "",
|
||||
Name: "test-pat",
|
||||
Description: "test description",
|
||||
Duration: 24 * time.Hour,
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty name",
|
||||
req: createPatReq{
|
||||
token: valid,
|
||||
Name: "",
|
||||
Description: "test description",
|
||||
Duration: 24 * time.Hour,
|
||||
},
|
||||
err: apiutil.ErrMissingName,
|
||||
},
|
||||
{
|
||||
desc: "whitespace only name",
|
||||
req: createPatReq{
|
||||
token: valid,
|
||||
Name: " ",
|
||||
Description: "test description",
|
||||
Duration: 24 * time.Hour,
|
||||
},
|
||||
err: apiutil.ErrMissingName,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePatReqUnmarshalJSON(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
data string
|
||||
expected createPatReq
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "valid JSON with duration",
|
||||
data: `{"name":"test-pat","description":"test desc","duration":"24h"}`,
|
||||
expected: createPatReq{
|
||||
Name: "test-pat",
|
||||
Description: "test desc",
|
||||
Duration: 24 * time.Hour,
|
||||
},
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "invalid duration format",
|
||||
data: `{"name":"test-pat","description":"test desc","duration":"invalid"}`,
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid JSON",
|
||||
data: `{invalid json}`,
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
var req createPatReq
|
||||
err := json.Unmarshal([]byte(tc.data), &req)
|
||||
if tc.err {
|
||||
assert.Error(t, err, "UnmarshalJSON() should return error")
|
||||
} else {
|
||||
assert.NoError(t, err, "UnmarshalJSON() should not return error")
|
||||
assert.Equal(t, tc.expected.Name, req.Name)
|
||||
assert.Equal(t, tc.expected.Description, req.Description)
|
||||
assert.Equal(t, tc.expected.Duration, req.Duration)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrievePatReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req retrievePatReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: retrievePatReq{
|
||||
token: valid,
|
||||
id: "pat-id",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: retrievePatReq{
|
||||
token: "",
|
||||
id: "pat-id",
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
req: retrievePatReq{
|
||||
token: valid,
|
||||
id: "",
|
||||
},
|
||||
err: apiutil.ErrMissingPATID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePatNameReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req updatePatNameReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: updatePatNameReq{
|
||||
token: valid,
|
||||
id: "pat-id",
|
||||
Name: "new-name",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: updatePatNameReq{
|
||||
token: "",
|
||||
id: "pat-id",
|
||||
Name: "new-name",
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
req: updatePatNameReq{
|
||||
token: valid,
|
||||
id: "",
|
||||
Name: "new-name",
|
||||
},
|
||||
err: apiutil.ErrMissingPATID,
|
||||
},
|
||||
{
|
||||
desc: "empty name",
|
||||
req: updatePatNameReq{
|
||||
token: valid,
|
||||
id: "pat-id",
|
||||
Name: "",
|
||||
},
|
||||
err: apiutil.ErrMissingName,
|
||||
},
|
||||
{
|
||||
desc: "whitespace only name",
|
||||
req: updatePatNameReq{
|
||||
token: valid,
|
||||
id: "pat-id",
|
||||
Name: " ",
|
||||
},
|
||||
err: apiutil.ErrMissingName,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePatDescriptionReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req updatePatDescriptionReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: updatePatDescriptionReq{
|
||||
token: valid,
|
||||
id: "pat-id",
|
||||
Description: "new description",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: updatePatDescriptionReq{
|
||||
token: "",
|
||||
id: "pat-id",
|
||||
Description: "new description",
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
req: updatePatDescriptionReq{
|
||||
token: valid,
|
||||
id: "",
|
||||
Description: "new description",
|
||||
},
|
||||
err: apiutil.ErrMissingPATID,
|
||||
},
|
||||
{
|
||||
desc: "empty description",
|
||||
req: updatePatDescriptionReq{
|
||||
token: valid,
|
||||
id: "pat-id",
|
||||
Description: "",
|
||||
},
|
||||
err: apiutil.ErrMissingDescription,
|
||||
},
|
||||
{
|
||||
desc: "whitespace only description",
|
||||
req: updatePatDescriptionReq{
|
||||
token: valid,
|
||||
id: "pat-id",
|
||||
Description: " ",
|
||||
},
|
||||
err: apiutil.ErrMissingDescription,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPatsReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req listPatsReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: listPatsReq{
|
||||
token: valid,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: listPatsReq{
|
||||
token: "",
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletePatReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req deletePatReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: deletePatReq{
|
||||
token: valid,
|
||||
id: "pat-id",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: deletePatReq{
|
||||
token: "",
|
||||
id: "pat-id",
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
req: deletePatReq{
|
||||
token: valid,
|
||||
id: "",
|
||||
},
|
||||
err: apiutil.ErrMissingPATID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetPatSecretReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req resetPatSecretReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: resetPatSecretReq{
|
||||
token: valid,
|
||||
id: "pat-id",
|
||||
Duration: 24 * time.Hour,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: resetPatSecretReq{
|
||||
token: "",
|
||||
id: "pat-id",
|
||||
Duration: 24 * time.Hour,
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
req: resetPatSecretReq{
|
||||
token: valid,
|
||||
id: "",
|
||||
Duration: 24 * time.Hour,
|
||||
},
|
||||
err: apiutil.ErrMissingPATID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetPatSecretReqUnmarshalJSON(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
data string
|
||||
expected resetPatSecretReq
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "valid JSON with duration",
|
||||
data: `{"duration":"48h"}`,
|
||||
expected: resetPatSecretReq{
|
||||
Duration: 48 * time.Hour,
|
||||
},
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "invalid duration format",
|
||||
data: `{"duration":"invalid"}`,
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid JSON",
|
||||
data: `{invalid}`,
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
var req resetPatSecretReq
|
||||
err := json.Unmarshal([]byte(tc.data), &req)
|
||||
if tc.err {
|
||||
assert.Error(t, err, "UnmarshalJSON() should return error")
|
||||
} else {
|
||||
assert.NoError(t, err, "UnmarshalJSON() should not return error")
|
||||
assert.Equal(t, tc.expected.Duration, req.Duration)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokePatSecretReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req revokePatSecretReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: revokePatSecretReq{
|
||||
token: valid,
|
||||
id: "pat-id",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: revokePatSecretReq{
|
||||
token: "",
|
||||
id: "pat-id",
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
req: revokePatSecretReq{
|
||||
token: valid,
|
||||
id: "",
|
||||
},
|
||||
err: apiutil.ErrMissingPATID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearAllPATReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req clearAllPATReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: clearAllPATReq{
|
||||
token: valid,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: clearAllPATReq{
|
||||
token: "",
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddScopeReqValidate(t *testing.T) {
|
||||
validScope := auth.Scope{
|
||||
DomainID: "domain1",
|
||||
EntityType: auth.GroupsType,
|
||||
EntityID: "entity1",
|
||||
Operation: "create",
|
||||
}
|
||||
|
||||
invalidScope := auth.Scope{
|
||||
DomainID: "",
|
||||
EntityType: auth.GroupsType,
|
||||
EntityID: "",
|
||||
Operation: "view",
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req addScopeReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: addScopeReq{
|
||||
token: valid,
|
||||
id: "pat-id",
|
||||
Scopes: []auth.Scope{validScope},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: addScopeReq{
|
||||
token: "",
|
||||
id: "pat-id",
|
||||
Scopes: []auth.Scope{validScope},
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
req: addScopeReq{
|
||||
token: valid,
|
||||
id: "",
|
||||
Scopes: []auth.Scope{validScope},
|
||||
},
|
||||
err: apiutil.ErrMissingPATID,
|
||||
},
|
||||
{
|
||||
desc: "empty scopes",
|
||||
req: addScopeReq{
|
||||
token: valid,
|
||||
id: "pat-id",
|
||||
Scopes: []auth.Scope{},
|
||||
},
|
||||
err: apiutil.ErrValidation,
|
||||
},
|
||||
{
|
||||
desc: "invalid scope",
|
||||
req: addScopeReq{
|
||||
token: valid,
|
||||
id: "pat-id",
|
||||
Scopes: []auth.Scope{invalidScope},
|
||||
},
|
||||
err: apiutil.ErrValidation,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.req.validate()
|
||||
if tc.err != nil {
|
||||
assert.Error(t, err, "validate() should return error")
|
||||
} else {
|
||||
assert.NoError(t, err, "validate() should not return error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveScopeReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req removeScopeReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: removeScopeReq{
|
||||
token: valid,
|
||||
id: "pat-id",
|
||||
ScopesID: []string{"scope1", "scope2"},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: removeScopeReq{
|
||||
token: "",
|
||||
id: "pat-id",
|
||||
ScopesID: []string{"scope1"},
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
req: removeScopeReq{
|
||||
token: valid,
|
||||
id: "",
|
||||
ScopesID: []string{"scope1"},
|
||||
},
|
||||
err: apiutil.ErrMissingPATID,
|
||||
},
|
||||
{
|
||||
desc: "empty scopes list",
|
||||
req: removeScopeReq{
|
||||
token: valid,
|
||||
id: "pat-id",
|
||||
ScopesID: []string{},
|
||||
},
|
||||
err: apiutil.ErrValidation,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearAllScopeReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req clearAllScopeReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: clearAllScopeReq{
|
||||
token: valid,
|
||||
id: "pat-id",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: clearAllScopeReq{
|
||||
token: "",
|
||||
id: "pat-id",
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
req: clearAllScopeReq{
|
||||
token: valid,
|
||||
id: "",
|
||||
},
|
||||
err: apiutil.ErrMissingPATID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListScopesReqValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req listScopesReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: listScopesReq{
|
||||
token: valid,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
patID: "pat-id",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: listScopesReq{
|
||||
token: "",
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
patID: "pat-id",
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty patID",
|
||||
req: listScopesReq{
|
||||
token: valid,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
patID: "",
|
||||
},
|
||||
err: apiutil.ErrMissingPATID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.req.validate()
|
||||
assert.Equal(t, tc.err, err, "validate() error = %v, expected %v", err, tc.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@ func decodeCreatePATRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
}
|
||||
req := createPatReq{token: token}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
@@ -171,7 +171,7 @@ func decodeUpdatePATNameRequest(_ context.Context, r *http.Request) (any, error)
|
||||
id: chi.URLParam(r, "id"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
@@ -190,7 +190,7 @@ func decodeUpdatePATDescriptionRequest(_ context.Context, r *http.Request) (any,
|
||||
id: chi.URLParam(r, "id"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
@@ -262,7 +262,7 @@ func decodeResetPATSecretRequest(_ context.Context, r *http.Request) (any, error
|
||||
id: chi.URLParam(r, "id"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
@@ -305,7 +305,7 @@ func decodeAddScopeRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -348,7 +348,7 @@ func decodeRemoveScopeRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
id: chi.URLParam(r, "id"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ import (
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc auth.Service, logger *slog.Logger, instanceID string) http.Handler {
|
||||
func MakeHandler(svc auth.Service, logger *slog.Logger, instanceID string, jwksCacheMaxAge, jwksCacheStaleWhileRevalidate int) http.Handler {
|
||||
mux := chi.NewRouter()
|
||||
|
||||
mux = keys.MakeHandler(svc, mux, logger)
|
||||
mux = keys.MakeHandler(svc, mux, logger, jwksCacheMaxAge, jwksCacheStaleWhileRevalidate)
|
||||
mux = pats.MakeHandler(svc, mux, logger)
|
||||
|
||||
mux.Get("/health", supermq.Health("auth", instanceID))
|
||||
|
||||
Vendored
+2
@@ -1,4 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package cache contains the domain concept definitions needed to
|
||||
// support SuperMQ auth cache service functionality.
|
||||
package cache
|
||||
|
||||
Vendored
+6
-6
@@ -28,7 +28,7 @@ func NewPatsCache(client *redis.Client, duration time.Duration) auth.Cache {
|
||||
|
||||
func (pc *patCache) Save(ctx context.Context, userID string, scopes []auth.Scope) error {
|
||||
for _, sc := range scopes {
|
||||
key := generateKey(userID, sc.PatID, sc.OptionalDomainID, sc.EntityType, sc.Operation, sc.EntityID)
|
||||
key := generateKey(userID, sc.PatID, sc.DomainID, sc.EntityType, sc.Operation, sc.EntityID)
|
||||
if err := pc.client.Set(ctx, key, sc.ID, pc.duration).Err(); err != nil {
|
||||
return errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
@@ -37,9 +37,9 @@ func (pc *patCache) Save(ctx context.Context, userID string, scopes []auth.Scope
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pc *patCache) CheckScope(ctx context.Context, userID, patID, optionalDomainID string, entityType auth.EntityType, operation auth.Operation, entityID string) bool {
|
||||
exactKey := fmt.Sprintf("pat:%s:%s:%s:%s:%s:%s", userID, patID, entityType, optionalDomainID, operation, entityID)
|
||||
wildcardKey := fmt.Sprintf("pat:%s:%s:%s:%s:%s:*", userID, patID, entityType, operation, operation)
|
||||
func (pc *patCache) CheckScope(ctx context.Context, userID, patID, domainID string, entityType auth.EntityType, operation string, entityID string) bool {
|
||||
exactKey := fmt.Sprintf("pat:%s:%s:%s:%s:%s:%s", userID, patID, entityType, domainID, operation, entityID)
|
||||
wildcardKey := fmt.Sprintf("pat:%s:%s:%s:%s:%s:*", userID, patID, entityType, domainID, operation)
|
||||
|
||||
res, err := pc.client.Exists(ctx, exactKey, wildcardKey).Result()
|
||||
if err != nil {
|
||||
@@ -115,6 +115,6 @@ func (pc *patCache) RemoveAllScope(ctx context.Context, userID, patID string) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateKey(userID, patID, optionalDomainId string, entityType auth.EntityType, operation auth.Operation, entityID string) string {
|
||||
return fmt.Sprintf("pat:%s:%s:%s:%s:%s:%s", userID, patID, entityType, optionalDomainId, operation, entityID)
|
||||
func generateKey(userID, patID, domainId string, entityType auth.EntityType, operation string, entityID string) string {
|
||||
return fmt.Sprintf("pat:%s:%s:%s:%s:%s:%s", userID, patID, entityType, domainId, operation, entityID)
|
||||
}
|
||||
|
||||
Vendored
+132
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const (
|
||||
refreshPrefix = "refresh_tokens:"
|
||||
scoreNegInf = "-inf"
|
||||
scorePosInf = "+inf"
|
||||
)
|
||||
|
||||
var _ auth.UserActiveTokensCache = (*tokensCache)(nil)
|
||||
|
||||
type tokensCache struct {
|
||||
client *redis.Client
|
||||
keyDuration time.Duration
|
||||
}
|
||||
|
||||
// NewUserActiveTokensCache returns redis auth cache implementation.
|
||||
func NewUserActiveTokensCache(client *redis.Client, duration time.Duration) (auth.UserActiveTokensCache, error) {
|
||||
if duration == 0 {
|
||||
return nil, errors.New("token cache duration must not be zero")
|
||||
}
|
||||
return &tokensCache{
|
||||
client: client,
|
||||
keyDuration: duration,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SaveActive saves an active refresh token ID for a user with optional description.
|
||||
func (tc *tokensCache) SaveActive(ctx context.Context, userID, tokenID, description string, expiry time.Time) error {
|
||||
ttl := min(tc.keyDuration, time.Until(expiry))
|
||||
|
||||
pipe := tc.client.TxPipeline()
|
||||
|
||||
pipe.Set(ctx, tokenKey(tokenID), description, ttl)
|
||||
pipe.ZAdd(ctx, userTokensKey(userID), redis.Z{
|
||||
Score: float64(time.Now().Add(ttl).Unix()),
|
||||
Member: tokenID,
|
||||
})
|
||||
|
||||
_, err := pipe.Exec(ctx)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// IsActive checks if the token ID is active for the given user.
|
||||
func (tc *tokensCache) IsActive(ctx context.Context, tokenID string) (bool, error) {
|
||||
count, err := tc.client.Exists(ctx, tokenKey(tokenID)).Result()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// ListUserTokens lists all active refresh token IDs with descriptions for a user.
|
||||
func (tc *tokensCache) ListUserTokens(ctx context.Context, userID string) ([]auth.TokenInfo, error) {
|
||||
key := userTokensKey(userID)
|
||||
now := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
|
||||
pipe := tc.client.TxPipeline()
|
||||
pipe.ZRemRangeByScore(ctx, key, scoreNegInf, now)
|
||||
zrangeCmd := pipe.ZRangeByScore(ctx, key, &redis.ZRangeBy{Min: "(" + now, Max: scorePosInf})
|
||||
if _, err := pipe.Exec(ctx); err != nil && err != redis.Nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tokenIDs, err := zrangeCmd.Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(tokenIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
getPipe := tc.client.Pipeline()
|
||||
getCmds := make([]*redis.StringCmd, len(tokenIDs))
|
||||
for i, tokenID := range tokenIDs {
|
||||
getCmds[i] = getPipe.Get(ctx, tokenKey(tokenID))
|
||||
}
|
||||
|
||||
if _, err = getPipe.Exec(ctx); err != nil && err != redis.Nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
valid := make([]auth.TokenInfo, 0, len(tokenIDs))
|
||||
for i, cmd := range getCmds {
|
||||
description, err := cmd.Result()
|
||||
if err == redis.Nil {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
valid = append(valid, auth.TokenInfo{
|
||||
ID: tokenIDs[i],
|
||||
Description: description,
|
||||
})
|
||||
}
|
||||
|
||||
return valid, nil
|
||||
}
|
||||
|
||||
// RemoveActive removes an active refresh token ID for a user.
|
||||
func (tc *tokensCache) RemoveActive(ctx context.Context, userID, tokenID string) error {
|
||||
pipe := tc.client.TxPipeline()
|
||||
pipe.Del(ctx, tokenKey(tokenID))
|
||||
pipe.ZRem(ctx, userTokensKey(userID), tokenID)
|
||||
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func tokenKey(tokenID string) string {
|
||||
return refreshPrefix + "token:" + tokenID
|
||||
}
|
||||
|
||||
func userTokensKey(userID string) string {
|
||||
return refreshPrefix + "user_tokens:" + userID
|
||||
}
|
||||
Vendored
+288
@@ -0,0 +1,288 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/auth/cache"
|
||||
"github.com/absmach/supermq/internal/testsutil"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
storeClient *redis.Client
|
||||
storeURL string
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
code := testsutil.RunRedisTest(m, &storeClient, &storeURL)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func setupRedisTokensClient() auth.UserActiveTokensCache {
|
||||
tc, err := cache.NewUserActiveTokensCache(storeClient, 10*time.Minute)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return tc
|
||||
}
|
||||
|
||||
func TestTokenSave(t *testing.T) {
|
||||
storeClient.FlushAll(context.Background())
|
||||
tokensCache := setupRedisTokensClient()
|
||||
|
||||
userID := testsutil.GenerateUUID(t)
|
||||
tokenID := testsutil.GenerateUUID(t)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
userID string
|
||||
tokenID string
|
||||
description string
|
||||
expiry time.Time
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "Save active token",
|
||||
userID: userID,
|
||||
tokenID: tokenID,
|
||||
description: "Test token",
|
||||
expiry: time.Now().Add(10 * time.Minute),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Save already cached token",
|
||||
userID: userID,
|
||||
tokenID: tokenID,
|
||||
description: "Updated token",
|
||||
expiry: time.Now().Add(10 * time.Minute),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Save another token for same user",
|
||||
userID: userID,
|
||||
tokenID: testsutil.GenerateUUID(t),
|
||||
description: "Another token",
|
||||
expiry: time.Now().Add(10 * time.Minute),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Save token with empty id",
|
||||
userID: userID,
|
||||
tokenID: "",
|
||||
description: "Empty ID token",
|
||||
expiry: time.Now().Add(10 * time.Minute),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Save token with empty description",
|
||||
userID: userID,
|
||||
tokenID: testsutil.GenerateUUID(t),
|
||||
description: "",
|
||||
expiry: time.Now().Add(10 * time.Minute),
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tokensCache.SaveActive(context.Background(), tc.userID, tc.tokenID, tc.description, tc.expiry)
|
||||
if err == nil {
|
||||
ok, err := tokensCache.IsActive(context.Background(), tc.tokenID)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
assert.True(t, errors.Contains(err, tc.err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenContains(t *testing.T) {
|
||||
storeClient.FlushAll(context.Background())
|
||||
tokensCache := setupRedisTokensClient()
|
||||
|
||||
userID := testsutil.GenerateUUID(t)
|
||||
tokenID := testsutil.GenerateUUID(t)
|
||||
|
||||
err := tokensCache.SaveActive(context.Background(), userID, tokenID, "Test token", time.Now().Add(10*time.Minute))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error while trying to save: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
userID string
|
||||
tokenID string
|
||||
ok bool
|
||||
}{
|
||||
{
|
||||
desc: "IsActive for existing token",
|
||||
userID: userID,
|
||||
tokenID: tokenID,
|
||||
ok: true,
|
||||
},
|
||||
{
|
||||
desc: "IsActive for non existing token",
|
||||
userID: userID,
|
||||
tokenID: testsutil.GenerateUUID(t),
|
||||
},
|
||||
{
|
||||
desc: "IsActive with empty token id",
|
||||
userID: userID,
|
||||
tokenID: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
ok, err := tokensCache.IsActive(context.Background(), tc.tokenID)
|
||||
if tc.ok {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tc.ok, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenRemove(t *testing.T) {
|
||||
storeClient.FlushAll(context.Background())
|
||||
tokensCache := setupRedisTokensClient()
|
||||
|
||||
userID := testsutil.GenerateUUID(t)
|
||||
num := 10
|
||||
var tokenIDs []string
|
||||
for i := range num {
|
||||
tokenID := testsutil.GenerateUUID(t)
|
||||
err := tokensCache.SaveActive(context.Background(), userID, tokenID, fmt.Sprintf("Token %d", i), time.Now().Add(10*time.Minute))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error while trying to save: %s", err))
|
||||
tokenIDs = append(tokenIDs, tokenID)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
userID string
|
||||
tokenID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "Remove an existing token from cache",
|
||||
userID: userID,
|
||||
tokenID: tokenIDs[0],
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Remove token with empty id from cache",
|
||||
userID: userID,
|
||||
tokenID: "",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Remove non existing id from cache",
|
||||
userID: userID,
|
||||
tokenID: testsutil.GenerateUUID(t),
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tokensCache.RemoveActive(context.Background(), tc.userID, tc.tokenID)
|
||||
assert.True(t, errors.Contains(err, tc.err))
|
||||
if err == nil {
|
||||
ok, err := tokensCache.IsActive(context.Background(), tc.tokenID)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUserTokens(t *testing.T) {
|
||||
storeClient.FlushAll(context.Background())
|
||||
tokensCache := setupRedisTokensClient()
|
||||
|
||||
userID := testsutil.GenerateUUID(t)
|
||||
userID2 := testsutil.GenerateUUID(t)
|
||||
num := 5
|
||||
var expectedTokens []auth.TokenInfo
|
||||
|
||||
for i := range num {
|
||||
tokenID := testsutil.GenerateUUID(t)
|
||||
description := fmt.Sprintf("Token %d", i)
|
||||
err := tokensCache.SaveActive(context.Background(), userID, tokenID, description, time.Now().Add(10*time.Minute))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error while trying to save: %s", err))
|
||||
expectedTokens = append(expectedTokens, auth.TokenInfo{
|
||||
ID: tokenID,
|
||||
Description: description,
|
||||
})
|
||||
}
|
||||
|
||||
tokenID2 := testsutil.GenerateUUID(t)
|
||||
desc2 := "User 2 token"
|
||||
err := tokensCache.SaveActive(context.Background(), userID2, tokenID2, desc2, time.Now().Add(10*time.Minute))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error while trying to save: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
userID string
|
||||
expectedCount int
|
||||
expectedTokens []auth.TokenInfo
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "List all tokens for user with multiple tokens",
|
||||
userID: userID,
|
||||
expectedCount: num,
|
||||
expectedTokens: expectedTokens,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "List tokens for user with single token",
|
||||
userID: userID2,
|
||||
expectedCount: 1,
|
||||
expectedTokens: []auth.TokenInfo{{ID: tokenID2, Description: desc2}},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "List tokens for user with no tokens",
|
||||
userID: testsutil.GenerateUUID(t),
|
||||
expectedCount: 0,
|
||||
expectedTokens: nil,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
tokens, err := tokensCache.ListUserTokens(context.Background(), tc.userID)
|
||||
assert.True(t, errors.Contains(err, tc.err))
|
||||
assert.Equal(t, tc.expectedCount, len(tokens))
|
||||
if tc.expectedTokens != nil {
|
||||
assert.ElementsMatch(t, tc.expectedTokens, tokens)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Cleanup expired tokens from list", func(t *testing.T) {
|
||||
// Remove one token directly from Redis to simulate expiration
|
||||
err := tokensCache.RemoveActive(context.Background(), userID, expectedTokens[0].ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// List should now return only valid tokens
|
||||
tokens, err := tokensCache.ListUserTokens(context.Background(), userID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, num-1, len(tokens))
|
||||
|
||||
// Check that the removed token is not in the list
|
||||
for _, token := range tokens {
|
||||
assert.NotEqual(t, expectedTokens[0].ID, token.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4,11 +4,10 @@
|
||||
package hasher
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
@@ -76,7 +75,6 @@ func (bh *bcryptHasher) Compare(plain, hashed string) error {
|
||||
}
|
||||
|
||||
func generateSalt(length int) ([]byte, error) {
|
||||
rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
|
||||
salt := make([]byte, length)
|
||||
_, err := rand.Read(salt)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package jwt_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
authjwt "github.com/absmach/supermq/auth/jwt"
|
||||
"github.com/absmach/supermq/internal/testsutil"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenType = "type"
|
||||
roleField = "role"
|
||||
issuerName = "supermq.auth"
|
||||
secret = "test"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidIssuer = errors.New("invalid token issuer value")
|
||||
reposecret = []byte("test")
|
||||
)
|
||||
|
||||
func newToken(issuerName string, key auth.Key) string {
|
||||
builder := jwt.NewBuilder()
|
||||
builder.
|
||||
Issuer(issuerName).
|
||||
IssuedAt(key.IssuedAt).
|
||||
Claim(tokenType, "r").
|
||||
Expiration(key.ExpiresAt)
|
||||
builder.Claim(roleField, key.Role)
|
||||
if key.Subject != "" {
|
||||
builder.Subject(key.Subject)
|
||||
}
|
||||
if key.ID != "" {
|
||||
builder.JwtID(key.ID)
|
||||
}
|
||||
tkn, _ := builder.Build()
|
||||
tokn, _ := jwt.Sign(tkn, jwt.WithKey(jwa.HS512, reposecret))
|
||||
return string(tokn)
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
tokenizer := authjwt.New([]byte(secret))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key auth.Key
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "issue new token",
|
||||
key: key(),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue token with OAuth token",
|
||||
key: auth.Key{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Type: auth.AccessKey,
|
||||
Subject: testsutil.GenerateUUID(t),
|
||||
IssuedAt: time.Now().Add(-10 * time.Second).Round(time.Second),
|
||||
ExpiresAt: time.Now().Add(10 * time.Minute).Round(time.Second),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue token without a domain",
|
||||
key: auth.Key{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Type: auth.AccessKey,
|
||||
Subject: testsutil.GenerateUUID(t),
|
||||
IssuedAt: time.Now().Add(-10 * time.Second).Round(time.Second),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue token without a subject",
|
||||
key: auth.Key{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Type: auth.AccessKey,
|
||||
Subject: "",
|
||||
IssuedAt: time.Now().Add(-10 * time.Second).Round(time.Second),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue token without type",
|
||||
key: auth.Key{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Type: auth.KeyType(auth.InvitationKey + 1),
|
||||
Subject: testsutil.GenerateUUID(t),
|
||||
IssuedAt: time.Now().Add(-10 * time.Second).Round(time.Second),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue token without a domain and subject",
|
||||
key: auth.Key{
|
||||
ID: testsutil.GenerateUUID(t),
|
||||
Type: auth.AccessKey,
|
||||
Subject: "",
|
||||
IssuedAt: time.Now().Add(-10 * time.Second).Round(time.Second),
|
||||
ExpiresAt: time.Now().Add(10 * time.Minute).Round(time.Second),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
tkn, err := tokenizer.Issue(tc.key)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s, got %s", tc.desc, tc.err, err))
|
||||
if err != nil {
|
||||
assert.NotEmpty(t, tkn, fmt.Sprintf("%s expected token, got empty string", tc.desc))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
tokenizer := authjwt.New([]byte(secret))
|
||||
|
||||
token, err := tokenizer.Issue(key())
|
||||
require.Nil(t, err, fmt.Sprintf("issuing key expected to succeed: %s", err))
|
||||
|
||||
apiKey := key()
|
||||
apiKey.Type = auth.APIKey
|
||||
apiKey.ExpiresAt = time.Now().UTC().Add(-1 * time.Minute).Round(time.Second)
|
||||
apiToken, err := tokenizer.Issue(apiKey)
|
||||
require.Nil(t, err, fmt.Sprintf("issuing user key expected to succeed: %s", err))
|
||||
|
||||
expKey := key()
|
||||
expKey.ExpiresAt = time.Now().UTC().Add(-1 * time.Minute).Round(time.Second)
|
||||
expToken, err := tokenizer.Issue(expKey)
|
||||
require.Nil(t, err, fmt.Sprintf("issuing expired key expected to succeed: %s", err))
|
||||
|
||||
emptySubjectKey := key()
|
||||
emptySubjectKey.Subject = ""
|
||||
emptySubjectToken, err := tokenizer.Issue(emptySubjectKey)
|
||||
require.Nil(t, err, fmt.Sprintf("issuing user key expected to succeed: %s", err))
|
||||
|
||||
emptyTypeKey := key()
|
||||
emptyTypeKey.Type = auth.KeyType(auth.InvitationKey + 1)
|
||||
emptyTypeToken, err := tokenizer.Issue(emptyTypeKey)
|
||||
require.Nil(t, err, fmt.Sprintf("issuing user key expected to succeed: %s", err))
|
||||
|
||||
emptyKey := key()
|
||||
emptyKey.Subject = ""
|
||||
require.Nil(t, err, fmt.Sprintf("issuing user key expected to succeed: %s", err))
|
||||
|
||||
inValidToken := newToken("invalid", key())
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key auth.Key
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "parse valid key",
|
||||
key: key(),
|
||||
token: token,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "parse invalid key",
|
||||
key: auth.Key{},
|
||||
token: "invalid",
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "parse expired key",
|
||||
key: auth.Key{},
|
||||
token: expToken,
|
||||
err: auth.ErrExpiry,
|
||||
},
|
||||
{
|
||||
desc: "parse expired API key",
|
||||
key: apiKey,
|
||||
token: apiToken,
|
||||
err: auth.ErrExpiry,
|
||||
},
|
||||
{
|
||||
desc: "parse token with invalid issuer",
|
||||
key: auth.Key{},
|
||||
token: inValidToken,
|
||||
err: errInvalidIssuer,
|
||||
},
|
||||
{
|
||||
desc: "parse token with invalid content",
|
||||
key: auth.Key{},
|
||||
token: newToken(issuerName, key()),
|
||||
err: authjwt.ErrJSONHandle,
|
||||
},
|
||||
{
|
||||
desc: "parse token with empty subject",
|
||||
key: emptySubjectKey,
|
||||
token: emptySubjectToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "parse token with empty type",
|
||||
key: emptyTypeKey,
|
||||
token: emptyTypeToken,
|
||||
err: errors.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
key, err := tokenizer.Parse(tc.token)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s, got %s", tc.desc, tc.err, err))
|
||||
if err == nil {
|
||||
assert.Equal(t, tc.key, key, fmt.Sprintf("%s expected %v, got %v", tc.desc, tc.key, key))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func key() auth.Key {
|
||||
exp := time.Now().UTC().Add(10 * time.Minute).Round(time.Second)
|
||||
return auth.Key{
|
||||
ID: "66af4a67-3823-438a-abd7-efdb613eaef6",
|
||||
Type: auth.AccessKey,
|
||||
Issuer: "supermq.auth",
|
||||
Role: auth.UserRole,
|
||||
Subject: "66af4a67-3823-438a-abd7-efdb613eaef6",
|
||||
IssuedAt: time.Now().UTC().Add(-10 * time.Second).Round(time.Second),
|
||||
ExpiresAt: exp,
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
)
|
||||
|
||||
var (
|
||||
// errInvalidIssuer is returned when the issuer is not supermq.auth.
|
||||
errInvalidIssuer = errors.New("invalid token issuer value")
|
||||
// errInvalidType is returned when there is no type field.
|
||||
errInvalidType = errors.New("invalid token type")
|
||||
// errInvalidRole is returned when the role is invalid.
|
||||
errInvalidRole = errors.New("invalid role")
|
||||
// errInvalidVerified is returned when the verified is invalid.
|
||||
errInvalidVerified = errors.New("invalid verified")
|
||||
// errJWTExpiryKey is used to check if the token is expired.
|
||||
errJWTExpiryKey = errors.New(`"exp" not satisfied`)
|
||||
// ErrSignJWT indicates an error in signing jwt token.
|
||||
ErrSignJWT = errors.New("failed to sign jwt token")
|
||||
// ErrValidateJWTToken indicates a failure to validate JWT token.
|
||||
ErrValidateJWTToken = errors.New("failed to validate jwt token")
|
||||
// ErrJSONHandle indicates an error in handling JSON.
|
||||
ErrJSONHandle = errors.New("failed to perform operation JSON")
|
||||
)
|
||||
|
||||
const (
|
||||
issuerName = "supermq.auth"
|
||||
tokenType = "type"
|
||||
userField = "user"
|
||||
RoleField = "role"
|
||||
VerifiedField = "verified"
|
||||
oauthProviderField = "oauth_provider"
|
||||
oauthAccessTokenField = "access_token"
|
||||
oauthRefreshTokenField = "refresh_token"
|
||||
)
|
||||
|
||||
type tokenizer struct {
|
||||
secret []byte
|
||||
}
|
||||
|
||||
var _ auth.Tokenizer = (*tokenizer)(nil)
|
||||
|
||||
// NewRepository instantiates an implementation of Token repository.
|
||||
func New(secret []byte) auth.Tokenizer {
|
||||
return &tokenizer{
|
||||
secret: secret,
|
||||
}
|
||||
}
|
||||
|
||||
func (tok *tokenizer) Issue(key auth.Key) (string, error) {
|
||||
builder := jwt.NewBuilder()
|
||||
builder.
|
||||
Issuer(issuerName).
|
||||
IssuedAt(key.IssuedAt).
|
||||
Claim(tokenType, key.Type).
|
||||
Expiration(key.ExpiresAt)
|
||||
builder.Claim(RoleField, key.Role)
|
||||
builder.Claim(VerifiedField, key.Verified)
|
||||
if key.Subject != "" {
|
||||
builder.Subject(key.Subject)
|
||||
}
|
||||
if key.ID != "" {
|
||||
builder.JwtID(key.ID)
|
||||
}
|
||||
tkn, err := builder.Build()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(svcerr.ErrAuthentication, err)
|
||||
}
|
||||
signedTkn, err := jwt.Sign(tkn, jwt.WithKey(jwa.HS512, tok.secret))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(ErrSignJWT, err)
|
||||
}
|
||||
return string(signedTkn), nil
|
||||
}
|
||||
|
||||
func (tok *tokenizer) Parse(token string) (auth.Key, error) {
|
||||
tkn, err := tok.validateToken(token)
|
||||
if err != nil {
|
||||
return auth.Key{}, errors.Wrap(svcerr.ErrAuthentication, err)
|
||||
}
|
||||
|
||||
key, err := toKey(tkn)
|
||||
if err != nil {
|
||||
return auth.Key{}, errors.Wrap(svcerr.ErrAuthentication, err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (tok *tokenizer) validateToken(token string) (jwt.Token, error) {
|
||||
tkn, err := jwt.Parse(
|
||||
[]byte(token),
|
||||
jwt.WithValidate(true),
|
||||
jwt.WithKey(jwa.HS512, tok.secret),
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Contains(err, errJWTExpiryKey) {
|
||||
return nil, auth.ErrExpiry
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
validator := jwt.ValidatorFunc(func(_ context.Context, t jwt.Token) jwt.ValidationError {
|
||||
if t.Issuer() != issuerName {
|
||||
return jwt.NewValidationError(errInvalidIssuer)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err := jwt.Validate(tkn, jwt.WithValidator(validator)); err != nil {
|
||||
return nil, errors.Wrap(ErrValidateJWTToken, err)
|
||||
}
|
||||
|
||||
return tkn, nil
|
||||
}
|
||||
|
||||
func toKey(tkn jwt.Token) (auth.Key, error) {
|
||||
data, err := json.Marshal(tkn.PrivateClaims())
|
||||
if err != nil {
|
||||
return auth.Key{}, errors.Wrap(ErrJSONHandle, err)
|
||||
}
|
||||
var key auth.Key
|
||||
if err := json.Unmarshal(data, &key); err != nil {
|
||||
return auth.Key{}, errors.Wrap(ErrJSONHandle, err)
|
||||
}
|
||||
|
||||
tType, ok := tkn.Get(tokenType)
|
||||
if !ok {
|
||||
return auth.Key{}, errInvalidType
|
||||
}
|
||||
kType, ok := tType.(float64)
|
||||
if !ok {
|
||||
return auth.Key{}, errInvalidType
|
||||
}
|
||||
kt := auth.KeyType(kType)
|
||||
if !kt.Validate() {
|
||||
return auth.Key{}, errInvalidType
|
||||
}
|
||||
|
||||
tRole, ok := tkn.Get(RoleField)
|
||||
if !ok {
|
||||
return auth.Key{}, errInvalidRole
|
||||
}
|
||||
kRole, ok := tRole.(float64)
|
||||
if !ok {
|
||||
return auth.Key{}, errInvalidRole
|
||||
}
|
||||
|
||||
tVerified, ok := tkn.Get(VerifiedField)
|
||||
if !ok {
|
||||
return auth.Key{}, errInvalidVerified
|
||||
}
|
||||
kVerified, ok := tVerified.(bool)
|
||||
if !ok {
|
||||
return auth.Key{}, errInvalidVerified
|
||||
}
|
||||
|
||||
kr := auth.Role(kRole)
|
||||
if !kr.Validate() {
|
||||
return auth.Key{}, errInvalidRole
|
||||
}
|
||||
|
||||
key.ID = tkn.JwtID()
|
||||
key.Type = auth.KeyType(kType)
|
||||
key.Role = auth.Role(kRole)
|
||||
key.Issuer = tkn.Issuer()
|
||||
key.Subject = tkn.Subject()
|
||||
key.IssuedAt = tkn.IssuedAt()
|
||||
key.ExpiresAt = tkn.Expiration()
|
||||
key.Verified = kVerified
|
||||
|
||||
return key, nil
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnsupportedKeyAlgorithm = errors.New("unsupported key algorithm")
|
||||
ErrInvalidSymmetricKey = errors.New("invalid symmetric key")
|
||||
ErrPublicKeysNotSupported = errors.New("public keys not supported for symmetric algorithm")
|
||||
ErrRevokedToken = errors.NewAuthNError("token is revoked")
|
||||
)
|
||||
|
||||
// PublicKeyInfo represents a public key for external distribution via JWKS.
|
||||
// This follows RFC 7517 (JSON Web Key) specification.
|
||||
type PublicKeyInfo struct {
|
||||
KeyID string `json:"kid"`
|
||||
KeyType string `json:"kty"`
|
||||
Algorithm string `json:"alg"`
|
||||
Use string `json:"use,omitempty"`
|
||||
|
||||
// EdDSA (Ed25519) fields
|
||||
Curve string `json:"crv,omitempty"`
|
||||
X string `json:"x,omitempty"`
|
||||
|
||||
// Future: RSA fields (n, e), ECDSA fields (x, y, crv), etc.
|
||||
}
|
||||
|
||||
// Tokenizer handles token creation and verification for authentication.
|
||||
// Implementations manage underlying cryptographic operations and key distribution.
|
||||
type Tokenizer interface {
|
||||
// Issue creates a signed token string from the given key claims.
|
||||
// For RefreshKey types, the token ID is stored as active in the cache.
|
||||
Issue(key Key) (token string, err error)
|
||||
|
||||
// Parse verifies and parses a token string (JWT or PAT), returning the extracted claims.
|
||||
// For PAT tokens (prefix "pat"), returns a Key with Type set to PersonalAccessToken.
|
||||
// For JWT tokens, performs cryptographic verification and returns the parsed claims.
|
||||
Parse(ctx context.Context, token string) (key Key, err error)
|
||||
|
||||
// RetrieveJWKS returns public keys for distribution via JWKS endpoint.
|
||||
// Returns ErrPublicKeysNotSupported for symmetric tokenizers (HMAC).
|
||||
RetrieveJWKS() ([]PublicKeyInfo, error)
|
||||
}
|
||||
|
||||
// UserActiveTokensCache represents a cache repository for managing active refresh tokens per user.
|
||||
type UserActiveTokensCache interface {
|
||||
// SaveActive saves an active refresh token ID for a user with optional description.
|
||||
SaveActive(ctx context.Context, userID, tokenID, description string, expiry time.Time) error
|
||||
|
||||
// IsActive checks if the token ID is active.
|
||||
IsActive(ctx context.Context, tokenID string) (bool, error)
|
||||
|
||||
// ListUserTokens lists all active token IDs with descriptions for a given user.
|
||||
ListUserTokens(ctx context.Context, userID string) ([]TokenInfo, error)
|
||||
|
||||
// RemoveActive removes an active refresh token ID.
|
||||
RemoveActive(ctx context.Context, userID, tokenID string) error
|
||||
}
|
||||
|
||||
// TokenInfo represents information about an active refresh token.
|
||||
type TokenInfo struct {
|
||||
ID string `json:"id"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// IsSymmetricAlgorithm determines if the given algorithm is symmetric (HMAC-based).
|
||||
// Returns true for HMAC algorithms (HS256, HS384, HS512).
|
||||
// Returns false for asymmetric algorithms (EdDSA).
|
||||
// Returns error for unsupported algorithms.
|
||||
func IsSymmetricAlgorithm(alg string) (bool, error) {
|
||||
switch alg {
|
||||
case "EdDSA":
|
||||
return false, nil
|
||||
case "HS256", "HS384", "HS512":
|
||||
return true, nil
|
||||
default:
|
||||
return false, ErrUnsupportedKeyAlgorithm
|
||||
}
|
||||
}
|
||||
+9
-8
@@ -81,14 +81,15 @@ func (r Role) Validate() bool {
|
||||
|
||||
// Key represents API key.
|
||||
type Key struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Type KeyType `json:"type,omitempty"`
|
||||
Issuer string `json:"issuer,omitempty"`
|
||||
Subject string `json:"subject,omitempty"` // user ID
|
||||
Role Role `json:"role,omitempty"`
|
||||
IssuedAt time.Time `json:"issued_at,omitempty"`
|
||||
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
||||
Verified bool `json:"verified,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Type KeyType `json:"type,omitempty"`
|
||||
Issuer string `json:"issuer,omitempty"`
|
||||
Subject string `json:"subject,omitempty"` // user ID
|
||||
Role Role `json:"role,omitempty"`
|
||||
IssuedAt time.Time `json:"issued_at,omitempty"`
|
||||
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
||||
Verified bool `json:"verified,omitempty"`
|
||||
Description string `json:"description,omitempty"` // Optional description for refresh tokens
|
||||
}
|
||||
|
||||
func (key Key) String() string {
|
||||
|
||||
+193
-2
@@ -12,6 +12,195 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestKeyTypeString(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
keyType auth.KeyType
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "Access key type",
|
||||
keyType: auth.AccessKey,
|
||||
expected: "access",
|
||||
},
|
||||
{
|
||||
desc: "Refresh key type",
|
||||
keyType: auth.RefreshKey,
|
||||
expected: "refresh",
|
||||
},
|
||||
{
|
||||
desc: "Recovery key type",
|
||||
keyType: auth.RecoveryKey,
|
||||
expected: "recovery",
|
||||
},
|
||||
{
|
||||
desc: "API key type",
|
||||
keyType: auth.APIKey,
|
||||
expected: "API",
|
||||
},
|
||||
{
|
||||
desc: "Personal access token type",
|
||||
keyType: auth.PersonalAccessToken,
|
||||
expected: "pat",
|
||||
},
|
||||
{
|
||||
desc: "Invitation key type",
|
||||
keyType: auth.InvitationKey,
|
||||
expected: "unknown",
|
||||
},
|
||||
{
|
||||
desc: "Unknown key type",
|
||||
keyType: auth.KeyType(100),
|
||||
expected: "unknown",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := tc.keyType.String()
|
||||
assert.Equal(t, tc.expected, got, "String() = %v, expected %v", got, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyTypeValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
keyType auth.KeyType
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
desc: "Valid access key",
|
||||
keyType: auth.AccessKey,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid refresh key",
|
||||
keyType: auth.RefreshKey,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid recovery key",
|
||||
keyType: auth.RecoveryKey,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid API key",
|
||||
keyType: auth.APIKey,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid personal access token",
|
||||
keyType: auth.PersonalAccessToken,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid invitation key",
|
||||
keyType: auth.InvitationKey,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid key type (too large)",
|
||||
keyType: auth.KeyType(100),
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := tc.keyType.Validate()
|
||||
assert.Equal(t, tc.expected, got, "Validate() = %v, expected %v", got, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleString(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
role auth.Role
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "User role",
|
||||
role: auth.UserRole,
|
||||
expected: "user",
|
||||
},
|
||||
{
|
||||
desc: "Admin role",
|
||||
role: auth.AdminRole,
|
||||
expected: "admin",
|
||||
},
|
||||
{
|
||||
desc: "Unknown role",
|
||||
role: auth.Role(100),
|
||||
expected: "unknown",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := tc.role.String()
|
||||
assert.Equal(t, tc.expected, got, "String() = %v, expected %v", got, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
role auth.Role
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
desc: "Valid user role",
|
||||
role: auth.UserRole,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid admin role",
|
||||
role: auth.AdminRole,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid role (zero)",
|
||||
role: auth.Role(0),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Invalid role (too large)",
|
||||
role: auth.Role(100),
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := tc.role.Validate()
|
||||
assert.Equal(t, tc.expected, got, "Validate() = %v, expected %v", got, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyString(t *testing.T) {
|
||||
key := auth.Key{
|
||||
ID: "test-id",
|
||||
Type: auth.APIKey,
|
||||
Issuer: "test-issuer",
|
||||
Subject: "test-subject",
|
||||
Role: auth.UserRole,
|
||||
IssuedAt: time.Now().UTC().Round(time.Second),
|
||||
ExpiresAt: time.Now().UTC().Add(24 * time.Hour).Round(time.Second),
|
||||
}
|
||||
|
||||
str := key.String()
|
||||
assert.NotEmpty(t, str, "String() should return non-empty string")
|
||||
assert.Contains(t, str, "test-id", "String() should contain ID")
|
||||
assert.Contains(t, str, "test-issuer", "String() should contain Issuer")
|
||||
assert.Contains(t, str, "test-subject", "String() should contain Subject")
|
||||
assert.Contains(t, str, "API", "String() should contain Type")
|
||||
assert.Contains(t, str, "user", "String() should contain Role")
|
||||
}
|
||||
|
||||
func TestExpired(t *testing.T) {
|
||||
exp := time.Now().Add(5 * time.Minute)
|
||||
exp1 := time.Now()
|
||||
@@ -54,7 +243,9 @@ func TestExpired(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
res := tc.key.Expired()
|
||||
assert.Equal(t, tc.expired, res, fmt.Sprintf("%s: expected %t got %t\n", tc.desc, tc.expired, res))
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
res := tc.key.Expired()
|
||||
assert.Equal(t, tc.expired, res, fmt.Sprintf("%s: expected %t got %t\n", tc.desc, tc.expired, res))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
// for SuperMQ Auth service.
|
||||
//
|
||||
// For more details about tracing instrumentation for SuperMQ refer to the
|
||||
// documentation at https://docs.supermq.abstractmachines.fr/tracing/.
|
||||
// documentation at https://docs.supermq.absmach.eu/tracing/.
|
||||
package middleware
|
||||
|
||||
@@ -100,7 +100,53 @@ func (lm *loggingMiddleware) Identify(ctx context.Context, token string) (id aut
|
||||
return lm.svc.Identify(ctx, token)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Authorize(ctx context.Context, pr policies.Policy) (err error) {
|
||||
func (lm *loggingMiddleware) RetrieveJWKS() (jwks []auth.PublicKeyInfo) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
}
|
||||
lm.logger.Info("Retrieve JWKS completed successfully", args...)
|
||||
}(time.Now())
|
||||
return lm.svc.RetrieveJWKS()
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RevokeToken(ctx context.Context, userID, tokenID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("user_id", userID),
|
||||
slog.String("token_id", tokenID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.String("error", err.Error()))
|
||||
lm.logger.Warn("Revoke token failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Revoke token completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RevokeToken(ctx, userID, tokenID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListUserRefreshTokens(ctx context.Context, userID string) (tokens []auth.TokenInfo, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("user_id", userID),
|
||||
slog.Int("tokens_count", len(tokens)),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.String("error", err.Error()))
|
||||
lm.logger.Warn("List user refresh tokens failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("List user refresh tokens completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListUserRefreshTokens(ctx, userID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Authorize(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
@@ -115,6 +161,18 @@ func (lm *loggingMiddleware) Authorize(ctx context.Context, pr policies.Policy)
|
||||
),
|
||||
slog.String("permission", pr.Permission),
|
||||
}
|
||||
if patAuthz != nil {
|
||||
args = append(args,
|
||||
slog.Group("pat",
|
||||
slog.String("pat_id", patAuthz.PatID),
|
||||
slog.String("user_id", patAuthz.UserID),
|
||||
slog.String("entity_type", patAuthz.EntityType.String()),
|
||||
slog.String("entity_id", patAuthz.EntityID),
|
||||
slog.String("operation", patAuthz.Operation),
|
||||
slog.String("domain", patAuthz.Domain),
|
||||
),
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.String("error", err.Error()))
|
||||
lm.logger.Warn("Authorize failed", args...)
|
||||
@@ -122,7 +180,7 @@ func (lm *loggingMiddleware) Authorize(ctx context.Context, pr policies.Policy)
|
||||
}
|
||||
lm.logger.Info("Authorize completed successfully", args...)
|
||||
}(time.Now())
|
||||
return lm.svc.Authorize(ctx, pr)
|
||||
return lm.svc.Authorize(ctx, pr, patAuthz)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CreatePAT(ctx context.Context, token, name, description string, duration time.Duration) (pa auth.PAT, err error) {
|
||||
@@ -297,8 +355,8 @@ func (lm *loggingMiddleware) AddScope(ctx context.Context, token, patID string,
|
||||
var groupArgs []any
|
||||
for _, s := range scopes {
|
||||
groupArgs = append(groupArgs, slog.String("entity_type", s.EntityType.String()))
|
||||
groupArgs = append(groupArgs, slog.String("optional_domain_id", s.OptionalDomainID))
|
||||
groupArgs = append(groupArgs, slog.String("operation", s.Operation.String()))
|
||||
groupArgs = append(groupArgs, slog.String("domain_id", s.DomainID))
|
||||
groupArgs = append(groupArgs, slog.String("operation", s.Operation))
|
||||
groupArgs = append(groupArgs, slog.String("entity_id", s.EntityID))
|
||||
}
|
||||
|
||||
@@ -369,13 +427,13 @@ func (lm *loggingMiddleware) IdentifyPAT(ctx context.Context, paToken string) (p
|
||||
return lm.svc.IdentifyPAT(ctx, paToken)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) AuthorizePAT(ctx context.Context, userID, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) (err error) {
|
||||
func (lm *loggingMiddleware) AuthorizePAT(ctx context.Context, userID, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("entity_type", entityType.String()),
|
||||
slog.String("optional_domain_id", optionalDomainID),
|
||||
slog.String("operation", operation.String()),
|
||||
slog.String("domain_id", domainID),
|
||||
slog.String("operation", operation),
|
||||
slog.String("entities", entityID),
|
||||
}
|
||||
if err != nil {
|
||||
@@ -385,5 +443,5 @@ func (lm *loggingMiddleware) AuthorizePAT(ctx context.Context, userID, patID str
|
||||
}
|
||||
lm.logger.Info("Authorize PAT completed successfully", args...)
|
||||
}(time.Now())
|
||||
return lm.svc.AuthorizePAT(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
|
||||
return lm.svc.AuthorizePAT(ctx, userID, patID, entityType, domainID, operation, entityID)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,15 @@ func (ms *metricsMiddleware) Issue(ctx context.Context, token string, key auth.K
|
||||
return ms.svc.Issue(ctx, token, key)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RevokeToken(ctx context.Context, userID, tokenID string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "revoke_token").Add(1)
|
||||
ms.latency.With("method", "revoke_token").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.RevokeToken(ctx, userID, tokenID)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Revoke(ctx context.Context, token, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "revoke_key").Add(1)
|
||||
@@ -67,12 +76,29 @@ func (ms *metricsMiddleware) Identify(ctx context.Context, token string) (auth.K
|
||||
return ms.svc.Identify(ctx, token)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Authorize(ctx context.Context, pr policies.Policy) error {
|
||||
func (ms *metricsMiddleware) RetrieveJWKS() []auth.PublicKeyInfo {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "retrieve_jwks").Add(1)
|
||||
ms.latency.With("method", "retrieve_jwks").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.RetrieveJWKS()
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListUserRefreshTokens(ctx context.Context, userID string) ([]auth.TokenInfo, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_user_refresh_tokens").Add(1)
|
||||
ms.latency.With("method", "list_user_refresh_tokens").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListUserRefreshTokens(ctx, userID)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Authorize(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "authorize").Add(1)
|
||||
ms.latency.With("method", "authorize").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.Authorize(ctx, pr)
|
||||
return ms.svc.Authorize(ctx, pr, patAuthz)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) CreatePAT(ctx context.Context, token, name, description string, duration time.Duration) (auth.PAT, error) {
|
||||
@@ -187,10 +213,10 @@ func (ms *metricsMiddleware) IdentifyPAT(ctx context.Context, paToken string) (a
|
||||
return ms.svc.IdentifyPAT(ctx, paToken)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) AuthorizePAT(ctx context.Context, userID, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error {
|
||||
func (ms *metricsMiddleware) AuthorizePAT(ctx context.Context, userID, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "authorize_pat").Add(1)
|
||||
ms.latency.With("method", "authorize_pat").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return ms.svc.AuthorizePAT(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
|
||||
return ms.svc.AuthorizePAT(ctx, userID, patID, entityType, domainID, operation, entityID)
|
||||
}
|
||||
|
||||
+46
-10
@@ -61,8 +61,31 @@ func (tm *tracingMiddleware) Identify(ctx context.Context, token string) (auth.K
|
||||
return tm.svc.Identify(ctx, token)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) Authorize(ctx context.Context, pr policies.Policy) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "authorize", trace.WithAttributes(
|
||||
func (tm *tracingMiddleware) RetrieveJWKS() []auth.PublicKeyInfo {
|
||||
return tm.svc.RetrieveJWKS()
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) RevokeToken(ctx context.Context, userID, tokenID string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "revoke_token", trace.WithAttributes(
|
||||
attribute.String("user_id", userID),
|
||||
attribute.String("token_id", tokenID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.RevokeToken(ctx, userID, tokenID)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) ListUserRefreshTokens(ctx context.Context, userID string) ([]auth.TokenInfo, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "list_user_refresh_tokens", trace.WithAttributes(
|
||||
attribute.String("user_id", userID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.ListUserRefreshTokens(ctx, userID)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) Authorize(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz) error {
|
||||
attributes := []attribute.KeyValue{
|
||||
attribute.String("subject", pr.Subject),
|
||||
attribute.String("subject_type", pr.SubjectType),
|
||||
attribute.String("subject_relation", pr.SubjectRelation),
|
||||
@@ -70,10 +93,23 @@ func (tm *tracingMiddleware) Authorize(ctx context.Context, pr policies.Policy)
|
||||
attribute.String("object_type", pr.ObjectType),
|
||||
attribute.String("relation", pr.Relation),
|
||||
attribute.String("permission", pr.Permission),
|
||||
))
|
||||
}
|
||||
|
||||
if patAuthz != nil {
|
||||
attributes = append(attributes,
|
||||
attribute.String("pat_id", patAuthz.PatID),
|
||||
attribute.String("pat_user_id", patAuthz.UserID),
|
||||
attribute.String("pat_entity_type", patAuthz.EntityType.String()),
|
||||
attribute.String("pat_entity_id", patAuthz.EntityID),
|
||||
attribute.String("pat_operation", patAuthz.Operation),
|
||||
attribute.String("pat_domain", patAuthz.Domain),
|
||||
)
|
||||
}
|
||||
|
||||
ctx, span := tm.tracer.Start(ctx, "authorize", trace.WithAttributes(attributes...))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Authorize(ctx, pr)
|
||||
return tm.svc.Authorize(ctx, pr, patAuthz)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) CreatePAT(ctx context.Context, token, name, description string, duration time.Duration) (auth.PAT, error) {
|
||||
@@ -165,8 +201,8 @@ func (tm *tracingMiddleware) AddScope(ctx context.Context, token, patID string,
|
||||
var attributes []attribute.KeyValue
|
||||
for _, s := range scopes {
|
||||
attributes = append(attributes, attribute.String("entity_type", s.EntityType.String()))
|
||||
attributes = append(attributes, attribute.String("optional_domain_id", s.OptionalDomainID))
|
||||
attributes = append(attributes, attribute.String("operation", s.Operation.String()))
|
||||
attributes = append(attributes, attribute.String("domain_id", s.DomainID))
|
||||
attributes = append(attributes, attribute.String("operation", s.Operation))
|
||||
attributes = append(attributes, attribute.String("entity_id", s.EntityID))
|
||||
}
|
||||
|
||||
@@ -204,14 +240,14 @@ func (tm *tracingMiddleware) IdentifyPAT(ctx context.Context, paToken string) (a
|
||||
return tm.svc.IdentifyPAT(ctx, paToken)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) AuthorizePAT(ctx context.Context, userID, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error {
|
||||
func (tm *tracingMiddleware) AuthorizePAT(ctx context.Context, userID, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "authorize_pat", trace.WithAttributes(
|
||||
attribute.String("pat_id", patID),
|
||||
attribute.String("entity_type", entityType.String()),
|
||||
attribute.String("optional_domain_id", optionalDomainID),
|
||||
attribute.String("operation", operation.String()),
|
||||
attribute.String("domain_id", domainID),
|
||||
attribute.String("operation", operation),
|
||||
attribute.String("entities", entityID),
|
||||
))
|
||||
defer span.End()
|
||||
return tm.svc.AuthorizePAT(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
|
||||
return tm.svc.AuthorizePAT(ctx, userID, patID, entityType, domainID, operation, entityID)
|
||||
}
|
||||
|
||||
+15
-8
@@ -11,6 +11,7 @@ package mocks
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
@@ -43,16 +44,16 @@ func (_m *Authz) EXPECT() *Authz_Expecter {
|
||||
}
|
||||
|
||||
// Authorize provides a mock function for the type Authz
|
||||
func (_mock *Authz) Authorize(ctx context.Context, pr policies.Policy) error {
|
||||
ret := _mock.Called(ctx, pr)
|
||||
func (_mock *Authz) Authorize(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz) error {
|
||||
ret := _mock.Called(ctx, pr, patAuthz)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Authorize")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, policies.Policy) error); ok {
|
||||
r0 = returnFunc(ctx, pr)
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, policies.Policy, *auth.PATAuthz) error); ok {
|
||||
r0 = returnFunc(ctx, pr, patAuthz)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
@@ -67,11 +68,12 @@ type Authz_Authorize_Call struct {
|
||||
// Authorize is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - pr policies.Policy
|
||||
func (_e *Authz_Expecter) Authorize(ctx interface{}, pr interface{}) *Authz_Authorize_Call {
|
||||
return &Authz_Authorize_Call{Call: _e.mock.On("Authorize", ctx, pr)}
|
||||
// - patAuthz *auth.PATAuthz
|
||||
func (_e *Authz_Expecter) Authorize(ctx interface{}, pr interface{}, patAuthz interface{}) *Authz_Authorize_Call {
|
||||
return &Authz_Authorize_Call{Call: _e.mock.On("Authorize", ctx, pr, patAuthz)}
|
||||
}
|
||||
|
||||
func (_c *Authz_Authorize_Call) Run(run func(ctx context.Context, pr policies.Policy)) *Authz_Authorize_Call {
|
||||
func (_c *Authz_Authorize_Call) Run(run func(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz)) *Authz_Authorize_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
@@ -81,9 +83,14 @@ func (_c *Authz_Authorize_Call) Run(run func(ctx context.Context, pr policies.Po
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(policies.Policy)
|
||||
}
|
||||
var arg2 *auth.PATAuthz
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(*auth.PATAuthz)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
@@ -94,7 +101,7 @@ func (_c *Authz_Authorize_Call) Return(err error) *Authz_Authorize_Call {
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Authz_Authorize_Call) RunAndReturn(run func(ctx context.Context, pr policies.Policy) error) *Authz_Authorize_Call {
|
||||
func (_c *Authz_Authorize_Call) RunAndReturn(run func(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz) error) *Authz_Authorize_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
+7
-7
@@ -43,7 +43,7 @@ func (_m *Cache) EXPECT() *Cache_Expecter {
|
||||
}
|
||||
|
||||
// CheckScope provides a mock function for the type Cache
|
||||
func (_mock *Cache) CheckScope(ctx context.Context, userID string, patID string, optionalDomainID string, entityType auth.EntityType, operation auth.Operation, entityID string) bool {
|
||||
func (_mock *Cache) CheckScope(ctx context.Context, userID string, patID string, optionalDomainID string, entityType auth.EntityType, operation string, entityID string) bool {
|
||||
ret := _mock.Called(ctx, userID, patID, optionalDomainID, entityType, operation, entityID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
@@ -51,7 +51,7 @@ func (_mock *Cache) CheckScope(ctx context.Context, userID string, patID string,
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, auth.EntityType, auth.Operation, string) bool); ok {
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, auth.EntityType, string, string) bool); ok {
|
||||
r0 = returnFunc(ctx, userID, patID, optionalDomainID, entityType, operation, entityID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
@@ -70,13 +70,13 @@ type Cache_CheckScope_Call struct {
|
||||
// - patID string
|
||||
// - optionalDomainID string
|
||||
// - entityType auth.EntityType
|
||||
// - operation auth.Operation
|
||||
// - operation string
|
||||
// - entityID string
|
||||
func (_e *Cache_Expecter) CheckScope(ctx interface{}, userID interface{}, patID interface{}, optionalDomainID interface{}, entityType interface{}, operation interface{}, entityID interface{}) *Cache_CheckScope_Call {
|
||||
return &Cache_CheckScope_Call{Call: _e.mock.On("CheckScope", ctx, userID, patID, optionalDomainID, entityType, operation, entityID)}
|
||||
}
|
||||
|
||||
func (_c *Cache_CheckScope_Call) Run(run func(ctx context.Context, userID string, patID string, optionalDomainID string, entityType auth.EntityType, operation auth.Operation, entityID string)) *Cache_CheckScope_Call {
|
||||
func (_c *Cache_CheckScope_Call) Run(run func(ctx context.Context, userID string, patID string, optionalDomainID string, entityType auth.EntityType, operation string, entityID string)) *Cache_CheckScope_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
@@ -98,9 +98,9 @@ func (_c *Cache_CheckScope_Call) Run(run func(ctx context.Context, userID string
|
||||
if args[4] != nil {
|
||||
arg4 = args[4].(auth.EntityType)
|
||||
}
|
||||
var arg5 auth.Operation
|
||||
var arg5 string
|
||||
if args[5] != nil {
|
||||
arg5 = args[5].(auth.Operation)
|
||||
arg5 = args[5].(string)
|
||||
}
|
||||
var arg6 string
|
||||
if args[6] != nil {
|
||||
@@ -124,7 +124,7 @@ func (_c *Cache_CheckScope_Call) Return(b bool) *Cache_CheckScope_Call {
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Cache_CheckScope_Call) RunAndReturn(run func(ctx context.Context, userID string, patID string, optionalDomainID string, entityType auth.EntityType, operation auth.Operation, entityID string) bool) *Cache_CheckScope_Call {
|
||||
func (_c *Cache_CheckScope_Call) RunAndReturn(run func(ctx context.Context, userID string, patID string, optionalDomainID string, entityType auth.EntityType, operation string, entityID string) bool) *Cache_CheckScope_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
+12
-12
@@ -113,16 +113,16 @@ func (_c *PATS_AddScope_Call) RunAndReturn(run func(ctx context.Context, token s
|
||||
}
|
||||
|
||||
// AuthorizePAT provides a mock function for the type PATS
|
||||
func (_mock *PATS) AuthorizePAT(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error {
|
||||
ret := _mock.Called(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
|
||||
func (_mock *PATS) AuthorizePAT(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error {
|
||||
ret := _mock.Called(ctx, userID, patID, entityType, domainID, operation, entityID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for AuthorizePAT")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, auth.EntityType, string, auth.Operation, string) error); ok {
|
||||
r0 = returnFunc(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, auth.EntityType, string, string, string) error); ok {
|
||||
r0 = returnFunc(ctx, userID, patID, entityType, domainID, operation, entityID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
@@ -139,14 +139,14 @@ type PATS_AuthorizePAT_Call struct {
|
||||
// - userID string
|
||||
// - patID string
|
||||
// - entityType auth.EntityType
|
||||
// - optionalDomainID string
|
||||
// - operation auth.Operation
|
||||
// - domainID string
|
||||
// - operation string
|
||||
// - entityID string
|
||||
func (_e *PATS_Expecter) AuthorizePAT(ctx interface{}, userID interface{}, patID interface{}, entityType interface{}, optionalDomainID interface{}, operation interface{}, entityID interface{}) *PATS_AuthorizePAT_Call {
|
||||
return &PATS_AuthorizePAT_Call{Call: _e.mock.On("AuthorizePAT", ctx, userID, patID, entityType, optionalDomainID, operation, entityID)}
|
||||
func (_e *PATS_Expecter) AuthorizePAT(ctx interface{}, userID interface{}, patID interface{}, entityType interface{}, domainID interface{}, operation interface{}, entityID interface{}) *PATS_AuthorizePAT_Call {
|
||||
return &PATS_AuthorizePAT_Call{Call: _e.mock.On("AuthorizePAT", ctx, userID, patID, entityType, domainID, operation, entityID)}
|
||||
}
|
||||
|
||||
func (_c *PATS_AuthorizePAT_Call) Run(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string)) *PATS_AuthorizePAT_Call {
|
||||
func (_c *PATS_AuthorizePAT_Call) Run(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string)) *PATS_AuthorizePAT_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
@@ -168,9 +168,9 @@ func (_c *PATS_AuthorizePAT_Call) Run(run func(ctx context.Context, userID strin
|
||||
if args[4] != nil {
|
||||
arg4 = args[4].(string)
|
||||
}
|
||||
var arg5 auth.Operation
|
||||
var arg5 string
|
||||
if args[5] != nil {
|
||||
arg5 = args[5].(auth.Operation)
|
||||
arg5 = args[5].(string)
|
||||
}
|
||||
var arg6 string
|
||||
if args[6] != nil {
|
||||
@@ -194,7 +194,7 @@ func (_c *PATS_AuthorizePAT_Call) Return(err error) *PATS_AuthorizePAT_Call {
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *PATS_AuthorizePAT_Call) RunAndReturn(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error) *PATS_AuthorizePAT_Call {
|
||||
func (_c *PATS_AuthorizePAT_Call) RunAndReturn(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error) *PATS_AuthorizePAT_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -107,16 +107,16 @@ func (_c *PATSRepository_AddScope_Call) RunAndReturn(run func(ctx context.Contex
|
||||
}
|
||||
|
||||
// CheckScope provides a mock function for the type PATSRepository
|
||||
func (_mock *PATSRepository) CheckScope(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error {
|
||||
ret := _mock.Called(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
|
||||
func (_mock *PATSRepository) CheckScope(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error {
|
||||
ret := _mock.Called(ctx, userID, patID, entityType, domainID, operation, entityID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CheckScope")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, auth.EntityType, string, auth.Operation, string) error); ok {
|
||||
r0 = returnFunc(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, auth.EntityType, string, string, string) error); ok {
|
||||
r0 = returnFunc(ctx, userID, patID, entityType, domainID, operation, entityID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
@@ -133,14 +133,14 @@ type PATSRepository_CheckScope_Call struct {
|
||||
// - userID string
|
||||
// - patID string
|
||||
// - entityType auth.EntityType
|
||||
// - optionalDomainID string
|
||||
// - operation auth.Operation
|
||||
// - domainID string
|
||||
// - operation string
|
||||
// - entityID string
|
||||
func (_e *PATSRepository_Expecter) CheckScope(ctx interface{}, userID interface{}, patID interface{}, entityType interface{}, optionalDomainID interface{}, operation interface{}, entityID interface{}) *PATSRepository_CheckScope_Call {
|
||||
return &PATSRepository_CheckScope_Call{Call: _e.mock.On("CheckScope", ctx, userID, patID, entityType, optionalDomainID, operation, entityID)}
|
||||
func (_e *PATSRepository_Expecter) CheckScope(ctx interface{}, userID interface{}, patID interface{}, entityType interface{}, domainID interface{}, operation interface{}, entityID interface{}) *PATSRepository_CheckScope_Call {
|
||||
return &PATSRepository_CheckScope_Call{Call: _e.mock.On("CheckScope", ctx, userID, patID, entityType, domainID, operation, entityID)}
|
||||
}
|
||||
|
||||
func (_c *PATSRepository_CheckScope_Call) Run(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string)) *PATSRepository_CheckScope_Call {
|
||||
func (_c *PATSRepository_CheckScope_Call) Run(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string)) *PATSRepository_CheckScope_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
@@ -162,9 +162,9 @@ func (_c *PATSRepository_CheckScope_Call) Run(run func(ctx context.Context, user
|
||||
if args[4] != nil {
|
||||
arg4 = args[4].(string)
|
||||
}
|
||||
var arg5 auth.Operation
|
||||
var arg5 string
|
||||
if args[5] != nil {
|
||||
arg5 = args[5].(auth.Operation)
|
||||
arg5 = args[5].(string)
|
||||
}
|
||||
var arg6 string
|
||||
if args[6] != nil {
|
||||
@@ -188,7 +188,7 @@ func (_c *PATSRepository_CheckScope_Call) Return(err error) *PATSRepository_Chec
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *PATSRepository_CheckScope_Call) RunAndReturn(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error) *PATSRepository_CheckScope_Call {
|
||||
func (_c *PATSRepository_CheckScope_Call) RunAndReturn(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error) *PATSRepository_CheckScope_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
+203
-20
@@ -114,16 +114,16 @@ func (_c *Service_AddScope_Call) RunAndReturn(run func(ctx context.Context, toke
|
||||
}
|
||||
|
||||
// Authorize provides a mock function for the type Service
|
||||
func (_mock *Service) Authorize(ctx context.Context, pr policies.Policy) error {
|
||||
ret := _mock.Called(ctx, pr)
|
||||
func (_mock *Service) Authorize(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz) error {
|
||||
ret := _mock.Called(ctx, pr, patAuthz)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Authorize")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, policies.Policy) error); ok {
|
||||
r0 = returnFunc(ctx, pr)
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, policies.Policy, *auth.PATAuthz) error); ok {
|
||||
r0 = returnFunc(ctx, pr, patAuthz)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
@@ -138,11 +138,12 @@ type Service_Authorize_Call struct {
|
||||
// Authorize is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - pr policies.Policy
|
||||
func (_e *Service_Expecter) Authorize(ctx interface{}, pr interface{}) *Service_Authorize_Call {
|
||||
return &Service_Authorize_Call{Call: _e.mock.On("Authorize", ctx, pr)}
|
||||
// - patAuthz *auth.PATAuthz
|
||||
func (_e *Service_Expecter) Authorize(ctx interface{}, pr interface{}, patAuthz interface{}) *Service_Authorize_Call {
|
||||
return &Service_Authorize_Call{Call: _e.mock.On("Authorize", ctx, pr, patAuthz)}
|
||||
}
|
||||
|
||||
func (_c *Service_Authorize_Call) Run(run func(ctx context.Context, pr policies.Policy)) *Service_Authorize_Call {
|
||||
func (_c *Service_Authorize_Call) Run(run func(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz)) *Service_Authorize_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
@@ -152,9 +153,14 @@ func (_c *Service_Authorize_Call) Run(run func(ctx context.Context, pr policies.
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(policies.Policy)
|
||||
}
|
||||
var arg2 *auth.PATAuthz
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(*auth.PATAuthz)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
@@ -165,22 +171,22 @@ func (_c *Service_Authorize_Call) Return(err error) *Service_Authorize_Call {
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_Authorize_Call) RunAndReturn(run func(ctx context.Context, pr policies.Policy) error) *Service_Authorize_Call {
|
||||
func (_c *Service_Authorize_Call) RunAndReturn(run func(ctx context.Context, pr policies.Policy, patAuthz *auth.PATAuthz) error) *Service_Authorize_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// AuthorizePAT provides a mock function for the type Service
|
||||
func (_mock *Service) AuthorizePAT(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error {
|
||||
ret := _mock.Called(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
|
||||
func (_mock *Service) AuthorizePAT(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error {
|
||||
ret := _mock.Called(ctx, userID, patID, entityType, domainID, operation, entityID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for AuthorizePAT")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, auth.EntityType, string, auth.Operation, string) error); ok {
|
||||
r0 = returnFunc(ctx, userID, patID, entityType, optionalDomainID, operation, entityID)
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, auth.EntityType, string, string, string) error); ok {
|
||||
r0 = returnFunc(ctx, userID, patID, entityType, domainID, operation, entityID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
@@ -197,14 +203,14 @@ type Service_AuthorizePAT_Call struct {
|
||||
// - userID string
|
||||
// - patID string
|
||||
// - entityType auth.EntityType
|
||||
// - optionalDomainID string
|
||||
// - operation auth.Operation
|
||||
// - domainID string
|
||||
// - operation string
|
||||
// - entityID string
|
||||
func (_e *Service_Expecter) AuthorizePAT(ctx interface{}, userID interface{}, patID interface{}, entityType interface{}, optionalDomainID interface{}, operation interface{}, entityID interface{}) *Service_AuthorizePAT_Call {
|
||||
return &Service_AuthorizePAT_Call{Call: _e.mock.On("AuthorizePAT", ctx, userID, patID, entityType, optionalDomainID, operation, entityID)}
|
||||
func (_e *Service_Expecter) AuthorizePAT(ctx interface{}, userID interface{}, patID interface{}, entityType interface{}, domainID interface{}, operation interface{}, entityID interface{}) *Service_AuthorizePAT_Call {
|
||||
return &Service_AuthorizePAT_Call{Call: _e.mock.On("AuthorizePAT", ctx, userID, patID, entityType, domainID, operation, entityID)}
|
||||
}
|
||||
|
||||
func (_c *Service_AuthorizePAT_Call) Run(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string)) *Service_AuthorizePAT_Call {
|
||||
func (_c *Service_AuthorizePAT_Call) Run(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string)) *Service_AuthorizePAT_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
@@ -226,9 +232,9 @@ func (_c *Service_AuthorizePAT_Call) Run(run func(ctx context.Context, userID st
|
||||
if args[4] != nil {
|
||||
arg4 = args[4].(string)
|
||||
}
|
||||
var arg5 auth.Operation
|
||||
var arg5 string
|
||||
if args[5] != nil {
|
||||
arg5 = args[5].(auth.Operation)
|
||||
arg5 = args[5].(string)
|
||||
}
|
||||
var arg6 string
|
||||
if args[6] != nil {
|
||||
@@ -252,7 +258,7 @@ func (_c *Service_AuthorizePAT_Call) Return(err error) *Service_AuthorizePAT_Cal
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_AuthorizePAT_Call) RunAndReturn(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error) *Service_AuthorizePAT_Call {
|
||||
func (_c *Service_AuthorizePAT_Call) RunAndReturn(run func(ctx context.Context, userID string, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error) *Service_AuthorizePAT_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -752,6 +758,74 @@ func (_c *Service_ListScopes_Call) RunAndReturn(run func(ctx context.Context, to
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListUserRefreshTokens provides a mock function for the type Service
|
||||
func (_mock *Service) ListUserRefreshTokens(ctx context.Context, userID string) ([]auth.TokenInfo, error) {
|
||||
ret := _mock.Called(ctx, userID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListUserRefreshTokens")
|
||||
}
|
||||
|
||||
var r0 []auth.TokenInfo
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]auth.TokenInfo, error)); ok {
|
||||
return returnFunc(ctx, userID)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) []auth.TokenInfo); ok {
|
||||
r0 = returnFunc(ctx, userID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]auth.TokenInfo)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = returnFunc(ctx, userID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Service_ListUserRefreshTokens_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListUserRefreshTokens'
|
||||
type Service_ListUserRefreshTokens_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListUserRefreshTokens is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - userID string
|
||||
func (_e *Service_Expecter) ListUserRefreshTokens(ctx interface{}, userID interface{}) *Service_ListUserRefreshTokens_Call {
|
||||
return &Service_ListUserRefreshTokens_Call{Call: _e.mock.On("ListUserRefreshTokens", ctx, userID)}
|
||||
}
|
||||
|
||||
func (_c *Service_ListUserRefreshTokens_Call) Run(run func(ctx context.Context, userID string)) *Service_ListUserRefreshTokens_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_ListUserRefreshTokens_Call) Return(tokenInfos []auth.TokenInfo, err error) *Service_ListUserRefreshTokens_Call {
|
||||
_c.Call.Return(tokenInfos, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_ListUserRefreshTokens_Call) RunAndReturn(run func(ctx context.Context, userID string) ([]auth.TokenInfo, error)) *Service_ListUserRefreshTokens_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RemoveAllPAT provides a mock function for the type Service
|
||||
func (_mock *Service) RemoveAllPAT(ctx context.Context, token string) error {
|
||||
ret := _mock.Called(ctx, token)
|
||||
@@ -1028,6 +1102,52 @@ func (_c *Service_ResetPATSecret_Call) RunAndReturn(run func(ctx context.Context
|
||||
return _c
|
||||
}
|
||||
|
||||
// RetrieveJWKS provides a mock function for the type Service
|
||||
func (_mock *Service) RetrieveJWKS() []auth.PublicKeyInfo {
|
||||
ret := _mock.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RetrieveJWKS")
|
||||
}
|
||||
|
||||
var r0 []auth.PublicKeyInfo
|
||||
if returnFunc, ok := ret.Get(0).(func() []auth.PublicKeyInfo); ok {
|
||||
r0 = returnFunc()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]auth.PublicKeyInfo)
|
||||
}
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Service_RetrieveJWKS_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveJWKS'
|
||||
type Service_RetrieveJWKS_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RetrieveJWKS is a helper method to define mock.On call
|
||||
func (_e *Service_Expecter) RetrieveJWKS() *Service_RetrieveJWKS_Call {
|
||||
return &Service_RetrieveJWKS_Call{Call: _e.mock.On("RetrieveJWKS")}
|
||||
}
|
||||
|
||||
func (_c *Service_RetrieveJWKS_Call) Run(run func()) *Service_RetrieveJWKS_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_RetrieveJWKS_Call) Return(publicKeyInfos []auth.PublicKeyInfo) *Service_RetrieveJWKS_Call {
|
||||
_c.Call.Return(publicKeyInfos)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_RetrieveJWKS_Call) RunAndReturn(run func() []auth.PublicKeyInfo) *Service_RetrieveJWKS_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RetrieveKey provides a mock function for the type Service
|
||||
func (_mock *Service) RetrieveKey(ctx context.Context, token string, id string) (auth.Key, error) {
|
||||
ret := _mock.Called(ctx, token, id)
|
||||
@@ -1298,6 +1418,69 @@ func (_c *Service_RevokePATSecret_Call) RunAndReturn(run func(ctx context.Contex
|
||||
return _c
|
||||
}
|
||||
|
||||
// RevokeToken provides a mock function for the type Service
|
||||
func (_mock *Service) RevokeToken(ctx context.Context, userID string, tokenID string) error {
|
||||
ret := _mock.Called(ctx, userID, tokenID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RevokeToken")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = returnFunc(ctx, userID, tokenID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Service_RevokeToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RevokeToken'
|
||||
type Service_RevokeToken_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RevokeToken is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - userID string
|
||||
// - tokenID string
|
||||
func (_e *Service_Expecter) RevokeToken(ctx interface{}, userID interface{}, tokenID interface{}) *Service_RevokeToken_Call {
|
||||
return &Service_RevokeToken_Call{Call: _e.mock.On("RevokeToken", ctx, userID, tokenID)}
|
||||
}
|
||||
|
||||
func (_c *Service_RevokeToken_Call) Run(run func(ctx context.Context, userID string, tokenID string)) *Service_RevokeToken_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_RevokeToken_Call) Return(err error) *Service_RevokeToken_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_RevokeToken_Call) RunAndReturn(run func(ctx context.Context, userID string, tokenID string) error) *Service_RevokeToken_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdatePATDescription provides a mock function for the type Service
|
||||
func (_mock *Service) UpdatePATDescription(ctx context.Context, token string, patID string, description string) (auth.PAT, error) {
|
||||
ret := _mock.Called(ctx, token, patID, description)
|
||||
|
||||
@@ -126,6 +126,89 @@ func (_c *TokenServiceClient_Issue_Call) RunAndReturn(run func(ctx context.Conte
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListUserRefreshTokens provides a mock function for the type TokenServiceClient
|
||||
func (_mock *TokenServiceClient) ListUserRefreshTokens(ctx context.Context, in *v1.ListUserRefreshTokensReq, opts ...grpc.CallOption) (*v1.ListUserRefreshTokensRes, error) {
|
||||
var tmpRet mock.Arguments
|
||||
if len(opts) > 0 {
|
||||
tmpRet = _mock.Called(ctx, in, opts)
|
||||
} else {
|
||||
tmpRet = _mock.Called(ctx, in)
|
||||
}
|
||||
ret := tmpRet
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListUserRefreshTokens")
|
||||
}
|
||||
|
||||
var r0 *v1.ListUserRefreshTokensRes
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *v1.ListUserRefreshTokensReq, ...grpc.CallOption) (*v1.ListUserRefreshTokensRes, error)); ok {
|
||||
return returnFunc(ctx, in, opts...)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *v1.ListUserRefreshTokensReq, ...grpc.CallOption) *v1.ListUserRefreshTokensRes); ok {
|
||||
r0 = returnFunc(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v1.ListUserRefreshTokensRes)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, *v1.ListUserRefreshTokensReq, ...grpc.CallOption) error); ok {
|
||||
r1 = returnFunc(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// TokenServiceClient_ListUserRefreshTokens_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListUserRefreshTokens'
|
||||
type TokenServiceClient_ListUserRefreshTokens_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListUserRefreshTokens is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - in *v1.ListUserRefreshTokensReq
|
||||
// - opts ...grpc.CallOption
|
||||
func (_e *TokenServiceClient_Expecter) ListUserRefreshTokens(ctx interface{}, in interface{}, opts ...interface{}) *TokenServiceClient_ListUserRefreshTokens_Call {
|
||||
return &TokenServiceClient_ListUserRefreshTokens_Call{Call: _e.mock.On("ListUserRefreshTokens",
|
||||
append([]interface{}{ctx, in}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *TokenServiceClient_ListUserRefreshTokens_Call) Run(run func(ctx context.Context, in *v1.ListUserRefreshTokensReq, opts ...grpc.CallOption)) *TokenServiceClient_ListUserRefreshTokens_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 *v1.ListUserRefreshTokensReq
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(*v1.ListUserRefreshTokensReq)
|
||||
}
|
||||
var arg2 []grpc.CallOption
|
||||
var variadicArgs []grpc.CallOption
|
||||
if len(args) > 2 {
|
||||
variadicArgs = args[2].([]grpc.CallOption)
|
||||
}
|
||||
arg2 = variadicArgs
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2...,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *TokenServiceClient_ListUserRefreshTokens_Call) Return(listUserRefreshTokensRes *v1.ListUserRefreshTokensRes, err error) *TokenServiceClient_ListUserRefreshTokens_Call {
|
||||
_c.Call.Return(listUserRefreshTokensRes, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *TokenServiceClient_ListUserRefreshTokens_Call) RunAndReturn(run func(ctx context.Context, in *v1.ListUserRefreshTokensReq, opts ...grpc.CallOption) (*v1.ListUserRefreshTokensRes, error)) *TokenServiceClient_ListUserRefreshTokens_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Refresh provides a mock function for the type TokenServiceClient
|
||||
func (_mock *TokenServiceClient) Refresh(ctx context.Context, in *v1.RefreshReq, opts ...grpc.CallOption) (*v1.Token, error) {
|
||||
var tmpRet mock.Arguments
|
||||
@@ -208,3 +291,86 @@ func (_c *TokenServiceClient_Refresh_Call) RunAndReturn(run func(ctx context.Con
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Revoke provides a mock function for the type TokenServiceClient
|
||||
func (_mock *TokenServiceClient) Revoke(ctx context.Context, in *v1.RevokeReq, opts ...grpc.CallOption) (*v1.RevokeRes, error) {
|
||||
var tmpRet mock.Arguments
|
||||
if len(opts) > 0 {
|
||||
tmpRet = _mock.Called(ctx, in, opts)
|
||||
} else {
|
||||
tmpRet = _mock.Called(ctx, in)
|
||||
}
|
||||
ret := tmpRet
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Revoke")
|
||||
}
|
||||
|
||||
var r0 *v1.RevokeRes
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *v1.RevokeReq, ...grpc.CallOption) (*v1.RevokeRes, error)); ok {
|
||||
return returnFunc(ctx, in, opts...)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *v1.RevokeReq, ...grpc.CallOption) *v1.RevokeRes); ok {
|
||||
r0 = returnFunc(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v1.RevokeRes)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, *v1.RevokeReq, ...grpc.CallOption) error); ok {
|
||||
r1 = returnFunc(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// TokenServiceClient_Revoke_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Revoke'
|
||||
type TokenServiceClient_Revoke_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Revoke is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - in *v1.RevokeReq
|
||||
// - opts ...grpc.CallOption
|
||||
func (_e *TokenServiceClient_Expecter) Revoke(ctx interface{}, in interface{}, opts ...interface{}) *TokenServiceClient_Revoke_Call {
|
||||
return &TokenServiceClient_Revoke_Call{Call: _e.mock.On("Revoke",
|
||||
append([]interface{}{ctx, in}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *TokenServiceClient_Revoke_Call) Run(run func(ctx context.Context, in *v1.RevokeReq, opts ...grpc.CallOption)) *TokenServiceClient_Revoke_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 *v1.RevokeReq
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(*v1.RevokeReq)
|
||||
}
|
||||
var arg2 []grpc.CallOption
|
||||
var variadicArgs []grpc.CallOption
|
||||
if len(args) > 2 {
|
||||
variadicArgs = args[2].([]grpc.CallOption)
|
||||
}
|
||||
arg2 = variadicArgs
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2...,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *TokenServiceClient_Revoke_Call) Return(revokeRes *v1.RevokeRes, err error) *TokenServiceClient_Revoke_Call {
|
||||
_c.Call.Return(revokeRes, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *TokenServiceClient_Revoke_Call) RunAndReturn(run func(ctx context.Context, in *v1.RevokeReq, opts ...grpc.CallOption) (*v1.RevokeRes, error)) *TokenServiceClient_Revoke_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewTokenizer creates a new instance of Tokenizer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewTokenizer(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Tokenizer {
|
||||
mock := &Tokenizer{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// Tokenizer is an autogenerated mock type for the Tokenizer type
|
||||
type Tokenizer struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type Tokenizer_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *Tokenizer) EXPECT() *Tokenizer_Expecter {
|
||||
return &Tokenizer_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Issue provides a mock function for the type Tokenizer
|
||||
func (_mock *Tokenizer) Issue(key auth.Key) (string, error) {
|
||||
ret := _mock.Called(key)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Issue")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(auth.Key) (string, error)); ok {
|
||||
return returnFunc(key)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(auth.Key) string); ok {
|
||||
r0 = returnFunc(key)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(auth.Key) error); ok {
|
||||
r1 = returnFunc(key)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Tokenizer_Issue_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Issue'
|
||||
type Tokenizer_Issue_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Issue is a helper method to define mock.On call
|
||||
// - key auth.Key
|
||||
func (_e *Tokenizer_Expecter) Issue(key interface{}) *Tokenizer_Issue_Call {
|
||||
return &Tokenizer_Issue_Call{Call: _e.mock.On("Issue", key)}
|
||||
}
|
||||
|
||||
func (_c *Tokenizer_Issue_Call) Run(run func(key auth.Key)) *Tokenizer_Issue_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 auth.Key
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(auth.Key)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Tokenizer_Issue_Call) Return(token string, err error) *Tokenizer_Issue_Call {
|
||||
_c.Call.Return(token, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Tokenizer_Issue_Call) RunAndReturn(run func(key auth.Key) (string, error)) *Tokenizer_Issue_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Parse provides a mock function for the type Tokenizer
|
||||
func (_mock *Tokenizer) Parse(ctx context.Context, token string) (auth.Key, error) {
|
||||
ret := _mock.Called(ctx, token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Parse")
|
||||
}
|
||||
|
||||
var r0 auth.Key
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) (auth.Key, error)); ok {
|
||||
return returnFunc(ctx, token)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) auth.Key); ok {
|
||||
r0 = returnFunc(ctx, token)
|
||||
} else {
|
||||
r0 = ret.Get(0).(auth.Key)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = returnFunc(ctx, token)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Tokenizer_Parse_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Parse'
|
||||
type Tokenizer_Parse_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Parse is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - token string
|
||||
func (_e *Tokenizer_Expecter) Parse(ctx interface{}, token interface{}) *Tokenizer_Parse_Call {
|
||||
return &Tokenizer_Parse_Call{Call: _e.mock.On("Parse", ctx, token)}
|
||||
}
|
||||
|
||||
func (_c *Tokenizer_Parse_Call) Run(run func(ctx context.Context, token string)) *Tokenizer_Parse_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Tokenizer_Parse_Call) Return(key auth.Key, err error) *Tokenizer_Parse_Call {
|
||||
_c.Call.Return(key, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Tokenizer_Parse_Call) RunAndReturn(run func(ctx context.Context, token string) (auth.Key, error)) *Tokenizer_Parse_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RetrieveJWKS provides a mock function for the type Tokenizer
|
||||
func (_mock *Tokenizer) RetrieveJWKS() ([]auth.PublicKeyInfo, error) {
|
||||
ret := _mock.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RetrieveJWKS")
|
||||
}
|
||||
|
||||
var r0 []auth.PublicKeyInfo
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func() ([]auth.PublicKeyInfo, error)); ok {
|
||||
return returnFunc()
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func() []auth.PublicKeyInfo); ok {
|
||||
r0 = returnFunc()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]auth.PublicKeyInfo)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = returnFunc()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Tokenizer_RetrieveJWKS_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveJWKS'
|
||||
type Tokenizer_RetrieveJWKS_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RetrieveJWKS is a helper method to define mock.On call
|
||||
func (_e *Tokenizer_Expecter) RetrieveJWKS() *Tokenizer_RetrieveJWKS_Call {
|
||||
return &Tokenizer_RetrieveJWKS_Call{Call: _e.mock.On("RetrieveJWKS")}
|
||||
}
|
||||
|
||||
func (_c *Tokenizer_RetrieveJWKS_Call) Run(run func()) *Tokenizer_RetrieveJWKS_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Tokenizer_RetrieveJWKS_Call) Return(publicKeyInfos []auth.PublicKeyInfo, err error) *Tokenizer_RetrieveJWKS_Call {
|
||||
_c.Call.Return(publicKeyInfos, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Tokenizer_RetrieveJWKS_Call) RunAndReturn(run func() ([]auth.PublicKeyInfo, error)) *Tokenizer_RetrieveJWKS_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewUserActiveTokensCache creates a new instance of UserActiveTokensCache. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewUserActiveTokensCache(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *UserActiveTokensCache {
|
||||
mock := &UserActiveTokensCache{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// UserActiveTokensCache is an autogenerated mock type for the UserActiveTokensCache type
|
||||
type UserActiveTokensCache struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type UserActiveTokensCache_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *UserActiveTokensCache) EXPECT() *UserActiveTokensCache_Expecter {
|
||||
return &UserActiveTokensCache_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// IsActive provides a mock function for the type UserActiveTokensCache
|
||||
func (_mock *UserActiveTokensCache) IsActive(ctx context.Context, tokenID string) (bool, error) {
|
||||
ret := _mock.Called(ctx, tokenID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for IsActive")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok {
|
||||
return returnFunc(ctx, tokenID)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) bool); ok {
|
||||
r0 = returnFunc(ctx, tokenID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = returnFunc(ctx, tokenID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UserActiveTokensCache_IsActive_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsActive'
|
||||
type UserActiveTokensCache_IsActive_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// IsActive is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - tokenID string
|
||||
func (_e *UserActiveTokensCache_Expecter) IsActive(ctx interface{}, tokenID interface{}) *UserActiveTokensCache_IsActive_Call {
|
||||
return &UserActiveTokensCache_IsActive_Call{Call: _e.mock.On("IsActive", ctx, tokenID)}
|
||||
}
|
||||
|
||||
func (_c *UserActiveTokensCache_IsActive_Call) Run(run func(ctx context.Context, tokenID string)) *UserActiveTokensCache_IsActive_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *UserActiveTokensCache_IsActive_Call) Return(b bool, err error) *UserActiveTokensCache_IsActive_Call {
|
||||
_c.Call.Return(b, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *UserActiveTokensCache_IsActive_Call) RunAndReturn(run func(ctx context.Context, tokenID string) (bool, error)) *UserActiveTokensCache_IsActive_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListUserTokens provides a mock function for the type UserActiveTokensCache
|
||||
func (_mock *UserActiveTokensCache) ListUserTokens(ctx context.Context, userID string) ([]auth.TokenInfo, error) {
|
||||
ret := _mock.Called(ctx, userID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListUserTokens")
|
||||
}
|
||||
|
||||
var r0 []auth.TokenInfo
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]auth.TokenInfo, error)); ok {
|
||||
return returnFunc(ctx, userID)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) []auth.TokenInfo); ok {
|
||||
r0 = returnFunc(ctx, userID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]auth.TokenInfo)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = returnFunc(ctx, userID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UserActiveTokensCache_ListUserTokens_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListUserTokens'
|
||||
type UserActiveTokensCache_ListUserTokens_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListUserTokens is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - userID string
|
||||
func (_e *UserActiveTokensCache_Expecter) ListUserTokens(ctx interface{}, userID interface{}) *UserActiveTokensCache_ListUserTokens_Call {
|
||||
return &UserActiveTokensCache_ListUserTokens_Call{Call: _e.mock.On("ListUserTokens", ctx, userID)}
|
||||
}
|
||||
|
||||
func (_c *UserActiveTokensCache_ListUserTokens_Call) Run(run func(ctx context.Context, userID string)) *UserActiveTokensCache_ListUserTokens_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *UserActiveTokensCache_ListUserTokens_Call) Return(tokenInfos []auth.TokenInfo, err error) *UserActiveTokensCache_ListUserTokens_Call {
|
||||
_c.Call.Return(tokenInfos, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *UserActiveTokensCache_ListUserTokens_Call) RunAndReturn(run func(ctx context.Context, userID string) ([]auth.TokenInfo, error)) *UserActiveTokensCache_ListUserTokens_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RemoveActive provides a mock function for the type UserActiveTokensCache
|
||||
func (_mock *UserActiveTokensCache) RemoveActive(ctx context.Context, userID string, tokenID string) error {
|
||||
ret := _mock.Called(ctx, userID, tokenID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RemoveActive")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = returnFunc(ctx, userID, tokenID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// UserActiveTokensCache_RemoveActive_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveActive'
|
||||
type UserActiveTokensCache_RemoveActive_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RemoveActive is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - userID string
|
||||
// - tokenID string
|
||||
func (_e *UserActiveTokensCache_Expecter) RemoveActive(ctx interface{}, userID interface{}, tokenID interface{}) *UserActiveTokensCache_RemoveActive_Call {
|
||||
return &UserActiveTokensCache_RemoveActive_Call{Call: _e.mock.On("RemoveActive", ctx, userID, tokenID)}
|
||||
}
|
||||
|
||||
func (_c *UserActiveTokensCache_RemoveActive_Call) Run(run func(ctx context.Context, userID string, tokenID string)) *UserActiveTokensCache_RemoveActive_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *UserActiveTokensCache_RemoveActive_Call) Return(err error) *UserActiveTokensCache_RemoveActive_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *UserActiveTokensCache_RemoveActive_Call) RunAndReturn(run func(ctx context.Context, userID string, tokenID string) error) *UserActiveTokensCache_RemoveActive_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SaveActive provides a mock function for the type UserActiveTokensCache
|
||||
func (_mock *UserActiveTokensCache) SaveActive(ctx context.Context, userID string, tokenID string, description string, expiry time.Time) error {
|
||||
ret := _mock.Called(ctx, userID, tokenID, description, expiry)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SaveActive")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, time.Time) error); ok {
|
||||
r0 = returnFunc(ctx, userID, tokenID, description, expiry)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// UserActiveTokensCache_SaveActive_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveActive'
|
||||
type UserActiveTokensCache_SaveActive_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SaveActive is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - userID string
|
||||
// - tokenID string
|
||||
// - description string
|
||||
// - expiry time.Time
|
||||
func (_e *UserActiveTokensCache_Expecter) SaveActive(ctx interface{}, userID interface{}, tokenID interface{}, description interface{}, expiry interface{}) *UserActiveTokensCache_SaveActive_Call {
|
||||
return &UserActiveTokensCache_SaveActive_Call{Call: _e.mock.On("SaveActive", ctx, userID, tokenID, description, expiry)}
|
||||
}
|
||||
|
||||
func (_c *UserActiveTokensCache_SaveActive_Call) Run(run func(ctx context.Context, userID string, tokenID string, description string, expiry time.Time)) *UserActiveTokensCache_SaveActive_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
var arg3 string
|
||||
if args[3] != nil {
|
||||
arg3 = args[3].(string)
|
||||
}
|
||||
var arg4 time.Time
|
||||
if args[4] != nil {
|
||||
arg4 = args[4].(time.Time)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
arg3,
|
||||
arg4,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *UserActiveTokensCache_SaveActive_Call) Return(err error) *UserActiveTokensCache_SaveActive_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *UserActiveTokensCache_SaveActive_Call) RunAndReturn(run func(ctx context.Context, userID string, tokenID string, description string, expiry time.Time) error) *UserActiveTokensCache_SaveActive_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
+143
-144
@@ -12,114 +12,53 @@ import (
|
||||
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
)
|
||||
|
||||
const AnyIDs = "*"
|
||||
|
||||
type Operation uint32
|
||||
|
||||
const (
|
||||
CreateOp Operation = iota
|
||||
ReadOp
|
||||
ListOp
|
||||
UpdateOp
|
||||
DeleteOp
|
||||
ShareOp
|
||||
UnshareOp
|
||||
PublishOp
|
||||
SubscribeOp
|
||||
"github.com/absmach/supermq/pkg/permissions"
|
||||
)
|
||||
|
||||
const (
|
||||
createOpStr = "create"
|
||||
readOpStr = "read"
|
||||
listOpStr = "list"
|
||||
updateOpStr = "update"
|
||||
deleteOpStr = "delete"
|
||||
shareOpStr = "share"
|
||||
UnshareOpStr = "unshare"
|
||||
PublishOpStr = "publish"
|
||||
SubscribeOpStr = "subscribe"
|
||||
AnyIDs = "*"
|
||||
RoleOperationPrefix = "role_"
|
||||
)
|
||||
|
||||
func (op Operation) String() string {
|
||||
switch op {
|
||||
case CreateOp:
|
||||
return createOpStr
|
||||
case ReadOp:
|
||||
return readOpStr
|
||||
case ListOp:
|
||||
return listOpStr
|
||||
case UpdateOp:
|
||||
return updateOpStr
|
||||
case DeleteOp:
|
||||
return deleteOpStr
|
||||
case ShareOp:
|
||||
return shareOpStr
|
||||
case UnshareOp:
|
||||
return UnshareOpStr
|
||||
case PublishOp:
|
||||
return PublishOpStr
|
||||
case SubscribeOp:
|
||||
return SubscribeOpStr
|
||||
default:
|
||||
return fmt.Sprintf("unknown operation type %d", op)
|
||||
}
|
||||
}
|
||||
const (
|
||||
OpCreate = "create"
|
||||
OpList = "list"
|
||||
|
||||
func (op Operation) ValidString() (string, error) {
|
||||
str := op.String()
|
||||
if str == fmt.Sprintf("unknown operation type %d", op) {
|
||||
return "", errors.New(str)
|
||||
}
|
||||
return str, nil
|
||||
}
|
||||
OpCreateClients = "create_clients"
|
||||
OpListClients = "list_clients"
|
||||
OpCreateChannels = "create_channels"
|
||||
OpListChannels = "list_channels"
|
||||
OpCreateGroups = "create_groups"
|
||||
OpListGroups = "list_groups"
|
||||
|
||||
func ParseOperation(op string) (Operation, error) {
|
||||
switch op {
|
||||
case createOpStr:
|
||||
return CreateOp, nil
|
||||
case readOpStr:
|
||||
return ReadOp, nil
|
||||
case listOpStr:
|
||||
return ListOp, nil
|
||||
case updateOpStr:
|
||||
return UpdateOp, nil
|
||||
case deleteOpStr:
|
||||
return DeleteOp, nil
|
||||
case shareOpStr:
|
||||
return ShareOp, nil
|
||||
case UnshareOpStr:
|
||||
return UnshareOp, nil
|
||||
case PublishOpStr:
|
||||
return PublishOp, nil
|
||||
case SubscribeOpStr:
|
||||
return SubscribeOp, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown operation type %s", op)
|
||||
}
|
||||
}
|
||||
OpShare = "share"
|
||||
OpUnshare = "unshare"
|
||||
|
||||
func (op Operation) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(op.String())
|
||||
}
|
||||
OpDashboardShare = "dashboard_share"
|
||||
OpDashboardUnshare = "dashboard_unshare"
|
||||
|
||||
func (op *Operation) UnmarshalJSON(data []byte) error {
|
||||
str := strings.Trim(string(data), "\"")
|
||||
val, err := ParseOperation(str)
|
||||
*op = val
|
||||
return err
|
||||
}
|
||||
OpPublish = "publish"
|
||||
OpSubscribe = "subscribe"
|
||||
|
||||
func (op Operation) MarshalText() (text []byte, err error) {
|
||||
return []byte(op.String()), nil
|
||||
}
|
||||
OpMessagePublish = "message_publish"
|
||||
OpMessageSubscribe = "message_subscribe"
|
||||
)
|
||||
|
||||
func (op *Operation) UnmarshalText(data []byte) (err error) {
|
||||
str := strings.Trim(string(data), "\"")
|
||||
*op, err = ParseOperation(str)
|
||||
return err
|
||||
}
|
||||
var errInvalidEntityOp = errors.NewRequestError("operation not valid for entity type")
|
||||
|
||||
type Operation = permissions.Operation
|
||||
|
||||
// Dashboard operations.
|
||||
const (
|
||||
DashboardShareOp Operation = iota + 400
|
||||
DashboardUnshareOp
|
||||
)
|
||||
|
||||
// Messages operations.
|
||||
const (
|
||||
MessagePublishOp Operation = iota + 500
|
||||
MessageSubscribeOp
|
||||
)
|
||||
|
||||
type EntityType uint32
|
||||
|
||||
@@ -127,20 +66,20 @@ const (
|
||||
GroupsType EntityType = iota
|
||||
ChannelsType
|
||||
ClientsType
|
||||
DomainsType
|
||||
UsersType
|
||||
DashboardType
|
||||
MessagesType
|
||||
DomainsType
|
||||
UsersType
|
||||
)
|
||||
|
||||
const (
|
||||
GroupsScopeStr = "groups"
|
||||
ChannelsScopeStr = "channels"
|
||||
ClientsScopeStr = "clients"
|
||||
DomainsStr = "domains"
|
||||
UsersStr = "users"
|
||||
DashboardsStr = "dashboards"
|
||||
MessagesStr = "messages"
|
||||
DomainsStr = "domains"
|
||||
UsersStr = "users"
|
||||
)
|
||||
|
||||
func (et EntityType) String() string {
|
||||
@@ -151,27 +90,19 @@ func (et EntityType) String() string {
|
||||
return ChannelsScopeStr
|
||||
case ClientsType:
|
||||
return ClientsScopeStr
|
||||
case DomainsType:
|
||||
return DomainsStr
|
||||
case UsersType:
|
||||
return UsersStr
|
||||
case DashboardType:
|
||||
return DashboardsStr
|
||||
case MessagesType:
|
||||
return MessagesStr
|
||||
case DomainsType:
|
||||
return DomainsStr
|
||||
case UsersType:
|
||||
return UsersStr
|
||||
default:
|
||||
return fmt.Sprintf("unknown domain entity type %d", et)
|
||||
}
|
||||
}
|
||||
|
||||
func (et EntityType) ValidString() (string, error) {
|
||||
str := et.String()
|
||||
if str == fmt.Sprintf("unknown operation type %d", et) {
|
||||
return "", errors.New(str)
|
||||
}
|
||||
return str, nil
|
||||
}
|
||||
|
||||
func ParseEntityType(et string) (EntityType, error) {
|
||||
switch et {
|
||||
case GroupsScopeStr:
|
||||
@@ -180,12 +111,14 @@ func ParseEntityType(et string) (EntityType, error) {
|
||||
return ChannelsType, nil
|
||||
case ClientsScopeStr:
|
||||
return ClientsType, nil
|
||||
case DashboardsStr:
|
||||
return DashboardType, nil
|
||||
case MessagesStr:
|
||||
return MessagesType, nil
|
||||
case DomainsStr:
|
||||
return DomainsType, nil
|
||||
case UsersStr:
|
||||
return UsersType, nil
|
||||
case DashboardsStr:
|
||||
return DashboardType, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown domain entity type %s", et)
|
||||
}
|
||||
@@ -212,39 +145,101 @@ func (et *EntityType) UnmarshalText(data []byte) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func IsValidOperationForEntity(entityType EntityType, operation string) bool {
|
||||
switch entityType {
|
||||
case ClientsType, ChannelsType, GroupsType, DomainsType:
|
||||
return true
|
||||
case DashboardType:
|
||||
return operation == OpDashboardShare || operation == OpDashboardUnshare
|
||||
case MessagesType:
|
||||
return operation == OpMessagePublish || operation == OpMessageSubscribe
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Example Scope as JSON
|
||||
//
|
||||
// [
|
||||
// {
|
||||
// "optional_domain_id": "domain_1",
|
||||
// "domain_id": "domain_1",
|
||||
// "entity_type": "groups",
|
||||
// "operation": "create",
|
||||
// "operation": "view",
|
||||
// "entity_id": "*"
|
||||
// },
|
||||
// {
|
||||
// "optional_domain_id": "domain_1",
|
||||
// "domain_id": "domain_1",
|
||||
// "entity_type": "channels",
|
||||
// "operation": "delete",
|
||||
// "entity_id": "channel1"
|
||||
// },
|
||||
// {
|
||||
// "optional_domain_id": "domain_1",
|
||||
// "entity_type": "things",
|
||||
// "domain_id": "domain_1",
|
||||
// "entity_type": "clients",
|
||||
// "operation": "update",
|
||||
// "entity_id": "*"
|
||||
// }
|
||||
// ]
|
||||
|
||||
type Scope struct {
|
||||
ID string `json:"id"`
|
||||
PatID string `json:"pat_id"`
|
||||
OptionalDomainID string `json:"optional_domain_id"`
|
||||
EntityType EntityType `json:"entity_type"`
|
||||
EntityID string `json:"entity_id"`
|
||||
Operation Operation `json:"operation"`
|
||||
ID string `json:"id"`
|
||||
PatID string `json:"pat_id"`
|
||||
DomainID string `json:"domain_id"`
|
||||
EntityType EntityType `json:"entity_type"`
|
||||
EntityID string `json:"entity_id"`
|
||||
Operation string `json:"operation"`
|
||||
}
|
||||
|
||||
func (s *Scope) Authorized(entityType EntityType, optionalDomainID string, operation Operation, entityID string) bool {
|
||||
func (s *Scope) UnmarshalJSON(data []byte) error {
|
||||
type Alias Scope
|
||||
aux := (*Alias)(s)
|
||||
|
||||
if err := json.Unmarshal(data, aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch s.EntityType {
|
||||
case ClientsType:
|
||||
switch s.Operation {
|
||||
case OpCreate:
|
||||
s.Operation = OpCreateClients
|
||||
case OpList:
|
||||
s.Operation = OpListClients
|
||||
}
|
||||
case ChannelsType:
|
||||
switch s.Operation {
|
||||
case OpCreate:
|
||||
s.Operation = OpCreateChannels
|
||||
case OpList:
|
||||
s.Operation = OpListChannels
|
||||
}
|
||||
case GroupsType:
|
||||
switch s.Operation {
|
||||
case OpCreate:
|
||||
s.Operation = OpCreateGroups
|
||||
case OpList:
|
||||
s.Operation = OpListGroups
|
||||
}
|
||||
case DashboardType:
|
||||
switch s.Operation {
|
||||
case OpShare:
|
||||
s.Operation = OpDashboardShare
|
||||
case OpUnshare:
|
||||
s.Operation = OpDashboardUnshare
|
||||
}
|
||||
case MessagesType:
|
||||
switch s.Operation {
|
||||
case OpPublish:
|
||||
s.Operation = OpMessagePublish
|
||||
case OpSubscribe:
|
||||
s.Operation = OpMessageSubscribe
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scope) Authorized(entityType EntityType, domainID string, operation string, entityID string) bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
@@ -253,7 +248,7 @@ func (s *Scope) Authorized(entityType EntityType, optionalDomainID string, opera
|
||||
return false
|
||||
}
|
||||
|
||||
if optionalDomainID != "" && s.OptionalDomainID != optionalDomainID {
|
||||
if s.DomainID != "" && s.DomainID != domainID {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -268,6 +263,7 @@ func (s *Scope) Authorized(entityType EntityType, optionalDomainID string, opera
|
||||
if s.EntityID == entityID {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -279,16 +275,27 @@ func (s *Scope) Validate() error {
|
||||
return apiutil.ErrMissingEntityID
|
||||
}
|
||||
|
||||
switch s.EntityType {
|
||||
case ChannelsType, GroupsType, ClientsType:
|
||||
if s.OptionalDomainID == "" {
|
||||
return apiutil.ErrMissingDomainID
|
||||
}
|
||||
if s.DomainID == "" {
|
||||
return apiutil.ErrMissingDomainID
|
||||
}
|
||||
|
||||
if !IsValidOperationForEntity(s.EntityType, s.Operation) {
|
||||
return errors.Wrap(apiutil.ErrInvalidQueryParams, errInvalidEntityOp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PATAuthz represents the PAT authorization request fields.
|
||||
type PATAuthz struct {
|
||||
PatID string
|
||||
UserID string
|
||||
EntityType EntityType
|
||||
EntityID string
|
||||
Operation string
|
||||
Domain string
|
||||
}
|
||||
|
||||
// PAT represents Personal Access Token.
|
||||
type PAT struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
@@ -356,14 +363,6 @@ func (pat *PAT) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pat *PAT) String() string {
|
||||
str, err := json.MarshalIndent(pat, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("failed to convert PAT to string: json marshal error :%s", err.Error())
|
||||
}
|
||||
return string(str)
|
||||
}
|
||||
|
||||
// PATS specifies function which are required for Personal access Token implementation.
|
||||
type PATS interface {
|
||||
// Create function creates new PAT for given valid inputs.
|
||||
@@ -409,7 +408,7 @@ type PATS interface {
|
||||
IdentifyPAT(ctx context.Context, paToken string) (PAT, error)
|
||||
|
||||
// AuthorizePAT function will valid the secret and check the given scope exists.
|
||||
AuthorizePAT(ctx context.Context, userID, patID string, entityType EntityType, optionalDomainID string, operation Operation, entityID string) error
|
||||
AuthorizePAT(ctx context.Context, userID, patID string, entityType EntityType, domainID string, operation string, entityID string) error
|
||||
}
|
||||
|
||||
// PATSRepository specifies PATS persistence API.
|
||||
@@ -454,7 +453,7 @@ type PATSRepository interface {
|
||||
|
||||
RemoveScope(ctx context.Context, userID string, scopesIDs ...string) error
|
||||
|
||||
CheckScope(ctx context.Context, userID, patID string, entityType EntityType, optionalDomainID string, operation Operation, entityID string) error
|
||||
CheckScope(ctx context.Context, userID, patID string, entityType EntityType, domainID string, operation string, entityID string) error
|
||||
|
||||
RemoveAllScope(ctx context.Context, patID string) error
|
||||
}
|
||||
@@ -462,7 +461,7 @@ type PATSRepository interface {
|
||||
type Cache interface {
|
||||
Save(ctx context.Context, userID string, scopes []Scope) error
|
||||
|
||||
CheckScope(ctx context.Context, userID, patID, optionalDomainID string, entityType EntityType, operation Operation, entityID string) bool
|
||||
CheckScope(ctx context.Context, userID, patID, optionalDomainID string, entityType EntityType, operation string, entityID string) bool
|
||||
|
||||
Remove(ctx context.Context, userID string, scopesID []string) error
|
||||
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEntityTypeString(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
et auth.EntityType
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "Groups entity type",
|
||||
et: auth.GroupsType,
|
||||
expected: "groups",
|
||||
},
|
||||
{
|
||||
desc: "Channels entity type",
|
||||
et: auth.ChannelsType,
|
||||
expected: "channels",
|
||||
},
|
||||
{
|
||||
desc: "Clients entity type",
|
||||
et: auth.ClientsType,
|
||||
expected: "clients",
|
||||
},
|
||||
{
|
||||
desc: "Dashboard entity type",
|
||||
et: auth.DashboardType,
|
||||
expected: "dashboards",
|
||||
},
|
||||
{
|
||||
desc: "Messages entity type",
|
||||
et: auth.MessagesType,
|
||||
expected: "messages",
|
||||
},
|
||||
{
|
||||
desc: "Unknown entity type",
|
||||
et: auth.EntityType(100),
|
||||
expected: "unknown domain entity type 100",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := tc.et.String()
|
||||
assert.Equal(t, tc.expected, got, "String() = %v, expected %v", got, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEntityType(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
et string
|
||||
expected auth.EntityType
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "Parse groups",
|
||||
et: "groups",
|
||||
expected: auth.GroupsType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Parse channels",
|
||||
et: "channels",
|
||||
expected: auth.ChannelsType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Parse clients",
|
||||
et: "clients",
|
||||
expected: auth.ClientsType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Parse dashboards",
|
||||
et: "dashboards",
|
||||
expected: auth.DashboardType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Parse unknown entity type",
|
||||
et: "unknown",
|
||||
expected: auth.EntityType(0),
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got, err := auth.ParseEntityType(tc.et)
|
||||
if tc.err {
|
||||
assert.Error(t, err, "ParseEntityType() should return error")
|
||||
} else {
|
||||
assert.NoError(t, err, "ParseEntityType() should not return error")
|
||||
assert.Equal(t, tc.expected, got, "ParseEntityType() = %v, expected %v", got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityTypeMarshalJSON(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
et auth.EntityType
|
||||
expected []byte
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "Marshal groups",
|
||||
et: auth.GroupsType,
|
||||
expected: []byte(`"groups"`),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Marshal channels",
|
||||
et: auth.ChannelsType,
|
||||
expected: []byte(`"channels"`),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Marshal clients",
|
||||
et: auth.ClientsType,
|
||||
expected: []byte(`"clients"`),
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got, err := tc.et.MarshalJSON()
|
||||
assert.Equal(t, tc.err, err, "MarshalJSON() error = %v, expected %v", err, tc.err)
|
||||
assert.Equal(t, tc.expected, got, "MarshalJSON() = %v, expected %v", got, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityTypeUnmarshalJSON(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
data []byte
|
||||
expected auth.EntityType
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "Unmarshal groups",
|
||||
data: []byte(`"groups"`),
|
||||
expected: auth.GroupsType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Unmarshal channels",
|
||||
data: []byte(`"channels"`),
|
||||
expected: auth.ChannelsType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Unmarshal unknown",
|
||||
data: []byte(`"unknown"`),
|
||||
expected: auth.EntityType(0),
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
var et auth.EntityType
|
||||
err := et.UnmarshalJSON(tc.data)
|
||||
if tc.err {
|
||||
assert.Error(t, err, "UnmarshalJSON() should return error")
|
||||
} else {
|
||||
assert.NoError(t, err, "UnmarshalJSON() should not return error")
|
||||
assert.Equal(t, tc.expected, et, "UnmarshalJSON() = %v, expected %v", et, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityTypeMarshalText(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
et auth.EntityType
|
||||
expected []byte
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "Marshal groups as text",
|
||||
et: auth.GroupsType,
|
||||
expected: []byte("groups"),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Marshal channels as text",
|
||||
et: auth.ChannelsType,
|
||||
expected: []byte("channels"),
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got, err := tc.et.MarshalText()
|
||||
assert.Equal(t, tc.err, err, "MarshalText() error = %v, expected %v", err, tc.err)
|
||||
assert.Equal(t, tc.expected, got, "MarshalText() = %v, expected %v", got, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityTypeUnmarshalText(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
data []byte
|
||||
expected auth.EntityType
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "Unmarshal groups from text",
|
||||
data: []byte("groups"),
|
||||
expected: auth.GroupsType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Unmarshal channels from text",
|
||||
data: []byte("channels"),
|
||||
expected: auth.ChannelsType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Unmarshal unknown from text",
|
||||
data: []byte("unknown"),
|
||||
expected: auth.EntityType(0),
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
var et auth.EntityType
|
||||
err := et.UnmarshalText(tc.data)
|
||||
if tc.err {
|
||||
assert.Error(t, err, "UnmarshalText() should return error")
|
||||
} else {
|
||||
assert.NoError(t, err, "UnmarshalText() should not return error")
|
||||
assert.Equal(t, tc.expected, et, "UnmarshalText() = %v, expected %v", et, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import "github.com/absmach/supermq/pkg/errors"
|
||||
|
||||
var _ errors.Mapper = (*duplicateErrors)(nil)
|
||||
|
||||
type duplicateErrors struct{}
|
||||
|
||||
// GetError maps constraint names to known errors.
|
||||
func (d duplicateErrors) GetError(constraint string) (error, bool) {
|
||||
switch constraint {
|
||||
case "revoked_tokens_pkey":
|
||||
return errors.NewRequestError("revoked token already exists"), true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func NewDuplicateErrors() errors.Mapper {
|
||||
return duplicateErrors{}
|
||||
}
|
||||
@@ -125,6 +125,15 @@ func Migration() *migrate.MemoryMigrationSource {
|
||||
`ALTER TABLE pats ALTER COLUMN last_used_at TYPE TIMESTAMP;`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "auth_7",
|
||||
Up: []string{
|
||||
`ALTER TABLE pat_scopes RENAME COLUMN optional_domain_id TO domain_id;`,
|
||||
},
|
||||
Down: []string{
|
||||
`ALTER TABLE pat_scopes RENAME COLUMN domain_id TO optional_domain_id;`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+18
-22
@@ -26,12 +26,12 @@ type dbPat struct {
|
||||
}
|
||||
|
||||
type dbScope struct {
|
||||
ID string `db:"id,omitempty"`
|
||||
PatID string `db:"pat_id,omitempty"`
|
||||
OptionalDomainID string `db:"optional_domain_id,omitempty"`
|
||||
EntityType string `db:"entity_type,omitempty"`
|
||||
EntityID string `db:"entity_id,omitempty"`
|
||||
Operation string `db:"operation,omitempty"`
|
||||
ID string `db:"id,omitempty"`
|
||||
PatID string `db:"pat_id,omitempty"`
|
||||
DomainID string `db:"domain_id,omitempty"`
|
||||
EntityType string `db:"entity_type,omitempty"`
|
||||
EntityID string `db:"entity_id,omitempty"`
|
||||
Operation string `db:"operation,omitempty"`
|
||||
}
|
||||
|
||||
type dbPagemeta struct {
|
||||
@@ -92,17 +92,13 @@ func toAuthScope(dsc []dbScope) ([]auth.Scope, error) {
|
||||
if err != nil {
|
||||
return []auth.Scope{}, err
|
||||
}
|
||||
operation, err := auth.ParseOperation(s.Operation)
|
||||
if err != nil {
|
||||
return []auth.Scope{}, err
|
||||
}
|
||||
scope = append(scope, auth.Scope{
|
||||
ID: s.ID,
|
||||
PatID: s.PatID,
|
||||
OptionalDomainID: s.OptionalDomainID,
|
||||
EntityType: entityType,
|
||||
EntityID: s.EntityID,
|
||||
Operation: operation,
|
||||
ID: s.ID,
|
||||
PatID: s.PatID,
|
||||
DomainID: s.DomainID,
|
||||
EntityType: entityType,
|
||||
EntityID: s.EntityID,
|
||||
Operation: s.Operation,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -152,12 +148,12 @@ func toDBScope(sc []auth.Scope) []dbScope {
|
||||
var scopes []dbScope
|
||||
for _, s := range sc {
|
||||
scopes = append(scopes, dbScope{
|
||||
ID: s.ID,
|
||||
PatID: s.PatID,
|
||||
OptionalDomainID: s.OptionalDomainID,
|
||||
EntityType: s.EntityType.String(),
|
||||
EntityID: s.EntityID,
|
||||
Operation: s.Operation.String(),
|
||||
ID: s.ID,
|
||||
PatID: s.PatID,
|
||||
DomainID: s.DomainID,
|
||||
EntityType: s.EntityType.String(),
|
||||
EntityID: s.EntityID,
|
||||
Operation: s.Operation,
|
||||
})
|
||||
}
|
||||
return scopes
|
||||
|
||||
+36
-38
@@ -18,6 +18,8 @@ import (
|
||||
|
||||
var _ auth.PATSRepository = (*patRepo)(nil)
|
||||
|
||||
var errInsufficientPATScope = errors.NewRequestError("PAT does not have the required scope permissions")
|
||||
|
||||
type patRepo struct {
|
||||
db postgres.Database
|
||||
cache auth.Cache
|
||||
@@ -374,8 +376,8 @@ func (pr *patRepo) RemoveAllPAT(ctx context.Context, userID string) error {
|
||||
|
||||
func (pr *patRepo) AddScope(ctx context.Context, userID string, scopes []auth.Scope) error {
|
||||
q := `
|
||||
INSERT INTO pat_scopes (id, pat_id, entity_type, optional_domain_id, operation, entity_id)
|
||||
VALUES (:id, :pat_id, :entity_type, :optional_domain_id, :operation, :entity_id)`
|
||||
INSERT INTO pat_scopes (id, pat_id, entity_type, domain_id, operation, entity_id)
|
||||
VALUES (:id, :pat_id, :entity_type, :domain_id, :operation, :entity_id)`
|
||||
|
||||
var newScopes []auth.Scope
|
||||
|
||||
@@ -410,17 +412,17 @@ func (pr *patRepo) processScope(ctx context.Context, sc auth.Scope) (auth.Scope,
|
||||
FROM pat_scopes
|
||||
WHERE pat_id = :pat_id
|
||||
AND entity_type = :entity_type
|
||||
AND optional_domain_id = :optional_domain_id
|
||||
AND domain_id = :domain_id
|
||||
AND operation = :operation
|
||||
AND entity_id = :entity_id
|
||||
LIMIT 1`
|
||||
|
||||
params := dbScope{
|
||||
PatID: sc.PatID,
|
||||
OptionalDomainID: sc.OptionalDomainID,
|
||||
EntityType: sc.EntityType.String(),
|
||||
Operation: sc.Operation.String(),
|
||||
EntityID: auth.AnyIDs,
|
||||
PatID: sc.PatID,
|
||||
DomainID: sc.DomainID,
|
||||
EntityType: sc.EntityType.String(),
|
||||
Operation: sc.Operation,
|
||||
EntityID: auth.AnyIDs,
|
||||
}
|
||||
|
||||
rows, err := pr.db.NamedQueryContext(ctx, q, params)
|
||||
@@ -442,10 +444,10 @@ func (pr *patRepo) processScope(ctx context.Context, sc auth.Scope) (auth.Scope,
|
||||
|
||||
if sc.EntityID == auth.AnyIDs {
|
||||
newParams := dbScope{
|
||||
PatID: sc.PatID,
|
||||
OptionalDomainID: sc.OptionalDomainID,
|
||||
EntityType: sc.EntityType.String(),
|
||||
Operation: sc.Operation.String(),
|
||||
PatID: sc.PatID,
|
||||
DomainID: sc.DomainID,
|
||||
EntityType: sc.EntityType.String(),
|
||||
Operation: sc.Operation,
|
||||
}
|
||||
|
||||
checkEntityQuery := `
|
||||
@@ -453,7 +455,7 @@ func (pr *patRepo) processScope(ctx context.Context, sc auth.Scope) (auth.Scope,
|
||||
FROM pat_scopes
|
||||
WHERE pat_id = :pat_id
|
||||
AND entity_type = :entity_type
|
||||
AND optional_domain_id = :optional_domain_id
|
||||
AND domain_id = :domain_id
|
||||
AND operation = :operation
|
||||
LIMIT 1`
|
||||
|
||||
@@ -476,7 +478,7 @@ func (pr *patRepo) processScope(ctx context.Context, sc auth.Scope) (auth.Scope,
|
||||
SET entity_id = :entity_id
|
||||
WHERE pat_id = :pat_id
|
||||
AND entity_type = :entity_type
|
||||
AND optional_domain_id = :optional_domain_id
|
||||
AND domain_id = :domain_id
|
||||
AND operation = :operation`
|
||||
|
||||
rows, err = pr.db.NamedQueryContext(ctx, updateWithWildcardQuery, params)
|
||||
@@ -493,9 +495,9 @@ func (pr *patRepo) processScope(ctx context.Context, sc auth.Scope) (auth.Scope,
|
||||
}
|
||||
|
||||
func (pr *patRepo) RemoveScope(ctx context.Context, userID string, scopesIDs ...string) error {
|
||||
deleteScopesQuery := fmt.Sprintf(`DELETE FROM pat_scopes WHERE id IN ('%s')`, strings.Join(scopesIDs, ","))
|
||||
deleteScopesQuery := `DELETE FROM pat_scopes WHERE id = ANY($1)`
|
||||
|
||||
res, err := pr.db.ExecContext(ctx, deleteScopesQuery)
|
||||
res, err := pr.db.ExecContext(ctx, deleteScopesQuery, scopesIDs)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
@@ -511,28 +513,28 @@ func (pr *patRepo) RemoveScope(ctx context.Context, userID string, scopesIDs ...
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pr *patRepo) CheckScope(ctx context.Context, userID, patID string, entityType auth.EntityType, optionalDomainID string, operation auth.Operation, entityID string) error {
|
||||
func (pr *patRepo) CheckScope(ctx context.Context, userID, patID string, entityType auth.EntityType, domainID string, operation string, entityID string) error {
|
||||
q := `
|
||||
SELECT id, pat_id, entity_type, optional_domain_id, operation, entity_id
|
||||
SELECT id, pat_id, entity_type, domain_id, operation, entity_id
|
||||
FROM pat_scopes
|
||||
WHERE pat_id = :pat_id
|
||||
AND entity_type = :entity_type
|
||||
AND optional_domain_id = :optional_domain_id
|
||||
AND domain_id = :domain_id
|
||||
AND operation = :operation
|
||||
AND (entity_id = :entity_id OR entity_id = '*')
|
||||
LIMIT 1`
|
||||
|
||||
authorized := pr.cache.CheckScope(ctx, userID, patID, optionalDomainID, entityType, operation, entityID)
|
||||
authorized := pr.cache.CheckScope(ctx, userID, patID, domainID, entityType, operation, entityID)
|
||||
if authorized {
|
||||
return nil
|
||||
}
|
||||
|
||||
scope := dbScope{
|
||||
PatID: patID,
|
||||
EntityType: entityType.String(),
|
||||
OptionalDomainID: optionalDomainID,
|
||||
Operation: operation.String(),
|
||||
EntityID: entityID,
|
||||
PatID: patID,
|
||||
EntityType: entityType.String(),
|
||||
DomainID: domainID,
|
||||
Operation: operation,
|
||||
EntityID: entityID,
|
||||
}
|
||||
|
||||
rows, err := pr.db.NamedQueryContext(ctx, q, scope)
|
||||
@@ -551,29 +553,25 @@ func (pr *patRepo) CheckScope(ctx context.Context, userID, patID string, entityT
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
operation, err := auth.ParseOperation(sc.Operation)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
authScope := auth.Scope{
|
||||
ID: sc.ID,
|
||||
PatID: sc.PatID,
|
||||
OptionalDomainID: sc.OptionalDomainID,
|
||||
EntityType: entityType,
|
||||
EntityID: sc.EntityID,
|
||||
Operation: operation,
|
||||
ID: sc.ID,
|
||||
PatID: sc.PatID,
|
||||
DomainID: sc.DomainID,
|
||||
EntityType: entityType,
|
||||
EntityID: sc.EntityID,
|
||||
Operation: sc.Operation,
|
||||
}
|
||||
|
||||
if err := pr.cache.Save(ctx, userID, []auth.Scope{authScope}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if authScope.Authorized(entityType, optionalDomainID, operation, entityID) {
|
||||
if authScope.Authorized(entityType, domainID, operation, entityID) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return repoerr.ErrNotFound
|
||||
return errInsufficientPATScope
|
||||
}
|
||||
|
||||
func (pr *patRepo) RemoveAllScope(ctx context.Context, patID string) error {
|
||||
@@ -625,7 +623,7 @@ func (pr *patRepo) RetrieveScope(ctx context.Context, pm auth.ScopesPageMeta) (a
|
||||
|
||||
func (pr *patRepo) retrieveScopeFromDB(ctx context.Context, pm dbPagemeta) ([]auth.Scope, error) {
|
||||
q := `
|
||||
SELECT id, pat_id, entity_type, optional_domain_id, operation, entity_id
|
||||
SELECT id, pat_id, entity_type, domain_id, operation, entity_id
|
||||
FROM pat_scopes WHERE pat_id = :pat_id OFFSET :offset LIMIT :limit`
|
||||
scopeRows, err := pr.db.NamedQueryContext(ctx, q, pm)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/auth"
|
||||
channelsOps "github.com/absmach/supermq/channels/operations"
|
||||
clientsOps "github.com/absmach/supermq/clients/operations"
|
||||
groupsOps "github.com/absmach/supermq/groups/operations"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScopeAuthorized(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
scope *auth.Scope
|
||||
entityType auth.EntityType
|
||||
domainID string
|
||||
operation string
|
||||
entityID string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
desc: "Authorized with matching entity type, domain, operation and entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "entity1",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Authorized with wildcard entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "*",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "any-entity",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Authorized without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ClientsType,
|
||||
DomainID: "",
|
||||
Operation: "view",
|
||||
EntityID: "client1",
|
||||
},
|
||||
entityType: auth.ClientsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "client1",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with different entity type",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.ChannelsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "entity1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with different domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain2",
|
||||
operation: "view",
|
||||
entityID: "entity1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with different operation",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "delete",
|
||||
entityID: "entity1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with different entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "entity2",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with nil scope",
|
||||
scope: nil,
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "entity1",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
result := tc.scope.Authorized(tc.entityType, tc.domainID, tc.operation, tc.entityID)
|
||||
assert.Equal(t, tc.expected, result, "Authorized() = %v, expected %v", result, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopeValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
scope *auth.Scope
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "Valid scope for groups with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope for channels with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ChannelsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "channel1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope for clients with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ClientsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "update",
|
||||
EntityID: "client1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope for messages with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.MessagesType,
|
||||
DomainID: "domain1",
|
||||
Operation: "message_publish",
|
||||
EntityID: "message1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope for dashboard with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.DashboardType,
|
||||
DomainID: "domain1",
|
||||
Operation: "dashboard_share",
|
||||
EntityID: "dashboard1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope with wildcard entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "*",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Invalid nil scope",
|
||||
scope: nil,
|
||||
err: assert.AnError, // Will be checked with Contains
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope without entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: groupsOps.OperationDetails()[groupsOps.OpViewGroup].Name,
|
||||
EntityID: "",
|
||||
},
|
||||
err: apiutil.ErrMissingEntityID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for groups without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "",
|
||||
Operation: groupsOps.OperationDetails()[groupsOps.OpViewGroup].Name,
|
||||
EntityID: "entity1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for channels without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ChannelsType,
|
||||
DomainID: "",
|
||||
Operation: channelsOps.OperationDetails()[channelsOps.OpViewChannel].Name,
|
||||
EntityID: "channel1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for clients without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ClientsType,
|
||||
DomainID: "",
|
||||
Operation: clientsOps.OperationDetails()[clientsOps.OpViewClient].Name,
|
||||
EntityID: "client1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for dashboard without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.DashboardType,
|
||||
DomainID: "",
|
||||
Operation: auth.OpShare,
|
||||
EntityID: "dashboard1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for messages without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.MessagesType,
|
||||
DomainID: "",
|
||||
Operation: auth.OpPublish,
|
||||
EntityID: "message1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.scope.Validate()
|
||||
if tc.err != nil {
|
||||
assert.Error(t, err, "Validate() should return error")
|
||||
if tc.err != assert.AnError {
|
||||
assert.Equal(t, tc.err, err, "Validate() error = %v, expected %v", err, tc.err)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err, "Validate() should not return error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPATValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
pat *auth.PAT
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "Valid PAT",
|
||||
pat: &auth.PAT{
|
||||
ID: "pat-id",
|
||||
User: "user-id",
|
||||
Name: "test-pat",
|
||||
Description: "test description",
|
||||
},
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Invalid nil PAT",
|
||||
pat: nil,
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid PAT without name",
|
||||
pat: &auth.PAT{
|
||||
ID: "pat-id",
|
||||
User: "user-id",
|
||||
Name: "",
|
||||
Description: "test description",
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid PAT without user",
|
||||
pat: &auth.PAT{
|
||||
ID: "pat-id",
|
||||
User: "",
|
||||
Name: "test-pat",
|
||||
Description: "test description",
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.pat.Validate()
|
||||
if tc.err {
|
||||
assert.Error(t, err, "Validate() should return error")
|
||||
} else {
|
||||
assert.NoError(t, err, "Validate() should not return error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPATMarshalUnmarshalBinary(t *testing.T) {
|
||||
pat := auth.PAT{
|
||||
ID: "pat-id",
|
||||
User: "user-id",
|
||||
Name: "test-pat",
|
||||
Description: "test description",
|
||||
Secret: "secret",
|
||||
IssuedAt: time.Now().UTC().Round(time.Second),
|
||||
ExpiresAt: time.Now().UTC().Add(24 * time.Hour).Round(time.Second),
|
||||
Status: auth.ActiveStatus,
|
||||
}
|
||||
|
||||
// Marshal
|
||||
data, err := pat.MarshalBinary()
|
||||
assert.NoError(t, err, "MarshalBinary() should not return error")
|
||||
assert.NotNil(t, data, "MarshalBinary() should return data")
|
||||
|
||||
// Unmarshal
|
||||
var newPAT auth.PAT
|
||||
err = newPAT.UnmarshalBinary(data)
|
||||
assert.NoError(t, err, "UnmarshalBinary() should not return error")
|
||||
|
||||
assert.Equal(t, pat.ID, newPAT.ID, "ID mismatch")
|
||||
assert.Equal(t, pat.User, newPAT.User, "User mismatch")
|
||||
assert.Equal(t, pat.Name, newPAT.Name, "Name mismatch")
|
||||
assert.Equal(t, pat.Description, newPAT.Description, "Description mismatch")
|
||||
assert.Equal(t, pat.Secret, newPAT.Secret, "Secret mismatch")
|
||||
assert.Equal(t, pat.Status, newPAT.Status, "Status mismatch")
|
||||
}
|
||||
+119
-77
@@ -5,13 +5,14 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
"github.com/google/uuid"
|
||||
@@ -19,7 +20,6 @@ import (
|
||||
|
||||
const (
|
||||
recoveryDuration = 5 * time.Minute
|
||||
defLimit = 100
|
||||
randStr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&&*|+-="
|
||||
patPrefix = "pat"
|
||||
patSecretSeparator = "_"
|
||||
@@ -29,22 +29,25 @@ var (
|
||||
// ErrExpiry indicates that the token is expired.
|
||||
ErrExpiry = errors.New("token is expired")
|
||||
|
||||
errIssueUser = errors.New("failed to issue new login key")
|
||||
errIssueTmp = errors.New("failed to issue new temporary key")
|
||||
errRevoke = errors.New("failed to remove key")
|
||||
errRetrieve = errors.New("failed to retrieve key data")
|
||||
errIdentify = errors.New("failed to validate token")
|
||||
errPlatform = errors.New("invalid platform id")
|
||||
errRoleAuth = errors.New("failed to authorize user role")
|
||||
errIssueUser = errors.New("failed to issue new login key")
|
||||
errIssueTmp = errors.New("failed to issue new temporary key")
|
||||
errRevoke = errors.New("failed to remove key")
|
||||
errRetrieve = errors.New("failed to retrieve key data")
|
||||
errIdentify = errors.New("failed to validate token")
|
||||
errPlatform = errors.New("invalid platform id")
|
||||
errRoleAuth = errors.New("failed to authorize user role")
|
||||
errSaveRefreshKey = errors.NewServiceError("failed to save refresh key")
|
||||
errRevokeRefreshKey = errors.NewServiceError("failed to revoke refresh key")
|
||||
errListRefreshKeys = errors.NewServiceError("failed to list refresh keys")
|
||||
|
||||
errMalformedPAT = errors.New("malformed personal access token")
|
||||
errFailedToParseUUID = errors.New("failed to parse string to UUID")
|
||||
errInvalidLenFor2UUIDs = errors.New("invalid input length for 2 UUID, excepted 32 byte")
|
||||
errRevokedPAT = errors.New("revoked pat")
|
||||
errCreatePAT = errors.New("failed to create PAT")
|
||||
errUpdatePAT = errors.New("failed to update PAT")
|
||||
errRetrievePAT = errors.New("failed to retrieve PAT")
|
||||
errDeletePAT = errors.New("failed to delete PAT")
|
||||
errRevokedPAT = errors.NewServiceError("revoked pat")
|
||||
errCreatePAT = errors.NewServiceError("failed to create PAT")
|
||||
errUpdatePAT = errors.NewServiceError("failed to update PAT")
|
||||
errRetrievePAT = errors.NewServiceError("failed to retrieve PAT")
|
||||
errDeletePAT = errors.NewServiceError("failed to delete PAT")
|
||||
errInvalidScope = errors.New("invalid scope")
|
||||
)
|
||||
|
||||
@@ -56,7 +59,7 @@ type Authz interface {
|
||||
// `object`. Authorize returns a non-nil error if the subject has
|
||||
// no relation on the object (which simply means the operation is
|
||||
// denied).
|
||||
Authorize(ctx context.Context, pr policies.Policy) error
|
||||
Authorize(ctx context.Context, pr policies.Policy, patAuthz *PATAuthz) error
|
||||
}
|
||||
|
||||
// Authn specifies an API that must be fulfilled by the domain service
|
||||
@@ -67,6 +70,9 @@ type Authn interface {
|
||||
// Issue issues a new Key, returning its token value alongside.
|
||||
Issue(ctx context.Context, token string, key Key) (Token, error)
|
||||
|
||||
// RevokeToken revokes the refresh token by its ID.
|
||||
RevokeToken(ctx context.Context, userID, tokenID string) error
|
||||
|
||||
// Revoke removes the Key with the provided id that is
|
||||
// issued by the user identified by the provided key.
|
||||
Revoke(ctx context.Context, token, id string) error
|
||||
@@ -79,6 +85,12 @@ type Authn interface {
|
||||
// is returned. If token is invalid, or invocation failed for some
|
||||
// other reason, non-nil error value is returned in response.
|
||||
Identify(ctx context.Context, token string) (Key, error)
|
||||
|
||||
// RetrieveJWKS retrieves public keys to validate issued tokens.
|
||||
RetrieveJWKS() []PublicKeyInfo
|
||||
|
||||
// ListUserRefreshTokens lists all active refresh token sessions for a user.
|
||||
ListUserRefreshTokens(ctx context.Context, userID string) ([]TokenInfo, error)
|
||||
}
|
||||
|
||||
// Service specifies an API that must be fulfilled by the domain service
|
||||
@@ -97,6 +109,7 @@ type service struct {
|
||||
keys KeyRepository
|
||||
pats PATSRepository
|
||||
cache Cache
|
||||
tokensCache UserActiveTokensCache
|
||||
hasher Hasher
|
||||
idProvider supermq.IDProvider
|
||||
evaluator policies.Evaluator
|
||||
@@ -108,12 +121,13 @@ type service struct {
|
||||
}
|
||||
|
||||
// New instantiates the auth service implementation.
|
||||
func New(keys KeyRepository, pats PATSRepository, cache Cache, hasher Hasher, idp supermq.IDProvider, tokenizer Tokenizer, policyEvaluator policies.Evaluator, policyService policies.Service, loginDuration, refreshDuration, invitationDuration time.Duration) Service {
|
||||
func New(keys KeyRepository, pats PATSRepository, cache Cache, tokensCache UserActiveTokensCache, hasher Hasher, idp supermq.IDProvider, tokenizer Tokenizer, policyEvaluator policies.Evaluator, policyService policies.Service, loginDuration, refreshDuration, invitationDuration time.Duration) Service {
|
||||
return &service{
|
||||
tokenizer: tokenizer,
|
||||
keys: keys,
|
||||
pats: pats,
|
||||
cache: cache,
|
||||
tokensCache: tokensCache,
|
||||
hasher: hasher,
|
||||
idProvider: idp,
|
||||
evaluator: policyEvaluator,
|
||||
@@ -140,8 +154,16 @@ func (svc service) Issue(ctx context.Context, token string, key Key) (Token, err
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) RevokeToken(ctx context.Context, userID, tokenID string) error {
|
||||
if err := svc.tokensCache.RemoveActive(ctx, userID, tokenID); err != nil {
|
||||
return errors.Wrap(errRevokeRefreshKey, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc service) Revoke(ctx context.Context, token, id string) error {
|
||||
issuerID, _, err := svc.authenticate(token)
|
||||
issuerID, _, err := svc.authenticate(ctx, token)
|
||||
if err != nil {
|
||||
return errors.Wrap(errRevoke, err)
|
||||
}
|
||||
@@ -152,7 +174,7 @@ func (svc service) Revoke(ctx context.Context, token, id string) error {
|
||||
}
|
||||
|
||||
func (svc service) RetrieveKey(ctx context.Context, token, id string) (Key, error) {
|
||||
issuerID, _, err := svc.authenticate(token)
|
||||
issuerID, _, err := svc.authenticate(ctx, token)
|
||||
if err != nil {
|
||||
return Key{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
@@ -165,7 +187,7 @@ func (svc service) RetrieveKey(ctx context.Context, token, id string) (Key, erro
|
||||
}
|
||||
|
||||
func (svc service) Identify(ctx context.Context, token string) (Key, error) {
|
||||
key, err := svc.tokenizer.Parse(token)
|
||||
key, err := svc.tokenizer.Parse(ctx, token)
|
||||
if errors.Contains(err, ErrExpiry) {
|
||||
err = svc.keys.Remove(ctx, key.Issuer, key.ID)
|
||||
return Key{}, errors.Wrap(svcerr.ErrAuthentication, errors.Wrap(ErrKeyExpired, err))
|
||||
@@ -175,6 +197,12 @@ func (svc service) Identify(ctx context.Context, token string) (Key, error) {
|
||||
}
|
||||
|
||||
switch key.Type {
|
||||
case PersonalAccessToken:
|
||||
res, err := svc.IdentifyPAT(ctx, token)
|
||||
if err != nil {
|
||||
return Key{}, err
|
||||
}
|
||||
return Key{ID: res.ID, Type: PersonalAccessToken, Subject: res.User, Role: res.Role}, nil
|
||||
case RecoveryKey, AccessKey, InvitationKey, RefreshKey:
|
||||
return key, nil
|
||||
case APIKey:
|
||||
@@ -188,23 +216,33 @@ func (svc service) Identify(ctx context.Context, token string) (Key, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) Authorize(ctx context.Context, pr policies.Policy) error {
|
||||
func (svc service) RetrieveJWKS() []PublicKeyInfo {
|
||||
keys, err := svc.tokenizer.RetrieveJWKS()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (svc service) ListUserRefreshTokens(ctx context.Context, userID string) ([]TokenInfo, error) {
|
||||
tokenInfo, err := svc.tokensCache.ListUserTokens(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errListRefreshKeys, err)
|
||||
}
|
||||
|
||||
return tokenInfo, nil
|
||||
}
|
||||
|
||||
func (svc service) Authorize(ctx context.Context, pr policies.Policy, patAuthz *PATAuthz) error {
|
||||
if patAuthz != nil {
|
||||
if err := svc.AuthorizePAT(ctx, patAuthz.UserID, patAuthz.PatID, patAuthz.EntityType, patAuthz.Domain, patAuthz.Operation, patAuthz.EntityID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := svc.PolicyValidation(pr); err != nil {
|
||||
return errors.Wrap(svcerr.ErrMalformedEntity, err)
|
||||
}
|
||||
if pr.SubjectKind == policies.TokenKind {
|
||||
key, err := svc.Identify(ctx, pr.Subject)
|
||||
if err != nil {
|
||||
return errors.Wrap(svcerr.ErrAuthentication, err)
|
||||
}
|
||||
if key.Subject == "" {
|
||||
if pr.ObjectType == policies.GroupType || pr.ObjectType == policies.ClientType || pr.ObjectType == policies.DomainType {
|
||||
return svcerr.ErrDomainAuthorization
|
||||
}
|
||||
return svcerr.ErrAuthentication
|
||||
}
|
||||
pr.Subject = key.Subject
|
||||
}
|
||||
if err := svc.checkPolicy(ctx, pr); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -213,39 +251,12 @@ func (svc service) Authorize(ctx context.Context, pr policies.Policy) error {
|
||||
}
|
||||
|
||||
func (svc service) checkPolicy(ctx context.Context, pr policies.Policy) error {
|
||||
// Domain status is required for if user sent authorization request on clients, channels, groups and domains
|
||||
if pr.SubjectType == policies.UserType && (pr.ObjectType == policies.GroupType || pr.ObjectType == policies.ClientType || pr.ObjectType == policies.DomainType) {
|
||||
domainID := pr.Domain
|
||||
if domainID == "" {
|
||||
if pr.ObjectType != policies.DomainType {
|
||||
return svcerr.ErrDomainAuthorization
|
||||
}
|
||||
domainID = pr.Object
|
||||
}
|
||||
if err := svc.checkDomain(ctx, pr.SubjectType, pr.Subject, domainID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := svc.evaluator.CheckPolicy(ctx, pr); err != nil {
|
||||
return errors.Wrap(svcerr.ErrAuthorization, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc service) checkDomain(ctx context.Context, subjectType, subject, domainID string) error {
|
||||
if err := svc.evaluator.CheckPolicy(ctx, policies.Policy{
|
||||
Subject: subject,
|
||||
SubjectType: subjectType,
|
||||
Permission: policies.MembershipPermission,
|
||||
Object: domainID,
|
||||
ObjectType: policies.DomainType,
|
||||
}); err != nil {
|
||||
return svcerr.ErrDomainAuthorization
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc service) PolicyValidation(pr policies.Policy) error {
|
||||
if pr.ObjectType == policies.PlatformType && pr.Object != policies.SuperMQObject {
|
||||
return errPlatform
|
||||
@@ -282,10 +293,20 @@ func (svc service) accessKey(ctx context.Context, key Key) (Token, error) {
|
||||
|
||||
key.ExpiresAt = time.Now().UTC().Add(svc.refreshDuration)
|
||||
key.Type = RefreshKey
|
||||
id, err := svc.idProvider.ID()
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(errIssueTmp, err)
|
||||
}
|
||||
key.ID = id
|
||||
refresh, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(errIssueTmp, err)
|
||||
}
|
||||
if key.Subject != "" && key.ExpiresAt.After(time.Now()) {
|
||||
if err := svc.tokensCache.SaveActive(ctx, key.Subject, key.ID, key.Description, key.ExpiresAt); err != nil {
|
||||
return Token{}, errors.Wrap(errSaveRefreshKey, err)
|
||||
}
|
||||
}
|
||||
|
||||
return Token{AccessToken: access, RefreshToken: refresh}, nil
|
||||
}
|
||||
@@ -308,13 +329,20 @@ func (svc service) invitationKey(ctx context.Context, key Key) (Token, error) {
|
||||
}
|
||||
|
||||
func (svc service) refreshKey(ctx context.Context, token string, key Key) (Token, error) {
|
||||
k, err := svc.tokenizer.Parse(token)
|
||||
k, err := svc.tokenizer.Parse(ctx, token)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
if k.Type != RefreshKey {
|
||||
return Token{}, errIssueUser
|
||||
}
|
||||
ok, err := svc.tokensCache.IsActive(ctx, key.ID)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
||||
}
|
||||
if !ok {
|
||||
return Token{}, ErrRevokedToken
|
||||
}
|
||||
key.ID = k.ID
|
||||
key.Type = AccessKey
|
||||
key.Subject = k.Subject
|
||||
@@ -330,7 +358,7 @@ func (svc service) refreshKey(ctx context.Context, token string, key Key) (Token
|
||||
return Token{}, errors.Wrap(errIssueTmp, err)
|
||||
}
|
||||
|
||||
key.ExpiresAt = time.Now().UTC().Add(svc.refreshDuration)
|
||||
key.ExpiresAt = k.ExpiresAt
|
||||
key.Type = RefreshKey
|
||||
refresh, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
@@ -349,7 +377,7 @@ func (svc service) checkUserRole(ctx context.Context, key Key) (err error) {
|
||||
Permission: policies.AdminPermission,
|
||||
Object: policies.SuperMQObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
}); err != nil {
|
||||
}, nil); err != nil {
|
||||
return errRoleAuth
|
||||
}
|
||||
return nil
|
||||
@@ -360,7 +388,7 @@ func (svc service) checkUserRole(ctx context.Context, key Key) (err error) {
|
||||
Permission: policies.MembershipPermission,
|
||||
Object: policies.SuperMQObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
}); err != nil {
|
||||
}, nil); err != nil {
|
||||
return errRoleAuth
|
||||
}
|
||||
return nil
|
||||
@@ -377,7 +405,7 @@ func (svc service) getUserRole(ctx context.Context, userID string) (role Role) {
|
||||
Permission: policies.AdminPermission,
|
||||
Object: policies.SuperMQObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
}); err == nil {
|
||||
}, nil); err == nil {
|
||||
rl = AdminRole
|
||||
}
|
||||
|
||||
@@ -385,7 +413,7 @@ func (svc service) getUserRole(ctx context.Context, userID string) (role Role) {
|
||||
}
|
||||
|
||||
func (svc service) userKey(ctx context.Context, token string, key Key) (Token, error) {
|
||||
id, sub, err := svc.authenticate(token)
|
||||
id, sub, err := svc.authenticate(ctx, token)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
@@ -416,8 +444,8 @@ func (svc service) userKey(ctx context.Context, token string, key Key) (Token, e
|
||||
return Token{AccessToken: tkn}, nil
|
||||
}
|
||||
|
||||
func (svc service) authenticate(token string) (string, string, error) {
|
||||
key, err := svc.tokenizer.Parse(token)
|
||||
func (svc service) authenticate(ctx context.Context, token string) (string, string, error) {
|
||||
key, err := svc.tokenizer.Parse(ctx, token)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(svcerr.ErrAuthentication, err)
|
||||
}
|
||||
@@ -702,11 +730,12 @@ func (svc service) IdentifyPAT(ctx context.Context, secret string) (PAT, error)
|
||||
return PAT{}, errors.Wrap(svcerr.ErrAuthentication, err)
|
||||
}
|
||||
role := svc.getUserRole(ctx, userID.String())
|
||||
return PAT{ID: patID.String(), User: userID.String(), Role: role}, nil
|
||||
pat := PAT{ID: patID.String(), User: userID.String(), Role: role}
|
||||
return pat, nil
|
||||
}
|
||||
|
||||
func (svc service) AuthorizePAT(ctx context.Context, userID, patID string, entityType EntityType, optionalDomainID string, operation Operation, entityID string) error {
|
||||
if err := svc.pats.CheckScope(ctx, userID, patID, entityType, optionalDomainID, operation, entityID); err != nil {
|
||||
func (svc service) AuthorizePAT(ctx context.Context, userID, patID string, entityType EntityType, domainID string, operation string, entityID string) error {
|
||||
if err := svc.pats.CheckScope(ctx, userID, patID, entityType, domainID, operation, entityID); err != nil {
|
||||
return errors.Wrap(svcerr.ErrAuthorization, err)
|
||||
}
|
||||
|
||||
@@ -723,7 +752,12 @@ func (svc service) generateSecretAndHash(userID, patID string) (string, string,
|
||||
return "", "", errors.Wrap(errFailedToParseUUID, err)
|
||||
}
|
||||
|
||||
secret := patPrefix + patSecretSeparator + encode(uID, pID) + patSecretSeparator + generateRandomString(100)
|
||||
randomPart, err := generateRandomString(100)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
secret := patPrefix + patSecretSeparator + encode(uID, pID) + patSecretSeparator + randomPart
|
||||
secretHash, err := svc.hasher.Hash(secret)
|
||||
return secret, secretHash, err
|
||||
}
|
||||
@@ -750,14 +784,19 @@ func decode(encoded string) (uuid.UUID, uuid.UUID, error) {
|
||||
return userID, patID, nil
|
||||
}
|
||||
|
||||
func generateRandomString(n int) string {
|
||||
func generateRandomString(n int) (string, error) {
|
||||
letterRunes := []rune(randStr)
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||
randBytes := make([]byte, n)
|
||||
|
||||
if _, err := rand.Read(randBytes); err != nil {
|
||||
return "", errors.Wrap(errors.New("failed to generate random string"), err)
|
||||
}
|
||||
return string(b)
|
||||
|
||||
for i := range b {
|
||||
b[i] = letterRunes[int(randBytes[i])%len(letterRunes)]
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (svc service) authnAuthzUserPAT(ctx context.Context, token, patID string) (Key, error) {
|
||||
@@ -768,6 +807,9 @@ func (svc service) authnAuthzUserPAT(ctx context.Context, token, patID string) (
|
||||
|
||||
_, err = svc.pats.Retrieve(ctx, key.Subject, patID)
|
||||
if err != nil {
|
||||
if errors.Contains(err, repoerr.ErrNotFound) {
|
||||
return Key{}, svcerr.ErrNotFound
|
||||
}
|
||||
return Key{}, errors.Wrap(svcerr.ErrAuthorization, err)
|
||||
}
|
||||
|
||||
|
||||
+782
-436
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,252 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStatusString(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
status auth.Status
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "Active",
|
||||
status: auth.ActiveStatus,
|
||||
expected: "active",
|
||||
},
|
||||
{
|
||||
desc: "Revoked",
|
||||
status: auth.RevokedStatus,
|
||||
expected: "revoked",
|
||||
},
|
||||
{
|
||||
desc: "Expired",
|
||||
status: auth.ExpiredStatus,
|
||||
expected: "expired",
|
||||
},
|
||||
{
|
||||
desc: "All",
|
||||
status: auth.AllStatus,
|
||||
expected: "all",
|
||||
},
|
||||
{
|
||||
desc: "Unknown",
|
||||
status: auth.Status(100),
|
||||
expected: "unknown",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := tc.status.String()
|
||||
assert.Equal(t, tc.expected, got, "String() = %v, expected %v", got, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToStatus(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
status string
|
||||
expected auth.Status
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "Active",
|
||||
status: "active",
|
||||
expected: auth.ActiveStatus,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Empty string defaults to Active",
|
||||
status: "",
|
||||
expected: auth.ActiveStatus,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Revoked",
|
||||
status: "revoked",
|
||||
expected: auth.RevokedStatus,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Expired",
|
||||
status: "expired",
|
||||
expected: auth.ExpiredStatus,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "All",
|
||||
status: "all",
|
||||
expected: auth.AllStatus,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Unknown",
|
||||
status: "unknown",
|
||||
expected: auth.Status(0),
|
||||
err: svcerr.ErrInvalidStatus,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got, err := auth.ToStatus(tc.status)
|
||||
assert.Equal(t, tc.err, err, "ToStatus() error = %v, expected %v", err, tc.err)
|
||||
assert.Equal(t, tc.expected, got, "ToStatus() = %v, expected %v", got, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusMarshalJSON(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
expected []byte
|
||||
status auth.Status
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "Active",
|
||||
expected: []byte(`"active"`),
|
||||
status: auth.ActiveStatus,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Revoked",
|
||||
expected: []byte(`"revoked"`),
|
||||
status: auth.RevokedStatus,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Expired",
|
||||
expected: []byte(`"expired"`),
|
||||
status: auth.ExpiredStatus,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "All",
|
||||
expected: []byte(`"all"`),
|
||||
status: auth.AllStatus,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Unknown",
|
||||
expected: []byte(`"unknown"`),
|
||||
status: auth.Status(100),
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got, err := tc.status.MarshalJSON()
|
||||
assert.Equal(t, tc.err, err, "MarshalJSON() error = %v, expected %v", err, tc.err)
|
||||
assert.Equal(t, tc.expected, got, "MarshalJSON() = %v, expected %v", got, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusUnmarshalJSON(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
expected auth.Status
|
||||
status []byte
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "Active",
|
||||
expected: auth.ActiveStatus,
|
||||
status: []byte(`"active"`),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Revoked",
|
||||
expected: auth.RevokedStatus,
|
||||
status: []byte(`"revoked"`),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Expired",
|
||||
expected: auth.ExpiredStatus,
|
||||
status: []byte(`"expired"`),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "All",
|
||||
expected: auth.AllStatus,
|
||||
status: []byte(`"all"`),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Unknown",
|
||||
expected: auth.Status(0),
|
||||
status: []byte(`"unknown"`),
|
||||
err: svcerr.ErrInvalidStatus,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
var s auth.Status
|
||||
err := s.UnmarshalJSON(tc.status)
|
||||
assert.Equal(t, tc.err, err, "UnmarshalJSON() error = %v, expected %v", err, tc.err)
|
||||
assert.Equal(t, tc.expected, s, "UnmarshalJSON() = %v, expected %v", s, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPATMarshalJSON(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
pat auth.PAT
|
||||
expected string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "Active PAT",
|
||||
pat: auth.PAT{
|
||||
ID: "test-id",
|
||||
Name: "test-pat",
|
||||
Status: auth.ActiveStatus,
|
||||
},
|
||||
expected: `"status":"active"`,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Revoked PAT",
|
||||
pat: auth.PAT{
|
||||
ID: "test-id",
|
||||
Name: "test-pat",
|
||||
Status: auth.RevokedStatus,
|
||||
},
|
||||
expected: `"status":"revoked"`,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Expired PAT",
|
||||
pat: auth.PAT{
|
||||
ID: "test-id",
|
||||
Name: "test-pat",
|
||||
Status: auth.ExpiredStatus,
|
||||
},
|
||||
expected: `"status":"expired"`,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got, err := tc.pat.MarshalJSON()
|
||||
assert.Equal(t, tc.err, err, "MarshalJSON() error = %v, expected %v", err, tc.err)
|
||||
assert.Contains(t, string(got), tc.expected, "MarshalJSON() should contain %v", tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth
|
||||
|
||||
// Tokenizer specifies API for encoding and decoding between string and Key.
|
||||
type Tokenizer interface {
|
||||
// Issue converts API Key to its string representation.
|
||||
Issue(key Key) (token string, err error)
|
||||
|
||||
// Parse extracts API Key data from string token.
|
||||
Parse(token string) (key Key, err error)
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
# Asymmetric Tokenizer
|
||||
|
||||
EdDSA (Ed25519) tokenizer with support for zero-downtime key rotation.
|
||||
|
||||
## Features
|
||||
|
||||
- **Single-key mode** - Simple setup with one active key
|
||||
- **Two-key mode** - Active + retiring keys for _zero-downtime rotation_
|
||||
- **JWKS endpoint** - Publishes all valid public keys for token verification
|
||||
|
||||
## Configuration
|
||||
|
||||
The tokenizer uses environment variables to specify key file paths:
|
||||
|
||||
| Environment Variable | Required | Description |
|
||||
| --------------------------------- | -------- | ------------------------------------------------ |
|
||||
| `SMQ_AUTH_KEYS_ACTIVE_KEY_PATH` | Yes | Path to active private key file |
|
||||
| `SMQ_AUTH_KEYS_RETIRING_KEY_PATH` | No | Path to retiring private key file (for rotation) |
|
||||
|
||||
Please note that key names are used as **key IDs (kid)**.
|
||||
|
||||
### Single-Key Mode
|
||||
|
||||
Set only the active key path:
|
||||
|
||||
```bash
|
||||
export SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/private.key"
|
||||
```
|
||||
|
||||
The tokenizer will:
|
||||
|
||||
- Issue new tokens signed with the active key
|
||||
- Verify tokens using the active key
|
||||
- Return one public key in JWKS endpoint
|
||||
|
||||
### Two-Key Mode (Key Rotation)
|
||||
|
||||
Set both active and retiring key paths:
|
||||
|
||||
```bash
|
||||
export SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/active.key"
|
||||
export SMQ_AUTH_KEYS_RETIRING_KEY_PATH="./keys/retiring.key"
|
||||
```
|
||||
|
||||
The tokenizer will:
|
||||
|
||||
- Issue new tokens signed with the active key
|
||||
- Verify tokens using both active and retiring keys
|
||||
- Return both public keys in JWKS endpoint
|
||||
|
||||
## Key Rotation Process
|
||||
|
||||
Zero-downtime key rotation in 3 simple steps:
|
||||
|
||||
### 1. Generate New Key
|
||||
|
||||
```bash
|
||||
openssl genpkey -algorithm Ed25519 -out keys/new.key
|
||||
```
|
||||
|
||||
### 2. Update Environment & Restart
|
||||
|
||||
Move the current active key to retiring position and set the new key as active:
|
||||
|
||||
```bash
|
||||
# Before rotation
|
||||
SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/current.key"
|
||||
SMQ_AUTH_KEYS_RETIRING_KEY_PATH="" # No retiring key
|
||||
|
||||
# During rotation (both keys active for grace period)
|
||||
SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/new.key"
|
||||
SMQ_AUTH_KEYS_RETIRING_KEY_PATH="./keys/current.key"
|
||||
|
||||
# After rotation (restart service with new config)
|
||||
docker-compose restart auth
|
||||
```
|
||||
|
||||
During the grace period, tokens signed with either key remain valid.
|
||||
|
||||
### 3. Clean Up After Grace Period
|
||||
|
||||
After the grace period expires (typically 7-30 days), remove the retiring key:
|
||||
|
||||
```bash
|
||||
# Remove retiring key configuration
|
||||
SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/new.key"
|
||||
SMQ_AUTH_KEYS_RETIRING_KEY_PATH="" # Remove retiring key
|
||||
|
||||
# Restart service
|
||||
docker-compose restart auth
|
||||
|
||||
# Delete old key file
|
||||
rm keys/current.key
|
||||
```
|
||||
|
||||
## Grace Period Recommendations
|
||||
|
||||
**Recommended:** 168 hours (7 days)
|
||||
**Minimum:** 24 hours
|
||||
**Maximum:** 720 hours (30 days)
|
||||
|
||||
The grace period should be longer than your longest-lived access token duration.
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
- Store private keys with `0600` permissions
|
||||
- Use cryptographically secure key generation:
|
||||
|
||||
```bash
|
||||
openssl genpkey -algorithm Ed25519 -out private.key
|
||||
chmod 600 private.key
|
||||
```
|
||||
|
||||
- Rotate keys regularly:
|
||||
- Standard environments: every 90 days
|
||||
- High-security environments: every 30 days
|
||||
- Never commit keys to version control
|
||||
- Use secrets management in production (HashiCorp Vault, AWS Secrets Manager, etc.)
|
||||
|
||||
## Example: Complete Rotation
|
||||
|
||||
```bash
|
||||
# Day 0: Normal operation
|
||||
export SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/key-2024.pem"
|
||||
export SMQ_AUTH_KEYS_RETIRING_KEY_PATH=""
|
||||
|
||||
# Day 1: Start rotation - generate new key
|
||||
openssl genpkey -algorithm Ed25519 -out ./keys/key-2025.pem
|
||||
chmod 600 ./keys/key-2025.pem
|
||||
|
||||
# Day 1: Update config and restart
|
||||
export SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/key-2025.pem"
|
||||
export SMQ_AUTH_KEYS_RETIRING_KEY_PATH="./keys/key-2024.pem"
|
||||
docker-compose restart auth
|
||||
|
||||
# Day 8: Grace period expired - remove old key
|
||||
export SMQ_AUTH_KEYS_RETIRING_KEY_PATH=""
|
||||
docker-compose restart auth
|
||||
rm ./keys/key-2024.pem
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Active key not found
|
||||
|
||||
```bash
|
||||
Error: active key file not found: ./keys/active.key
|
||||
```
|
||||
|
||||
**Solution:** Ensure the file exists and path is correct. Verify `SMQ_AUTH_KEYS_ACTIVE_KEY_PATH` environment variable.
|
||||
|
||||
### Retiring key warning
|
||||
|
||||
If the retiring key path is set but the file is missing or invalid, the tokenizer logs a warning but continues with only the active key:
|
||||
|
||||
```bash
|
||||
WARN: failed to load retiring key, continuing without it
|
||||
```
|
||||
|
||||
This is by design - a missing retiring key won't prevent startup.
|
||||
|
||||
### Invalid key format
|
||||
|
||||
```bash
|
||||
Error: failed to parse private key
|
||||
```
|
||||
|
||||
**Solution:** Ensure you're using Ed25519 keys in PEM format (PKCS8).
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package asymmetric_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/auth/tokenizer/asymmetric"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type incrementingIDProvider struct {
|
||||
counter int
|
||||
}
|
||||
|
||||
func (p *incrementingIDProvider) ID() (string, error) {
|
||||
p.counter++
|
||||
return fmt.Sprintf("key-id-%d", p.counter), nil
|
||||
}
|
||||
|
||||
func TestTwoKeyRotation(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
_, activePriv, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, retiringPriv, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
activeKeyPath := filepath.Join(tmpDir, "active.key")
|
||||
retiringKeyPath := filepath.Join(tmpDir, "retiring.key")
|
||||
|
||||
saveKey(t, activePriv, activeKeyPath)
|
||||
saveKey(t, retiringPriv, retiringKeyPath)
|
||||
|
||||
idProvider := &incrementingIDProvider{}
|
||||
tokenizer, err := asymmetric.NewTokenizer(activeKeyPath, retiringKeyPath, idProvider, newTestLogger())
|
||||
require.NoError(t, err)
|
||||
|
||||
testKey := auth.Key{
|
||||
ID: "test-key",
|
||||
Type: auth.AccessKey,
|
||||
Subject: "user-123",
|
||||
Role: auth.UserRole,
|
||||
IssuedAt: time.Now().UTC(),
|
||||
ExpiresAt: time.Now().Add(1 * time.Hour).UTC(),
|
||||
Verified: true,
|
||||
}
|
||||
|
||||
token, err := tokenizer.Issue(testKey)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
|
||||
verified, err := tokenizer.Parse(context.Background(), token)
|
||||
require.NoError(t, err, "Should work with active token")
|
||||
assert.Equal(t, testKey.Subject, verified.Subject)
|
||||
|
||||
publicKeys, err := tokenizer.RetrieveJWKS()
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, publicKeys, 2, "Should return both active and retiring keys")
|
||||
|
||||
keyIDs := make(map[string]bool)
|
||||
for _, pk := range publicKeys {
|
||||
keyIDs[pk.KeyID] = true
|
||||
}
|
||||
assert.Len(t, keyIDs, 2, "Both keys should have unique IDs")
|
||||
}
|
||||
|
||||
func TestSingleKeyMode(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyPath := filepath.Join(tmpDir, "single.key")
|
||||
saveKey(t, privateKey, keyPath)
|
||||
|
||||
idProvider := &mockIDProvider{id: "single-id"}
|
||||
tokenizer, err := asymmetric.NewTokenizer(keyPath, "", idProvider, newTestLogger())
|
||||
require.NoError(t, err)
|
||||
|
||||
testKey := auth.Key{
|
||||
ID: "test",
|
||||
Type: auth.AccessKey,
|
||||
Subject: "user",
|
||||
Role: auth.UserRole,
|
||||
IssuedAt: time.Now().UTC(),
|
||||
ExpiresAt: time.Now().Add(1 * time.Hour).UTC(),
|
||||
}
|
||||
|
||||
token, err := tokenizer.Issue(testKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = tokenizer.Parse(context.Background(), token)
|
||||
require.NoError(t, err)
|
||||
|
||||
publicKeys, err := tokenizer.RetrieveJWKS()
|
||||
require.NoError(t, err, "Should return one active key")
|
||||
assert.Len(t, publicKeys, 1, "Should return only the active key")
|
||||
}
|
||||
|
||||
func TestMissingRetiringKey(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
activeKeyPath := filepath.Join(tmpDir, "active.key")
|
||||
saveKey(t, privateKey, activeKeyPath)
|
||||
|
||||
retiringKeyPath := filepath.Join(tmpDir, "nonexistent.key")
|
||||
|
||||
idProvider := &mockIDProvider{id: "test-id"}
|
||||
tokenizer, err := asymmetric.NewTokenizer(activeKeyPath, retiringKeyPath, idProvider, newTestLogger())
|
||||
require.NoError(t, err, "Should succeed even if retiring key is missing")
|
||||
|
||||
testKey := auth.Key{
|
||||
ID: "test",
|
||||
Type: auth.AccessKey,
|
||||
Subject: "user",
|
||||
Role: auth.UserRole,
|
||||
IssuedAt: time.Now().UTC(),
|
||||
ExpiresAt: time.Now().Add(1 * time.Hour).UTC(),
|
||||
}
|
||||
|
||||
token, err := tokenizer.Issue(testKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = tokenizer.Parse(context.Background(), token)
|
||||
require.NoError(t, err)
|
||||
|
||||
publicKeys, err := tokenizer.RetrieveJWKS()
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, publicKeys, 1, "Should return only active key when retiring key is missing")
|
||||
}
|
||||
|
||||
func saveKey(t *testing.T, privateKey ed25519.PrivateKey, path string) {
|
||||
pkcs8Key, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
pemBlock := &pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: pkcs8Key,
|
||||
}
|
||||
|
||||
err = os.WriteFile(path, pem.EncodeToMemory(pemBlock), 0o600)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user