Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a708965e4 | ||
|
|
bb450e5d8c | ||
|
|
206d2ab14b | ||
|
|
78b4730687 | ||
|
|
3941ff4950 | ||
|
|
09766ebe8b | ||
|
|
e0d1ce3b31 | ||
|
|
5100d53c76 | ||
|
|
01f30cfd2b | ||
|
|
9d1b981644 | ||
|
|
3347d598a4 | ||
|
|
6bdf6e1cd3 | ||
|
|
f5e9eb52a7 | ||
|
|
85d29c1196 | ||
|
|
37d9282f7e | ||
|
|
5c2477eded | ||
|
|
bfd5fd4c16 | ||
|
|
dfc666ebb0 | ||
|
|
75f87fd618 | ||
|
|
752f80ea96 | ||
|
|
08269c034b | ||
|
|
89fa8928dc | ||
|
|
6fccd2010c | ||
|
|
4296334275 | ||
|
|
daa836e97d | ||
|
|
f9a89b64be | ||
|
|
775d25cb59 | ||
|
|
ef992cb9db | ||
|
|
c3b8a3f1ce | ||
|
|
45512115c5 | ||
|
|
fe0abd62c8 | ||
|
|
a54cf45a4b | ||
|
|
f9a5ef7085 | ||
|
|
200e7cf963 | ||
|
|
110f6206f9 | ||
|
|
c53d80792c | ||
|
|
ebc0c3bb8e | ||
|
|
cb59762fc9 | ||
|
|
a82466cb27 | ||
|
|
29eab978f7 | ||
|
|
f3166426b6 | ||
|
|
3171c53393
|
||
|
|
7af8e00b4c | ||
|
|
0c0f321b33
|
||
|
|
87805f1552 | ||
|
|
74ee30bccc | ||
|
|
646e4f31c4 | ||
|
|
881fac379f | ||
|
|
2c2bd2798f | ||
|
|
204b108ece | ||
|
|
fa795a58cf | ||
|
|
60650b9c04 | ||
|
|
60d9eea9c9
|
||
|
|
dd571f8d85 | ||
|
|
1f822b2957 | ||
|
|
5b49b36a32 | ||
|
|
a06bae1da9 | ||
|
|
90084cc3a4 | ||
|
|
7825fa17a6 | ||
|
|
73f854ba06
|
||
|
|
5b3527439f | ||
|
|
681afe2626
|
||
|
|
2e1eb327e0 | ||
|
|
622d907e03 | ||
|
|
4b38ce4f0f | ||
|
|
b82e15c49b | ||
|
|
f0ea0d7d26 | ||
|
|
26de10c2b9 | ||
|
|
3865b1b5f7 | ||
|
|
cd84a51f91
|
||
|
|
eab39dc818 | ||
|
|
524cad9180 | ||
|
|
b339804535 | ||
|
|
d2ed9ed12a | ||
|
|
f521fb29c9 | ||
|
|
123dd2a4c2 | ||
|
|
c5943b41ec | ||
|
|
5644b061c0 | ||
|
|
8330219579 | ||
|
|
425013f115 | ||
|
|
7f3b78b000 | ||
|
|
ab8a9809d5 | ||
|
|
9ef9084ffa | ||
|
|
e48c5b3bb9 | ||
|
|
4421bcfbeb | ||
|
|
18748ceaad | ||
|
|
14d32b3b51 | ||
|
|
d571e92a0b | ||
|
|
d355edd642 | ||
|
|
abd34b334a | ||
|
|
da73907913 | ||
|
|
becde50685 | ||
|
|
a84a14a0d3 | ||
|
|
707e26b420 | ||
|
|
ffa2eca348 | ||
|
|
76fc782c96 | ||
|
|
4d3147c65c | ||
|
|
6643990160 | ||
|
|
20d69f9c19 | ||
|
|
d327307539 | ||
|
|
5dce8a0f2b | ||
|
|
6f6272cb02 | ||
|
|
7eddad8d4b | ||
|
|
1fd3ae5123
|
||
|
|
247c04a710 | ||
|
|
37f6c63025 | ||
|
|
215a9ed976 | ||
|
|
006ebd101e | ||
|
|
600653518c | ||
|
|
c95cd1c80a | ||
|
|
881a6f0e3c | ||
|
|
84939fa04b | ||
|
|
0821cbb6eb | ||
|
|
0cb8363ab1 | ||
|
|
8a440bd28c | ||
|
|
13461b43e3 | ||
|
|
49100894e9 | ||
|
|
7818f97a7c | ||
|
|
66c429bde1 | ||
|
|
585fa5dfa4 | ||
|
|
82cca9b09c | ||
|
|
bcbddac138 | ||
|
|
e62257d933 | ||
|
|
f45918bac8 | ||
|
|
77ac58202a | ||
|
|
257a97f191 | ||
|
|
32e8127273 | ||
|
|
223f65396d | ||
|
|
01d4a4bc9f | ||
|
|
1038cff1d9 | ||
|
|
f961bf91f7 | ||
|
|
721cb1be91 | ||
|
|
2f570a0638 | ||
|
|
ee52c50e76 | ||
|
|
675ac0338f | ||
|
|
f708a18960 | ||
|
|
a8ba5635e3 | ||
|
|
4efc6572ee
|
||
|
|
05c59fa4a5 | ||
|
|
52ea18c616 | ||
|
|
9dd2b5aaa8 | ||
|
|
af93e418f4
|
||
|
|
46e26efe67 | ||
|
|
f65e3868e3 | ||
|
|
003407ecaa | ||
|
|
50849e8682 | ||
|
|
3e8b3d00e5 | ||
|
|
a5ac3c3500
|
||
|
|
2502c85497 | ||
|
|
ed60a0ccb3 | ||
|
|
047e09823d | ||
|
|
a0326a1594 | ||
|
|
fe5a401bfc | ||
|
|
c12617975f | ||
|
|
64a3a6e533 | ||
|
|
c46753598d | ||
|
|
30637a081d | ||
|
|
44bebba3c1 | ||
|
|
830e1307bd | ||
|
|
f094014c90 | ||
|
|
e9ebb2206c | ||
|
|
b6ec9feeae
|
||
|
|
a1c9ace5ec | ||
|
|
250d017f93 | ||
|
|
f48319ffb0 | ||
|
|
10c3448ec3 | ||
|
|
3e0bb48af7 | ||
|
|
c9169d5cbc | ||
|
|
e8da7bb3e9 | ||
|
|
1755ed37dd | ||
|
|
d2faf4bd88 | ||
|
|
96001183fb | ||
|
|
f652c92328 | ||
|
|
4152e9830f | ||
|
|
caafda1f7c | ||
|
|
e2041fe618 | ||
|
|
a05c89475f | ||
|
|
a028c38a4d | ||
|
|
982fb0bb09 | ||
|
|
ce344d29bf | ||
|
|
4e29b77504 | ||
|
|
d2492e98fa | ||
|
|
35b8148f7f | ||
|
|
3a9bd8dffe | ||
|
|
c8b6f33464 | ||
|
|
ff672b7a81 | ||
|
|
dba9093ed4 | ||
|
|
e02917ba5d | ||
|
|
05425c1d5f | ||
|
|
b42a60844e | ||
|
|
72b66efc07 | ||
|
|
fb45f1eccc | ||
|
|
b10691a7ad | ||
|
|
0677bb1e60 | ||
|
|
3bc038ba17 | ||
|
|
1c870c3d4e | ||
|
|
8b8bf53790
|
||
|
|
1f20ead820 | ||
|
|
6b567fce3c | ||
|
|
08000f5e9f | ||
|
|
761bd0d75b | ||
|
|
2f7cf19696 | ||
|
|
06a017f5a5 | ||
|
|
0a5e432071 | ||
|
|
957d9229b1 | ||
|
|
681804c5e4 | ||
|
|
50a6071eec
|
||
|
|
c7ceee0a14 | ||
|
|
09f0769a3e | ||
|
|
ae9ac8b449 | ||
|
|
7f251668b2 | ||
|
|
114883aef4 | ||
|
|
6c0dbaf903 | ||
|
|
b0a1757e38
|
||
|
|
d6736b0187 | ||
|
|
668e0dacfb | ||
|
|
1c2e98a973 | ||
|
|
026fe0ab3f | ||
|
|
6b15aac9f3 | ||
|
|
001f23eab5 | ||
|
|
e3c303b162 | ||
|
|
61204541a4 | ||
|
|
fbd579a4f6 | ||
|
|
7a1a0b5941 | ||
|
|
d6db4b664c | ||
|
|
44a711baa1 | ||
|
|
63ae255179 | ||
|
|
24f3184ccc | ||
|
|
b6f20b7ae0
|
||
|
|
c009f3d87c | ||
|
|
e197f2c7da | ||
|
|
2655fcb211 | ||
|
|
9fcbdd259b | ||
|
|
ba73d36b91 | ||
|
|
739440046b | ||
|
|
2243597853 | ||
|
|
e9e684949c | ||
|
|
a6426f5973 | ||
|
|
e29f5b331b | ||
|
|
e1b2e5a5c1 | ||
|
|
7371d54831 | ||
|
|
badfe89281 | ||
|
|
17dd541cc6 | ||
|
|
4ad0fb4f86 | ||
|
|
a53df01028 | ||
|
|
622179a6b1 | ||
|
|
b6b3084ae7 | ||
|
|
f6a0f478c0 | ||
|
|
5cce412515 | ||
|
|
f9f881614b | ||
|
|
a76276787f
|
||
|
|
619bccdcf0 | ||
|
|
919589a397 | ||
|
|
bcf67ea71e | ||
|
|
674117e8ff | ||
|
|
1307fcd993 | ||
|
|
aeb92016c8 | ||
|
|
53ddd9413a | ||
|
|
791703e632 | ||
|
|
abec3883c7 | ||
|
|
c78e07cdca | ||
|
|
0b8ef99e8b | ||
|
|
82505437e1 | ||
|
|
510790dab3 | ||
|
|
3bbab5a037 | ||
|
|
53fbe8d8b1 | ||
|
|
660a58a94c | ||
|
|
0a4c0c7920 | ||
|
|
511fce32e1 | ||
|
|
7477dd929e
|
||
|
|
3b2528e447 | ||
|
|
628d5f68d3 | ||
|
|
b810402cea | ||
|
|
c18735eb92 | ||
|
|
b362af8bbe | ||
|
|
2ad082150d | ||
|
|
a2a06a49c9 | ||
|
|
c7765a7838 | ||
|
|
e37c082d0b | ||
|
|
fb4f0ebe44 | ||
|
|
adc0dbed67 | ||
|
|
43ef782b72 | ||
|
|
7c485ad14d | ||
|
|
70e6001c4a | ||
|
|
002ae561cb | ||
|
|
3c7787a0eb
|
||
|
|
521df4ec4c
|
||
|
|
e347883c05 | ||
|
|
078d6fee28 | ||
|
|
79bd756ddd | ||
|
|
ae9e79eec5 | ||
|
|
cef1c9d166 | ||
|
|
3d4341dbb3 | ||
|
|
968ee335dd | ||
|
|
6b1ada6155 | ||
|
|
c4c70ab5f3 | ||
|
|
d68ed4e55b | ||
|
|
1e66b292fa | ||
|
|
81f19bafd9 | ||
|
|
b3f4330aa4
|
||
|
|
f876c2a964 | ||
|
|
01006d5713 | ||
|
|
62ee90cb93 | ||
|
|
54e8bbc9a3 | ||
|
|
d8fb015d1f | ||
|
|
3c53f6d19e | ||
|
|
cce56c2fd4 | ||
|
|
8088b2e9c0 | ||
|
|
f1ebe5f920 | ||
|
|
e474981df4 | ||
|
|
3519a15cfd | ||
|
|
ede05d8335 | ||
|
|
2e151d003f | ||
|
|
2347345cc9 | ||
|
|
7543e07add | ||
|
|
1823219b05 | ||
|
|
43b988801b | ||
|
|
1c3e889a6e | ||
|
|
72d27535f5 | ||
|
|
950470c49a | ||
|
|
71d8e85610 | ||
|
|
da4ad80ab5 | ||
|
|
f030c9ec07 | ||
|
|
d72e3c3422 | ||
|
|
afa66d0a6d | ||
|
|
d64fabb1a4 | ||
|
|
529069439c | ||
|
|
cf05e91286 | ||
|
|
fd2c013368 | ||
|
|
a9123af4ff | ||
|
|
8d9920c108 | ||
|
|
50a4930038 | ||
|
|
7c2d1e75b2 | ||
|
|
9a3a3c9de7 | ||
|
|
fd862c499b | ||
|
|
e02b3c92f3 | ||
|
|
49af247019 | ||
|
|
46e1bd284c | ||
|
|
16e5ad4f23 | ||
|
|
f5c18e4515 | ||
|
|
e3c79092c2 | ||
|
|
7815c85c92 | ||
|
|
de664d7a05 | ||
|
|
67963fcdc1 | ||
|
|
d3a504ca68 | ||
|
|
233197999f | ||
|
|
1ea2469c77 | ||
|
|
775553c2d5 | ||
|
|
4203f4474f | ||
|
|
cc7de2e5a1 | ||
|
|
f14aec80d9 | ||
|
|
cbd358ba85 | ||
|
|
df845a7b4b | ||
|
|
d8f6785032 | ||
|
|
ff779d9456 | ||
|
|
eba7784e59 | ||
|
|
d022eb8ebd | ||
|
|
9c802b1239 | ||
|
|
2e3bd451df | ||
|
|
0608960a02 | ||
|
|
bbbfb99dd9 | ||
|
|
eef42d4b32 | ||
|
|
d1a9414390 | ||
|
|
dee3bb5d60 | ||
|
|
dcbdef35cf | ||
|
|
1794fcd2ad | ||
|
|
abfeb751b2 | ||
|
|
51f0aaf0ca | ||
|
|
8604a3568a | ||
|
|
fee49889cf | ||
|
|
7e70411553 | ||
|
|
610edd6576 | ||
|
|
2f232e6850 | ||
|
|
be4409a745 | ||
|
|
83b513ebf8 | ||
|
|
51e8d43795 | ||
|
|
e816866e75 | ||
|
|
ad1bf3cbfa | ||
|
|
f4e3891f62 | ||
|
|
cf73d6c399 | ||
|
|
74e81114de | ||
|
|
2299434912 | ||
|
|
3c6ab4bfde | ||
|
|
a305db2206 | ||
|
|
6e8b78c6e2 | ||
|
|
4578694015 | ||
|
|
dbc4412b9b | ||
|
|
f151464330 | ||
|
|
bcfa15e4a2 | ||
|
|
41c7e4a3ef | ||
|
|
5cdd3bd244 | ||
|
|
398d267ee5 | ||
|
|
26f8e762c7 | ||
|
|
e395518de2 | ||
|
|
60795d413d | ||
|
|
a2164c4beb | ||
|
|
90e028b173 | ||
|
|
6ce176b927 | ||
|
|
48a063eba2 | ||
|
|
5634c26039 | ||
|
|
429f45935a | ||
|
|
3c36225665 | ||
|
|
f6417140cb | ||
|
|
fb0273fe3c |
No files matched your search
@@ -0,0 +1,81 @@
|
|||||||
|
name: authz_client
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: actions/setup-go@v7
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: Run tests
|
||||||
|
run: go test -race -coverprofile=coverage.txt ./...
|
||||||
|
|
||||||
|
- name: Check coverage
|
||||||
|
id: coverage
|
||||||
|
run: |
|
||||||
|
go install github.com/vladopajic/go-test-coverage/v2@latest
|
||||||
|
go-test-coverage --config ./.testcoverage.yml --github-action-output
|
||||||
|
- name: Download baseline coverage
|
||||||
|
if: gitea.event_name == 'pull_request'
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: coverage-baseline
|
||||||
|
path: ./baseline
|
||||||
|
continue-on-error: true
|
||||||
|
- name: Compare coverage
|
||||||
|
if: gitea.event_name == 'pull_request'
|
||||||
|
run: |
|
||||||
|
CURRENT="${{ steps.coverage.outputs.total-coverage }}"
|
||||||
|
if [ -f ./baseline/coverage.txt ]; then
|
||||||
|
BASE=$(cat ./baseline/coverage.txt)
|
||||||
|
echo "Base coverage: ${BASE}%"
|
||||||
|
echo "Current coverage: ${CURRENT}%"
|
||||||
|
if [ "$(echo "$CURRENT < $BASE" | bc -l)" -eq 1 ]; then
|
||||||
|
echo "::error::Coverage decreased from ${BASE}% to ${CURRENT}%"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Coverage maintained or improved: ${BASE}% -> ${CURRENT}%"
|
||||||
|
else
|
||||||
|
echo "No baseline coverage found, skipping comparison"
|
||||||
|
echo "Current coverage: ${CURRENT}%"
|
||||||
|
fi
|
||||||
|
- name: Save coverage baseline
|
||||||
|
if: gitea.ref == 'refs/heads/main'
|
||||||
|
run: echo "${{ steps.coverage.outputs.total-coverage }}" > coverage.txt
|
||||||
|
- name: Upload coverage baseline
|
||||||
|
if: gitea.ref == 'refs/heads/main'
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: coverage-baseline
|
||||||
|
path: coverage.txt
|
||||||
|
retention-days: 90
|
||||||
|
- name: Post coverage comment
|
||||||
|
if: gitea.event_name == 'pull_request'
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
GITEA_URL: ${{ gitea.server_url }}
|
||||||
|
run: |
|
||||||
|
COVERAGE="${{ steps.coverage.outputs.total-coverage }}"
|
||||||
|
curl -X POST "${GITEA_URL}/api/v1/repos/${{ gitea.repository }}/issues/${{ gitea.event.pull_request.number }}/comments" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"body\": \"## Coverage Report\n\nTotal coverage: **${COVERAGE}%**\"}"
|
||||||
|
|
||||||
|
vulnerabilities:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: actions/setup-go@v7
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: Check vulnerabilities
|
||||||
|
run: |
|
||||||
|
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
govulncheck ./...
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: pre-commit
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-commit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
SKIP: no-commit-to-branch
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: actions/setup-go@v7
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
- uses: actions/setup-python@v7
|
||||||
|
with:
|
||||||
|
python-version: '3.14'
|
||||||
|
- name: Install goimports
|
||||||
|
run: go install golang.org/x/tools/cmd/goimports@latest
|
||||||
|
- uses: pre-commit/action@v3.0.1
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
uses: unboundsoftware/shared-workflows/.gitea/workflows/Release.yml@main
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
.idea
|
.idea
|
||||||
|
.claude
|
||||||
/release
|
/release
|
||||||
|
coverage.txt
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
variables:
|
|
||||||
GOCACHE: "${CI_PROJECT_DIR}/_go/cache"
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- mkdir -p ${CI_PROJECT_DIR}/_go/{pkg,bin,cache}
|
|
||||||
- rm -rf /go/pkg || true
|
|
||||||
- mkdir -p /go
|
|
||||||
- ln -s ${CI_PROJECT_DIR}/_go/pkg /go/pkg
|
|
||||||
- ln -s ${CI_PROJECT_DIR}/_go/bin /go/bin
|
|
||||||
|
|
||||||
cache:
|
|
||||||
key: "$CI_COMMIT_REF_NAME"
|
|
||||||
paths:
|
|
||||||
- _go
|
|
||||||
untracked: true
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- deps
|
|
||||||
- test
|
|
||||||
|
|
||||||
deps:
|
|
||||||
stage: deps
|
|
||||||
image: golang:1.14
|
|
||||||
script:
|
|
||||||
- go mod download
|
|
||||||
|
|
||||||
test:
|
|
||||||
stage: test
|
|
||||||
dependencies:
|
|
||||||
- deps
|
|
||||||
image: golang:1.14
|
|
||||||
script:
|
|
||||||
- go fmt $(go list ./...)
|
|
||||||
- go vet $(go list ./...)
|
|
||||||
- unset "${!CI@}"
|
|
||||||
- CGO_ENABLED=1 go test -p 1 -mod=readonly -race -coverprofile=.testCoverage.txt -covermode=atomic -coverpkg=$(go list ./... | tr '\n' , | sed 's/,$//') ./...
|
|
||||||
- go tool cover -html=.testCoverage.txt -o coverage.html
|
|
||||||
- go tool cover -func=.testCoverage.txt
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- coverage.html
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
version: "2"
|
||||||
|
run:
|
||||||
|
allow-parallel-runners: true
|
||||||
|
linters:
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
formatters:
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# See https://pre-commit.com for more information
|
||||||
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v6.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
args:
|
||||||
|
- --allow-multiple-documents
|
||||||
|
- id: check-added-large-files
|
||||||
|
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
|
||||||
|
rev: v9.27.0
|
||||||
|
hooks:
|
||||||
|
- id: commitlint
|
||||||
|
stages: [ commit-msg ]
|
||||||
|
additional_dependencies: [ '@commitlint/config-conventional' ]
|
||||||
|
- repo: https://github.com/dnephin/pre-commit-golang
|
||||||
|
rev: v0.5.1
|
||||||
|
hooks:
|
||||||
|
- id: go-mod-tidy
|
||||||
|
- id: go-imports
|
||||||
|
args:
|
||||||
|
- -local
|
||||||
|
- gitea.unbound.se/shiny/authz_client
|
||||||
|
- repo: https://github.com/lietu/go-pre-commit
|
||||||
|
rev: v1.0.0
|
||||||
|
hooks:
|
||||||
|
- id: go-test
|
||||||
|
- id: gofumpt
|
||||||
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
|
rev: v2.14.0
|
||||||
|
hooks:
|
||||||
|
- id: golangci-lint-full
|
||||||
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
|
rev: v8.30.1
|
||||||
|
hooks:
|
||||||
|
- id: gitleaks
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Coverage configuration for go-test-coverage
|
||||||
|
# https://github.com/vladopajic/go-test-coverage
|
||||||
|
|
||||||
|
profile: coverage.txt
|
||||||
|
|
||||||
|
threshold:
|
||||||
|
file: 0
|
||||||
|
package: 0
|
||||||
|
total: 0
|
||||||
|
|
||||||
|
exclude:
|
||||||
|
paths:
|
||||||
|
- _test\.go$
|
||||||
+502
@@ -0,0 +1,502 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.7.0] - 2026-09-16
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- [**breaking**] Order privilege events by sequence number and merge snapshots by position (#333)
|
||||||
|
|
||||||
|
## [0.6.1] - 2026-09-16
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Keep existing privileges when User.Added is processed late (#331)
|
||||||
|
|
||||||
|
## [0.6.0] - 2026-09-14
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- [**breaking**] Consume privilege events with go-messaging-amqp (#327)
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.13.2 (#325)
|
||||||
|
- Bump the minor version for breaking changes before 1.0.0 (#328)
|
||||||
|
|
||||||
|
## [0.5.1] - 2026-08-29
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(ci)* Use go-test-coverage binary directly to fix Gitea Actions (#303)
|
||||||
|
- *(deps)* Update module github.com/stretchr/testify to v1.12.0 (#319)
|
||||||
|
- *(deps)* Update module github.com/stretchr/testify to v1.12.1 (#321)
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.30.1 (#296)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.11.4 (#298)
|
||||||
|
- *(deps)* Update dependency go to v1.26.2 (#300)
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.25.0 (#304)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.12.0 (#306)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.12.1 (#308)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.12.2 (#309)
|
||||||
|
- *(deps)* Update actions/checkout action to v7 (#311)
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.26.0 (#313)
|
||||||
|
- *(deps)* Update actions/setup-go action to v7 (#315)
|
||||||
|
- *(deps)* Update actions/setup-python action to v7 (#317)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.13.1 (#322)
|
||||||
|
|
||||||
|
## [0.5.0] - 2026-03-12
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- *(client)* Add API key authentication for /authz endpoint (#294)
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update golang:1.25.5 docker digest to 3a01526 (#271)
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.24.0 (#273)
|
||||||
|
- *(deps)* Update dependency go to v1.25.6 (#274)
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.6 (#275)
|
||||||
|
- Remove GitLab CI configuration
|
||||||
|
- Add code coverage integration
|
||||||
|
- *(deps)* Update dependency go to v1.25.7 (#279)
|
||||||
|
- *(deps)* Update dependency go to v1.26.0 (#280)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.9.0 (#281)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.10.0 (#282)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.10.1 (#283)
|
||||||
|
- *(deps)* Update dependency go to v1.26.1 (#286)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.11.1 (#288)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.11.2 (#290)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.11.3 (#292)
|
||||||
|
|
||||||
|
## [0.4.1] - 2026-01-09
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update actions/setup-python action to v6
|
||||||
|
- Migrate module path to gitea.unbound.se
|
||||||
|
|
||||||
|
## [0.4.0] - 2026-01-09
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Migrate from GitLab CI to Gitea Actions
|
||||||
|
|
||||||
|
### 🚜 Refactor
|
||||||
|
|
||||||
|
- Update module path to new repository location
|
||||||
|
|
||||||
|
### 📚 Documentation
|
||||||
|
|
||||||
|
- Add CLAUDE.md for Claude Code integration
|
||||||
|
|
||||||
|
### 🧪 Testing
|
||||||
|
|
||||||
|
- Add concurrent fetch and read tests for privileges
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update golang:1.25.3 docker digest to 9ac0edc
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.6.1
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.29.0
|
||||||
|
- *(deps)* Update dependency go to v1.25.4
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.4
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.6.2
|
||||||
|
- *(deps)* Update golang:1.25.4 docker digest to efe81fa
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.29.1
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.30.0
|
||||||
|
- *(deps)* Update dependency go to v1.25.5
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.5
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.7.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.7.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.7.2
|
||||||
|
- *(deps)* Update golang:1.25.5 docker digest to 0c27bcf
|
||||||
|
- *(deps)* Update golang:1.25.5 docker digest to ad03ba9
|
||||||
|
- *(deps)* Update actions/setup-go action to v6
|
||||||
|
- *(deps)* Update actions/checkout action to v6
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.8.0
|
||||||
|
- Add pre-commit and release workflows
|
||||||
|
|
||||||
|
## [0.3.1] - 2025-11-02
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Change to write lock for thread safety in json unmarshal
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update golang:1.25.1 docker digest to 53f7808
|
||||||
|
- *(deps)* Update pre-commit hook lietu/go-pre-commit to v1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.5.0
|
||||||
|
- *(deps)* Update golang:1.25.1 docker digest to 12640a4
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.23.0
|
||||||
|
- *(deps)* Update dependency go to v1.25.2
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.2
|
||||||
|
- *(deps)* Update dependency go to v1.25.3
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.3
|
||||||
|
- Add default configuration for git-cliff
|
||||||
|
- *(deps)* Update golang:1.25.3 docker digest to 69d1009
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.6.0
|
||||||
|
|
||||||
|
## [0.3.0] - 2025-09-06
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- *(ci)* Add defaults pipeline template for CI workflow
|
||||||
|
- Add salary privilege to privilege management system
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/sparetimecoders/goamqp to v0.3.3
|
||||||
|
- *(deps)* Update module github.com/stretchr/testify to v1.11.0
|
||||||
|
- *(deps)* Update module github.com/stretchr/testify to v1.11.1
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.1.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.1.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.1.2
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.1.4
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.1.5
|
||||||
|
- *(deps)* Update golang:1.24.2 docker digest to bf7899c
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.25.0
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.25.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.1.6
|
||||||
|
- *(deps)* Update dependency go to v1.24.3
|
||||||
|
- *(deps)* Update golang docker tag to v1.24.3
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.26.0
|
||||||
|
- *(deps)* Update golang:1.24.3 docker digest to f255a7d
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.27.0
|
||||||
|
- *(deps)* Update dependency go to v1.24.4
|
||||||
|
- *(deps)* Update golang docker tag to v1.24.4
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.27.1
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.27.2
|
||||||
|
- *(deps)* Update golang:1.24.4 docker digest to 3494bbe
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.2.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.2.1
|
||||||
|
- *(deps)* Update golang:1.24.4 docker digest to 9f820b6
|
||||||
|
- *(deps)* Update dependency go to v1.24.5
|
||||||
|
- *(deps)* Update golang docker tag to v1.24.5
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.2.2
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.28.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.3.0
|
||||||
|
- *(deps)* Update golang:1.24.5 docker digest to 0a156a4
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.3.1
|
||||||
|
- *(deps)* Update dependency go to v1.24.6
|
||||||
|
- *(deps)* Update golang docker tag to v1.24.6
|
||||||
|
- *(deps)* Update pre-commit hook pre-commit/pre-commit-hooks to v6
|
||||||
|
- *(deps)* Update golang:1.24.6 docker digest to 958bfd1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.4.0
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.0
|
||||||
|
- *(deps)* Update dependency go to v1.25.0
|
||||||
|
- *(deps)* Update golang:1.25.0 docker digest to f6b9e1a
|
||||||
|
- *(deps)* Update dependency go to v1.25.1
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.1
|
||||||
|
|
||||||
|
## [0.2.0] - 2025-04-11
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Add timeout configuration to golangci-lint
|
||||||
|
- Add setup method for privilege event consumers
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/stretchr/testify to v1.10.0
|
||||||
|
- *(ci)* Update go image to amd64 architecture version
|
||||||
|
- *(deps)* Update module github.com/sparetimecoders/goamqp to v0.3.2
|
||||||
|
|
||||||
|
### 🚜 Refactor
|
||||||
|
|
||||||
|
- *(ci)* Remove pre-commit job and add new template
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook pre-commit/pre-commit-hooks to v5
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.20.1
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.21.0
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.21.1
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.21.2
|
||||||
|
- *(deps)* Pin golang docker tag to ad5c126
|
||||||
|
- *(deps)* Pin unbound/pre-commit docker tag to 92fce44
|
||||||
|
- *(deps)* Update golang docker tag to v1.23.3
|
||||||
|
- *(deps)* Update unbound/pre-commit docker digest to 596abf5
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.62.0
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to 8956c08
|
||||||
|
- *(deps)* Update unbound/pre-commit docker digest to e78425c
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to 3694e36
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to b2ca381
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to 2660218
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to c2d828f
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to 73f06be
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.62.2
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.19.0
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to ee5f0ad
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to b4aabba
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to 2b01164
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to 017ec6b
|
||||||
|
- *(deps)* Update golang docker tag to v1.23.4
|
||||||
|
- *(deps)* Update golang:1.23.4 docker digest to 574185e
|
||||||
|
- *(deps)* Update golang:1.23.4 docker digest to 7003184
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.20.0
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.21.3
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.21.4
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.22.0
|
||||||
|
- *(deps)* Update golang:1.23.4 docker digest to 7ea4c9d
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.22.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.63.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.63.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.63.2
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.63.3
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.63.4
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.23.0
|
||||||
|
- *(deps)* Update golang:1.23.4 docker digest to 3b1a7de
|
||||||
|
- *(deps)* Update golang:1.23.4 docker digest to 08e1417
|
||||||
|
- *(deps)* Update golang:1.23.4 docker digest to 585103a
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.23.1
|
||||||
|
- *(deps)* Update golang:1.23.4 docker digest to 5305905
|
||||||
|
- *(deps)* Update golang:1.23.4 docker digest to 9820aca
|
||||||
|
- *(deps)* Update golang docker tag to v1.23.5
|
||||||
|
- *(deps)* Update golang:1.23.5 docker digest to 8c10f21
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.23.2
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.23.3
|
||||||
|
- *(deps)* Update golang:1.23.5 docker digest to e213430
|
||||||
|
- *(deps)* Update golang docker tag to v1.23.6
|
||||||
|
- *(deps)* Update golang:1.23.6 docker digest to 958bd2e
|
||||||
|
- *(deps)* Update golang:1.23.6 docker digest to 9271129
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.2
|
||||||
|
- *(deps)* Update golang docker tag to v1.24.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.4
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.5
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.21.0
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.24.0
|
||||||
|
- *(deps)* Update golang:1.24.0 docker digest to 4a3f101
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.6
|
||||||
|
- *(deps)* Update golang docker tag to v1.24.1
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.22.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.7
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.8
|
||||||
|
- *(deps)* Update golang:1.24.1 docker digest to 5ecf333
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.24.2
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.0.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.0.2
|
||||||
|
- *(deps)* Update golang docker tag to v1.24.2
|
||||||
|
- *(deps)* Update golang:1.24.2 docker digest to aebb7df
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.24.3
|
||||||
|
|
||||||
|
## [0.1.4] - 2024-10-05
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/stretchr/testify to v1.9.0
|
||||||
|
- *(deps)* Update module github.com/sparetimecoders/goamqp to v0.3.1
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Update to Go 1.21.6
|
||||||
|
- Update to Go 1.22.0
|
||||||
|
- *(deps)* Update node.js to v20
|
||||||
|
- *(deps)* Update pre-commit hook devopshq/gitlab-ci-linter to v1.0.6
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.11.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.56.2
|
||||||
|
- *(deps)* Update pre-commit hook lietu/go-pre-commit to v0.1.0
|
||||||
|
- *(deps)* Update pre-commit hook pre-commit/pre-commit-hooks to v4.5.0
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.12.0
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.13.0
|
||||||
|
- *(deps)* Update golang docker tag to v1.22.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.57.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.57.1
|
||||||
|
- Add gitleaks to pre-commit setup
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.18.2
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.57.2
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.14.0
|
||||||
|
- *(deps)* Update golang docker tag to v1.22.2
|
||||||
|
- *(deps)* Update pre-commit hook pre-commit/pre-commit-hooks to v4.6.0
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.15.0
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.16.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.58.0
|
||||||
|
- *(deps)* Update golang docker tag to v1.22.3
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.58.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.58.2
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.59.0
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.18.3
|
||||||
|
- *(deps)* Update golang docker tag to v1.22.4
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.59.1
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.18.4
|
||||||
|
- *(deps)* Update golang docker tag to v1.22.5
|
||||||
|
- *(deps)* Update golang docker tag to v1.22.6
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.60.1
|
||||||
|
- *(deps)* Update golang docker tag to v1.23.0
|
||||||
|
- Update golangci-lint to use full version
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.60.2
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.60.3
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.17.0
|
||||||
|
- *(deps)* Update golang docker tag to v1.23.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.61.0
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.18.0
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.19.1
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.19.2
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.19.3
|
||||||
|
- *(deps)* Update golang docker tag to v1.23.2
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.20.0
|
||||||
|
- Add release flow
|
||||||
|
- Remove old release job
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.2.1 to 0.3.0
|
||||||
|
|
||||||
|
## [0.1.3] - 2023-12-26
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Prohibit concurrent read/write
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Update Go version
|
||||||
|
- Make releases handle multi-line release notes
|
||||||
|
- Update to Go 1.20.7
|
||||||
|
- Update pre-commit versions
|
||||||
|
- Update to Golang 1.21.0
|
||||||
|
- Update to Go 1.21.1 for vulnerabilities
|
||||||
|
- Use 1.21.1 in go.mod for Dependabot
|
||||||
|
- Update to go 1.21.3 and remove patch level
|
||||||
|
- Update version of Go
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.1.4 to 0.1.5
|
||||||
|
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.1.5 to 0.2.0
|
||||||
|
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.2.0 to 0.2.1
|
||||||
|
|
||||||
|
## [0.1.2] - 2023-06-04
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Update pre-commit and fix golangci-lint
|
||||||
|
- Update golangci-lint
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
- *(deps)* Bump github.com/stretchr/testify from 1.8.2 to 1.8.3
|
||||||
|
- *(deps)* Bump github.com/stretchr/testify from 1.8.3 to 1.8.4
|
||||||
|
|
||||||
|
## [0.1.1] - 2023-05-11
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Run builds with Go 1.19.2 to fix vulnerabilities
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Add vulnerability-check
|
||||||
|
- Replace deprecated ioutil.ReadAll
|
||||||
|
- Add pre-commit and remove those checks from test step
|
||||||
|
- Add local module to pre-commit config
|
||||||
|
- Add release handling
|
||||||
|
- Update to Go 1.19.5
|
||||||
|
- Change dependabot rebase strategy
|
||||||
|
- Update to golang 1.20.1
|
||||||
|
- Update Go verion for vulnerabilities scan
|
||||||
|
- Update to Go 1.20.3
|
||||||
|
- Update Go version and fix gitlabci lint
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
- *(deps)* Bump github.com/stretchr/testify from 1.8.0 to 1.8.1
|
||||||
|
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.1.1 to 0.1.2
|
||||||
|
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.1.2 to 0.1.3
|
||||||
|
- *(deps)* Bump github.com/stretchr/testify from 1.8.1 to 1.8.2
|
||||||
|
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.1.3 to 0.1.4
|
||||||
|
|
||||||
|
## [0.1.0] - 2022-07-20
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Pipeline
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Add dependabot config
|
||||||
|
- *(deps)* Bump gitlab.com/sparetimecoders/goamqp from 0.3.1 to 0.3.2
|
||||||
|
- *(deps)* Bump github.com/stretchr/testify from 1.4.0 to 1.7.0
|
||||||
|
- Remove dependabot-standalone
|
||||||
|
- Change to codecov binary instead of bash uploader
|
||||||
|
- *(deps)* Bump gitlab.com/sparetimecoders/goamqp from 0.3.2 to 0.4.0
|
||||||
|
- Switch to moved goamqp
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
- Add params to codecov
|
||||||
|
- *(deps)* Bump github.com/stretchr/testify from 1.7.0 to 1.7.1
|
||||||
|
- *(deps)* Bump gitlab.com/sparetimecoders/goamqp from 0.4.0 to 0.5.0
|
||||||
|
- *(deps)* Bump github.com/stretchr/testify from 1.7.1 to 1.7.2
|
||||||
|
- *(deps)* Bump github.com/stretchr/testify from 1.7.2 to 1.7.4
|
||||||
|
- *(deps)* Bump github.com/stretchr/testify from 1.7.4 to 1.7.5
|
||||||
|
- *(deps)* Bump github.com/stretchr/testify from 1.7.5 to 1.8.0
|
||||||
|
|
||||||
|
## [0.0.8] - 2021-05-15
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Add codecov upload
|
||||||
|
- Simplify pipeline
|
||||||
|
- Change coverage badge to codecov.io
|
||||||
|
- Add CI workflows
|
||||||
|
- Rename master -> main
|
||||||
|
- Group imports
|
||||||
|
- Update to latest version of goamqp
|
||||||
|
|
||||||
|
## [0.0.7] - 2020-04-12
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Update to Go 1.14 to fix test errors
|
||||||
|
- Use go mod download
|
||||||
|
- Sort companies before comparing since map-iteration is not stable
|
||||||
|
|
||||||
|
## [0.0.6] - 2020-04-12
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Path to repo
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Add tests
|
||||||
|
- Modify event structure
|
||||||
|
|
||||||
|
## [0.0.5] - 2019-12-31
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Add handling of removed privilege
|
||||||
|
|
||||||
|
## [0.0.4] - 2019-12-08
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Add name and registration number to event
|
||||||
|
|
||||||
|
## [0.0.3] - 2019-11-22
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Print unexpected messages
|
||||||
|
|
||||||
|
## [0.0.2] - 2019-11-06
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Initial version
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Rename module
|
||||||
|
|
||||||
|
<!-- generated by git-cliff -->
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# authz_client
|
||||||
|
|
||||||
|
Shared Go library for authorization service client integration.
|
||||||
|
|
||||||
|
## Shared Documentation
|
||||||
|
|
||||||
|
@../docs/claude/architecture.md
|
||||||
|
@../docs/claude/go-services.md
|
||||||
|
@../docs/claude/conventions.md
|
||||||
|
|
||||||
|
## Library Information
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
Provides a client for the authz-service, handling privilege management for users across companies. Used by all microservices that need to check user permissions.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
import client "gitea.unbound.se/shiny/authz_client"
|
||||||
|
|
||||||
|
// Create handler with options
|
||||||
|
handler := client.New(client.WithBaseURL("http://authz-service"))
|
||||||
|
|
||||||
|
// Check user privileges
|
||||||
|
allowed := handler.IsAllowed(email, companyID, func(p client.CompanyPrivileges) bool {
|
||||||
|
return p.Invoicing
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Privileges
|
||||||
|
|
||||||
|
The `CompanyPrivileges` struct contains permission flags:
|
||||||
|
- `Admin` - Administrative access
|
||||||
|
- `Company` - Company management
|
||||||
|
- `Consumer` - Consumer/customer access
|
||||||
|
- `Time` - Time tracking
|
||||||
|
- `Invoicing` - Invoice management
|
||||||
|
- `Accounting` - Accounting access
|
||||||
|
- `Supplier` - Supplier management
|
||||||
|
- `Salary` - Salary/payroll access
|
||||||
|
|
||||||
|
### Event Handling
|
||||||
|
|
||||||
|
Registers per-replica (transient) go-messaging-amqp consumers for privilege events from authz-service (`Setup()`). Each routing key gets its own queue, so events arrive in any order. `Process` orders them by the `sequenceNo` authz-service's event store stamps on every event: all four events come from authz-service's `Company` aggregate, so the global sequence number orders them within a company. Each (email, company) keeps the sequence number of the last fact about membership and about each privilege; an event only overrides older facts. `User.Removed` stamps every privilege, so a late grant can't come back. An event without `sequenceNo` fails closed: additions are dropped, and removals apply and block every later event for those facts until the next snapshot. An event with a negative or implausibly large `sequenceNo` is dropped. Tests that seed the handler must set `SequenceNo`.
|
||||||
|
|
||||||
|
### Startup
|
||||||
|
|
||||||
|
Call `Fetch()` **after** `conn.Start` (consumers bound), never before: events published between the snapshot and the binding would otherwise be lost. authz-service only serves a snapshot whose read view has applied every stored event. It sends that position in the `X-Authz-Sequence` header and answers 503 while the read view lags (backlog, backfill, reset); `Fetch` retries 503 for about a minute. `Fetch` merges the snapshot as facts at that sequence number (newer event facts win, pairs missing from the snapshot are removed) and drops later events at or below it. A snapshot older than one already merged is ignored, so concurrent or repeated fetches are safe. A snapshot without the header merges at 0 and logs a warning (rollout window only). When the `/authz` contract changes, deploy authz-service before the services that use this library.
|
||||||
|
|
||||||
|
Don't combine `Setup()` with go-messaging-amqp's `WithReconnect`: a reconnect declares new per-replica queues, so revocations published during the outage are lost unless `Fetch()` runs again. Services exit on connection loss (`CloseListener`) and re-fetch on start.
|
||||||
@@ -1,3 +1 @@
|
|||||||
# Shiny authz-client
|
# Shiny authz-client
|
||||||
|
|
||||||
[](https://gitlab.com/unboundsoftware/shiny/authz_client/commits/master)[](https://gitlab.com/unboundsoftware/shiny/authz_client/commits/master)
|
|
||||||
@@ -1,13 +1,31 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
goamqp "codeberg.org/messaging/go-messaging-amqp"
|
||||||
|
spec "codeberg.org/messaging/messaging"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SequenceHeader carries the global sequence number of the authz-service read view the
|
||||||
|
// /authz snapshot was read at.
|
||||||
|
const SequenceHeader = "X-Authz-Sequence"
|
||||||
|
|
||||||
|
// pending marks a fact cleared by an event without a sequence number. No event can
|
||||||
|
// override it; only a snapshot can.
|
||||||
|
const pending = math.MaxInt
|
||||||
|
|
||||||
// CompanyPrivileges contains the privileges for a combination of email address and company id
|
// CompanyPrivileges contains the privileges for a combination of email address and company id
|
||||||
type CompanyPrivileges struct {
|
type CompanyPrivileges struct {
|
||||||
Admin bool `json:"admin"`
|
Admin bool `json:"admin"`
|
||||||
@@ -17,13 +35,87 @@ type CompanyPrivileges struct {
|
|||||||
Invoicing bool `json:"invoicing"`
|
Invoicing bool `json:"invoicing"`
|
||||||
Accounting bool `json:"accounting"`
|
Accounting bool `json:"accounting"`
|
||||||
Supplier bool `json:"supplier"`
|
Supplier bool `json:"supplier"`
|
||||||
|
Salary bool `json:"salary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// field returns the flag for privilege, or nil for an unknown privilege.
|
||||||
|
func (c *CompanyPrivileges) field(privilege Privilege) *bool {
|
||||||
|
switch privilege {
|
||||||
|
case PrivilegeAdmin:
|
||||||
|
return &c.Admin
|
||||||
|
case PrivilegeCompany:
|
||||||
|
return &c.Company
|
||||||
|
case PrivilegeConsumer:
|
||||||
|
return &c.Consumer
|
||||||
|
case PrivilegeTime:
|
||||||
|
return &c.Time
|
||||||
|
case PrivilegeInvoicing:
|
||||||
|
return &c.Invoicing
|
||||||
|
case PrivilegeAccounting:
|
||||||
|
return &c.Accounting
|
||||||
|
case PrivilegeSupplier:
|
||||||
|
return &c.Supplier
|
||||||
|
case PrivilegeSalary:
|
||||||
|
return &c.Salary
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// companyState is what the handler knows about one email in one company. Every
|
||||||
|
// privilege event for a company comes from authz-service's Company aggregate, so
|
||||||
|
// its global sequence number orders the events for the pair. The four routing
|
||||||
|
// keys arrive on separate queues in any order; a fact is only overwritten by a
|
||||||
|
// fact with a higher sequence number.
|
||||||
|
type companyState struct {
|
||||||
|
privileges CompanyPrivileges
|
||||||
|
// privilegeSeq is the sequence number of the last fact about each privilege.
|
||||||
|
privilegeSeq map[Privilege]int
|
||||||
|
// member is whether the user belongs to the company, as of memberSeq. A
|
||||||
|
// User.Removed stamps every privilege with its sequence number, so a grant
|
||||||
|
// older than the removal can't come back.
|
||||||
|
member bool
|
||||||
|
memberSeq int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCompanyState() *companyState {
|
||||||
|
return &companyState{privilegeSeq: map[Privilege]int{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearPending turns facts cleared without a sequence number into unordered facts, so
|
||||||
|
// the snapshot being merged replaces them.
|
||||||
|
func (s *companyState) clearPending() {
|
||||||
|
if s.memberSeq == pending {
|
||||||
|
s.memberSeq = 0
|
||||||
|
}
|
||||||
|
for p, seq := range s.privilegeSeq {
|
||||||
|
if seq == pending {
|
||||||
|
s.privilegeSeq[p] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxSeq is the highest sequence number of any fact in the state.
|
||||||
|
func (s *companyState) maxSeq() int {
|
||||||
|
m := s.memberSeq
|
||||||
|
for _, seq := range s.privilegeSeq {
|
||||||
|
m = max(m, seq)
|
||||||
|
}
|
||||||
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrivilegeHandler processes PrivilegeAdded-events and fetches the initial set of privileges from an authz-service
|
// PrivilegeHandler processes PrivilegeAdded-events and fetches the initial set of privileges from an authz-service
|
||||||
type PrivilegeHandler struct {
|
type PrivilegeHandler struct {
|
||||||
|
*sync.RWMutex
|
||||||
client *http.Client
|
client *http.Client
|
||||||
baseURL string
|
baseURL string
|
||||||
privileges map[string]map[string]*CompanyPrivileges
|
apiKey string
|
||||||
|
state map[string]map[string]*companyState
|
||||||
|
// floor is the sequence number of the newest snapshot. Its effects are all in
|
||||||
|
// the state, so an event at or below it is stale.
|
||||||
|
floor int
|
||||||
|
// retries is how many times Fetch retries a snapshot authz-service isn't ready to serve.
|
||||||
|
retries int
|
||||||
|
retryDelay time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// OptsFunc is used to configure the PrivilegeHandler
|
// OptsFunc is used to configure the PrivilegeHandler
|
||||||
@@ -36,12 +128,22 @@ func WithBaseURL(url string) OptsFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithAPIKey sets an API key used as a Bearer token when fetching privileges
|
||||||
|
func WithAPIKey(key string) OptsFunc {
|
||||||
|
return func(handler *PrivilegeHandler) {
|
||||||
|
handler.apiKey = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// New creates a new PrivilegeHandler. Pass OptsFuncs to configure.
|
// New creates a new PrivilegeHandler. Pass OptsFuncs to configure.
|
||||||
func New(opts ...OptsFunc) *PrivilegeHandler {
|
func New(opts ...OptsFunc) *PrivilegeHandler {
|
||||||
handler := &PrivilegeHandler{
|
handler := &PrivilegeHandler{
|
||||||
client: &http.Client{},
|
RWMutex: &sync.RWMutex{},
|
||||||
|
client: &http.Client{Timeout: 30 * time.Second},
|
||||||
baseURL: "http://authz-service",
|
baseURL: "http://authz-service",
|
||||||
privileges: map[string]map[string]*CompanyPrivileges{},
|
state: map[string]map[string]*companyState{},
|
||||||
|
retries: 60,
|
||||||
|
retryDelay: time.Second,
|
||||||
}
|
}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(handler)
|
opt(handler)
|
||||||
@@ -49,103 +151,272 @@ func New(opts ...OptsFunc) *PrivilegeHandler {
|
|||||||
return handler
|
return handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the initial set of privileges from an authz-service
|
// Fetch a snapshot of all privileges from an authz-service and merge it into the state.
|
||||||
|
//
|
||||||
|
// Call it after the AMQP connection has started: authz-service only serves a snapshot
|
||||||
|
// that includes every stored event, so together with the bound queues no event is
|
||||||
|
// missed. While its read view is behind it answers 503, and Fetch retries. The snapshot
|
||||||
|
// is applied as facts at its sequence number: a fact from an event newer than the
|
||||||
|
// snapshot is kept, everything else is replaced, and a pair missing from the snapshot is
|
||||||
|
// removed. A snapshot older than one already merged is ignored.
|
||||||
func (h *PrivilegeHandler) Fetch() error {
|
func (h *PrivilegeHandler) Fetch() error {
|
||||||
resp, err := h.client.Get(fmt.Sprintf("%s/authz", h.baseURL))
|
for attempt := 0; ; attempt++ {
|
||||||
|
err := h.fetch()
|
||||||
|
if !errors.Is(err, errNotReady) || attempt >= h.retries {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
time.Sleep(h.retryDelay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errNotReady = errors.New("fetch privileges: authz-service read view not ready")
|
||||||
|
|
||||||
|
func (h *PrivilegeHandler) fetch() error {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/authz", h.baseURL), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
buff, err := ioutil.ReadAll(resp.Body)
|
if h.apiKey != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+h.apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusServiceUnavailable {
|
||||||
|
return errNotReady
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("fetch privileges: unexpected status %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
buff, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(buff, &h.privileges)
|
var snapshot map[string]map[string]CompanyPrivileges
|
||||||
if err != nil {
|
if err := json.Unmarshal(buff, &snapshot); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
seq := 0
|
||||||
|
if v := resp.Header.Get(SequenceHeader); v != "" {
|
||||||
|
seq, err = strconv.Atoi(v)
|
||||||
|
if err != nil || seq < 0 {
|
||||||
|
return fmt.Errorf("fetch privileges: invalid %s header %q", SequenceHeader, v)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ponytail: only during the rollout window, before authz-service sends the header.
|
||||||
|
slog.Warn("authz snapshot has no sequence number; events older than it can revert it", "header", SequenceHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Lock()
|
||||||
|
defer h.Unlock()
|
||||||
|
h.merge(snapshot, seq)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process privilege-related events and update the internal state
|
func (h *PrivilegeHandler) merge(snapshot map[string]map[string]CompanyPrivileges, seq int) {
|
||||||
func (h *PrivilegeHandler) Process(msg interface{}) bool {
|
if seq < h.floor {
|
||||||
switch ev := msg.(type) {
|
// A newer snapshot is already merged; this one could only bring back removed state.
|
||||||
case *UserAdded:
|
slog.Warn("ignoring authz snapshot older than the one already merged", "seq", seq, "floor", h.floor)
|
||||||
if priv, exists := h.privileges[ev.Email]; exists {
|
return
|
||||||
priv[ev.CompanyID] = &CompanyPrivileges{}
|
}
|
||||||
} else {
|
for email, companies := range h.state {
|
||||||
h.privileges[ev.Email] = map[string]*CompanyPrivileges{
|
for companyID, s := range companies {
|
||||||
ev.CompanyID: {},
|
s.clearPending()
|
||||||
|
if _, exists := snapshot[email][companyID]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.maxSeq() <= seq {
|
||||||
|
// Nothing newer than the snapshot; the floor keeps stale events out.
|
||||||
|
delete(companies, companyID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
removeUser(s, seq, false)
|
||||||
|
}
|
||||||
|
if len(companies) == 0 {
|
||||||
|
delete(h.state, email)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
for email, companies := range snapshot {
|
||||||
case *UserRemoved:
|
for companyID, privileges := range companies {
|
||||||
if priv, exists := h.privileges[ev.Email]; exists {
|
s := h.company(email, companyID)
|
||||||
delete(priv, ev.CompanyID)
|
if s.memberSeq <= seq {
|
||||||
|
s.member = true
|
||||||
|
s.memberSeq = seq
|
||||||
}
|
}
|
||||||
return true
|
for _, p := range AllPrivilege {
|
||||||
case *PrivilegeAdded:
|
if s.privilegeSeq[p] <= seq {
|
||||||
h.setPrivileges(ev.Email, ev.CompanyID, ev.Privilege, true)
|
*s.privileges.field(p) = *privileges.field(p)
|
||||||
return true
|
s.privilegeSeq[p] = seq
|
||||||
case *PrivilegeRemoved:
|
}
|
||||||
h.setPrivileges(ev.Email, ev.CompanyID, ev.Privilege, false)
|
}
|
||||||
return true
|
}
|
||||||
default:
|
}
|
||||||
fmt.Printf("Got unexpected message type (%s): '%+v'\n", reflect.TypeOf(msg).String(), msg)
|
h.floor = max(h.floor, seq)
|
||||||
return false
|
}
|
||||||
|
|
||||||
|
func (h *PrivilegeHandler) Setup() []goamqp.Setup {
|
||||||
|
return []goamqp.Setup{
|
||||||
|
goamqp.TransientEventStreamConsumer("User.Added", process[UserAdded](h)),
|
||||||
|
goamqp.TransientEventStreamConsumer("User.Removed", process[UserRemoved](h)),
|
||||||
|
goamqp.TransientEventStreamConsumer("Privilege.Added", process[PrivilegeAdded](h)),
|
||||||
|
goamqp.TransientEventStreamConsumer("Privilege.Removed", process[PrivilegeRemoved](h)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *PrivilegeHandler) setPrivileges(email, companyId string, privilege Privilege, set bool) {
|
// privilegeEvent is the set of events Process handles.
|
||||||
if priv, exists := h.privileges[email]; exists {
|
type privilegeEvent interface {
|
||||||
if c, exists := priv[companyId]; exists {
|
UserAdded | UserRemoved | PrivilegeAdded | PrivilegeRemoved
|
||||||
switch privilege {
|
|
||||||
case PrivilegeAdmin:
|
|
||||||
c.Admin = set
|
|
||||||
case PrivilegeCompany:
|
|
||||||
c.Company = set
|
|
||||||
case PrivilegeConsumer:
|
|
||||||
c.Consumer = set
|
|
||||||
case PrivilegeTime:
|
|
||||||
c.Time = set
|
|
||||||
case PrivilegeInvoicing:
|
|
||||||
c.Invoicing = set
|
|
||||||
case PrivilegeAccounting:
|
|
||||||
c.Accounting = set
|
|
||||||
case PrivilegeSupplier:
|
|
||||||
c.Supplier = set
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
priv[companyId] = &CompanyPrivileges{}
|
// process adapts Process to a typed go-messaging-amqp handler.
|
||||||
h.setPrivileges(email, companyId, privilege, set)
|
func process[T privilegeEvent](h *PrivilegeHandler) spec.EventHandler[T] {
|
||||||
|
return func(_ context.Context, event spec.ConsumableEvent[T]) error {
|
||||||
|
return h.Process(&event.Payload)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
h.privileges[email] = map[string]*CompanyPrivileges{}
|
|
||||||
h.setPrivileges(email, companyId, privilege, set)
|
// Process privilege-related events and update the internal state.
|
||||||
|
//
|
||||||
|
// Events are applied by sequence number, not by arrival order. An event without a
|
||||||
|
// sequence number can't be ordered, so it fails closed: a removal is applied and an
|
||||||
|
// addition is dropped.
|
||||||
|
func (h *PrivilegeHandler) Process(msg any) error {
|
||||||
|
h.Lock()
|
||||||
|
defer h.Unlock()
|
||||||
|
|
||||||
|
switch ev := msg.(type) {
|
||||||
|
case *UserAdded:
|
||||||
|
if h.stale(ev.SequenceNo, true, ev) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s := h.company(ev.Email, ev.CompanyID)
|
||||||
|
if ev.SequenceNo > s.memberSeq {
|
||||||
|
s.member = true
|
||||||
|
s.memberSeq = ev.SequenceNo
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case *UserRemoved:
|
||||||
|
if h.stale(ev.SequenceNo, false, ev) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
removeUser(h.company(ev.Email, ev.CompanyID), ev.SequenceNo, ev.SequenceNo == 0)
|
||||||
|
return nil
|
||||||
|
case *PrivilegeAdded:
|
||||||
|
if h.stale(ev.SequenceNo, true, ev) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
h.setPrivilege(ev.Email, ev.CompanyID, ev.Privilege, ev.SequenceNo, true)
|
||||||
|
return nil
|
||||||
|
case *PrivilegeRemoved:
|
||||||
|
if h.stale(ev.SequenceNo, false, ev) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
h.setPrivilege(ev.Email, ev.CompanyID, ev.Privilege, ev.SequenceNo, false)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
slog.Error("unexpected privilege message type", "type", reflect.TypeOf(msg).String())
|
||||||
|
return fmt.Errorf("unexpected event type: '%s'", reflect.TypeOf(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stale reports whether an event must be skipped: an addition without a sequence
|
||||||
|
// number, or any event already covered by the latest snapshot.
|
||||||
|
func (h *PrivilegeHandler) stale(seq int, addition bool, ev any) bool {
|
||||||
|
if seq < 0 || seq >= pending {
|
||||||
|
slog.Error("dropping privilege event with invalid sequence number", "type", reflect.TypeOf(ev).String(), "seq", seq)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if seq == 0 {
|
||||||
|
if addition {
|
||||||
|
slog.Warn("dropping privilege addition without sequence number", "type", reflect.TypeOf(ev).String())
|
||||||
|
}
|
||||||
|
return addition
|
||||||
|
}
|
||||||
|
return seq <= h.floor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PrivilegeHandler) company(email, companyID string) *companyState {
|
||||||
|
companies, exists := h.state[email]
|
||||||
|
if !exists {
|
||||||
|
companies = map[string]*companyState{}
|
||||||
|
h.state[email] = companies
|
||||||
|
}
|
||||||
|
s, exists := companies[companyID]
|
||||||
|
if !exists {
|
||||||
|
s = newCompanyState()
|
||||||
|
companies[companyID] = s
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeUser removes the user at seq, overriding every fact older than seq. An
|
||||||
|
// unordered removal (all) overrides every fact and marks them pending, so no event can
|
||||||
|
// bring them back before the next snapshot.
|
||||||
|
func removeUser(s *companyState, seq int, all bool) {
|
||||||
|
stamp := seq
|
||||||
|
if all {
|
||||||
|
stamp = pending
|
||||||
|
}
|
||||||
|
if all || s.memberSeq < seq {
|
||||||
|
s.member = false
|
||||||
|
s.memberSeq = max(s.memberSeq, stamp)
|
||||||
|
}
|
||||||
|
for _, p := range AllPrivilege {
|
||||||
|
if all || s.privilegeSeq[p] < seq {
|
||||||
|
*s.privileges.field(p) = false
|
||||||
|
s.privilegeSeq[p] = max(s.privilegeSeq[p], stamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PrivilegeHandler) setPrivilege(email, companyID string, privilege Privilege, seq int, set bool) {
|
||||||
|
if !privilege.IsValid() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s := h.company(email, companyID)
|
||||||
|
if seq != 0 && seq <= s.privilegeSeq[privilege] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*s.privileges.field(privilege) = set
|
||||||
|
if seq == 0 {
|
||||||
|
// Only an unordered removal gets here; keep it until the next snapshot.
|
||||||
|
seq = pending
|
||||||
|
}
|
||||||
|
s.privilegeSeq[privilege] = max(s.privilegeSeq[privilege], seq)
|
||||||
|
// authz-service's aggregate adds the user when a privilege is granted.
|
||||||
|
if set && seq > s.memberSeq {
|
||||||
|
s.member = true
|
||||||
|
s.memberSeq = seq
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompaniesByUser return a slice of company ids matching the provided email and predicate func
|
// CompaniesByUser return a slice of company ids matching the provided email and predicate func
|
||||||
func (h *PrivilegeHandler) CompaniesByUser(email string, predicate func(privileges CompanyPrivileges) bool) []string {
|
func (h *PrivilegeHandler) CompaniesByUser(email string, predicate func(privileges CompanyPrivileges) bool) []string {
|
||||||
var result []string
|
h.RLock()
|
||||||
if p, exists := h.privileges[email]; exists {
|
defer h.RUnlock()
|
||||||
for k, v := range p {
|
result := []string{}
|
||||||
if predicate(*v) {
|
for k, s := range h.state[email] {
|
||||||
|
if s.member && predicate(s.privileges) {
|
||||||
result = append(result, k)
|
result = append(result, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAllowed return true if the provided predicate return true for the privileges matching the provided email and companyID, return false otherwise
|
// IsAllowed return true if the provided predicate return true for the privileges matching the provided email and companyID, return false otherwise
|
||||||
func (h *PrivilegeHandler) IsAllowed(email, companyID string, predicate func(privileges CompanyPrivileges) bool) bool {
|
func (h *PrivilegeHandler) IsAllowed(email, companyID string, predicate func(privileges CompanyPrivileges) bool) bool {
|
||||||
if p, exists := h.privileges[email]; exists {
|
h.RLock()
|
||||||
if v, exists := p[companyID]; exists {
|
defer h.RUnlock()
|
||||||
return predicate(*v)
|
if s, exists := h.state[email][companyID]; exists && s.member {
|
||||||
|
return predicate(s.privileges)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
+787
-30
@@ -1,31 +1,41 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stretchr/testify/assert"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"sort"
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
goamqp "codeberg.org/messaging/go-messaging-amqp"
|
||||||
|
spec "codeberg.org/messaging/messaging"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPrivilegeHandler_Process_InvalidType(t *testing.T) {
|
func TestPrivilegeHandler_Process_InvalidType(t *testing.T) {
|
||||||
handler := New(WithBaseURL("base"))
|
handler := New(WithBaseURL("base"))
|
||||||
|
|
||||||
result := handler.Process("abc")
|
err := handler.Process("abc")
|
||||||
|
|
||||||
assert.False(t, result)
|
assert.EqualError(t, err, "unexpected event type: 'string'")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrivilegeHandler_Process_PrivilegeRemoved(t *testing.T) {
|
func TestPrivilegeHandler_Process_PrivilegeRemoved(t *testing.T) {
|
||||||
handler := New(WithBaseURL("base"))
|
handler := New(WithBaseURL("base"))
|
||||||
|
|
||||||
result := handler.Process(&PrivilegeAdded{
|
err := handler.Process(&PrivilegeAdded{
|
||||||
|
SequenceNo: 1,
|
||||||
Email: "jim@example.org",
|
Email: "jim@example.org",
|
||||||
CompanyID: "abc-123",
|
CompanyID: "abc-123",
|
||||||
Privilege: PrivilegeAdmin,
|
Privilege: PrivilegeAdmin,
|
||||||
})
|
})
|
||||||
assert.True(t, result)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
||||||
return privileges.Admin
|
return privileges.Admin
|
||||||
@@ -33,12 +43,13 @@ func TestPrivilegeHandler_Process_PrivilegeRemoved(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, []string{"abc-123"}, companies)
|
assert.Equal(t, []string{"abc-123"}, companies)
|
||||||
|
|
||||||
result = handler.Process(&PrivilegeRemoved{
|
err = handler.Process(&PrivilegeRemoved{
|
||||||
|
SequenceNo: 2,
|
||||||
Email: "jim@example.org",
|
Email: "jim@example.org",
|
||||||
CompanyID: "abc-123",
|
CompanyID: "abc-123",
|
||||||
Privilege: PrivilegeAdmin,
|
Privilege: PrivilegeAdmin,
|
||||||
})
|
})
|
||||||
assert.True(t, result)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
companies = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
companies = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
||||||
return privileges.Admin
|
return privileges.Admin
|
||||||
@@ -50,17 +61,19 @@ func TestPrivilegeHandler_Process_PrivilegeRemoved(t *testing.T) {
|
|||||||
func TestPrivilegeHandler_Process_UserAdded_And_UserRemoved(t *testing.T) {
|
func TestPrivilegeHandler_Process_UserAdded_And_UserRemoved(t *testing.T) {
|
||||||
handler := New(WithBaseURL("base"))
|
handler := New(WithBaseURL("base"))
|
||||||
|
|
||||||
result := handler.Process(&UserAdded{
|
err := handler.Process(&UserAdded{
|
||||||
|
SequenceNo: 1,
|
||||||
Email: "jim@example.org",
|
Email: "jim@example.org",
|
||||||
CompanyID: "abc-123",
|
CompanyID: "abc-123",
|
||||||
})
|
})
|
||||||
assert.True(t, result)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
result = handler.Process(&UserAdded{
|
err = handler.Process(&UserAdded{
|
||||||
|
SequenceNo: 2,
|
||||||
Email: "jim@example.org",
|
Email: "jim@example.org",
|
||||||
CompanyID: "abc-456",
|
CompanyID: "abc-456",
|
||||||
})
|
})
|
||||||
assert.True(t, result)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
||||||
return true
|
return true
|
||||||
@@ -68,17 +81,19 @@ func TestPrivilegeHandler_Process_UserAdded_And_UserRemoved(t *testing.T) {
|
|||||||
sort.Strings(companies)
|
sort.Strings(companies)
|
||||||
assert.Equal(t, []string{"abc-123", "abc-456"}, companies)
|
assert.Equal(t, []string{"abc-123", "abc-456"}, companies)
|
||||||
|
|
||||||
result = handler.Process(&UserRemoved{
|
err = handler.Process(&UserRemoved{
|
||||||
|
SequenceNo: 3,
|
||||||
Email: "jim@example.org",
|
Email: "jim@example.org",
|
||||||
CompanyID: "abc-123",
|
CompanyID: "abc-123",
|
||||||
})
|
})
|
||||||
assert.True(t, result)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
result = handler.Process(&UserRemoved{
|
err = handler.Process(&UserRemoved{
|
||||||
|
SequenceNo: 4,
|
||||||
Email: "jim@example.org",
|
Email: "jim@example.org",
|
||||||
CompanyID: "abc-456",
|
CompanyID: "abc-456",
|
||||||
})
|
})
|
||||||
assert.True(t, result)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
companies = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
companies = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
||||||
return true
|
return true
|
||||||
@@ -86,6 +101,51 @@ func TestPrivilegeHandler_Process_UserAdded_And_UserRemoved(t *testing.T) {
|
|||||||
assert.Empty(t, companies)
|
assert.Empty(t, companies)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Process_UserAdded_Keeps_Existing_Privileges(t *testing.T) {
|
||||||
|
// Each routing key has its own transient queue, so Privilege.Added can be
|
||||||
|
// processed before the User.Added published before it. The privilege must
|
||||||
|
// survive either order: nothing re-reads it until the next Fetch at start.
|
||||||
|
userAdded := &UserAdded{SequenceNo: 1, Email: "jim@example.org", CompanyID: "abc-123"}
|
||||||
|
privilegeAdded := &PrivilegeAdded{SequenceNo: 2, Email: "jim@example.org", CompanyID: "abc-123", Privilege: PrivilegeAdmin}
|
||||||
|
for name, order := range map[string][]any{
|
||||||
|
"user added first": {userAdded, privilegeAdded},
|
||||||
|
"privilege added first": {privilegeAdded, userAdded},
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
handler := New(WithBaseURL("base"))
|
||||||
|
for _, event := range order {
|
||||||
|
require.NoError(t, handler.Process(event))
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
|
||||||
|
return privileges.Admin
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Process_UserAdded_After_UserRemoved_Starts_Empty(t *testing.T) {
|
||||||
|
handler := New(WithBaseURL("base"))
|
||||||
|
|
||||||
|
assert.NoError(t, handler.Process(&UserAdded{SequenceNo: 1, Email: "jim@example.org", CompanyID: "abc-123"}))
|
||||||
|
assert.NoError(t, handler.Process(&PrivilegeAdded{
|
||||||
|
SequenceNo: 2,
|
||||||
|
Email: "jim@example.org",
|
||||||
|
CompanyID: "abc-123",
|
||||||
|
Privilege: PrivilegeAdmin,
|
||||||
|
}))
|
||||||
|
assert.NoError(t, handler.Process(&UserRemoved{SequenceNo: 3, Email: "jim@example.org", CompanyID: "abc-123"}))
|
||||||
|
assert.NoError(t, handler.Process(&UserAdded{SequenceNo: 4, Email: "jim@example.org", CompanyID: "abc-123"}))
|
||||||
|
|
||||||
|
// Membership is back, without the privileges the removal took away.
|
||||||
|
assert.Equal(t, []string{"abc-123"}, handler.CompaniesByUser("jim@example.org", func(CompanyPrivileges) bool {
|
||||||
|
return true
|
||||||
|
}))
|
||||||
|
assert.False(t, handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
|
||||||
|
return privileges.Admin
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
func TestPrivilegeHandler_GetCompanies_Email_Not_Found(t *testing.T) {
|
func TestPrivilegeHandler_GetCompanies_Email_Not_Found(t *testing.T) {
|
||||||
handler := New(WithBaseURL("base"))
|
handler := New(WithBaseURL("base"))
|
||||||
|
|
||||||
@@ -99,11 +159,12 @@ func TestPrivilegeHandler_GetCompanies_Email_Not_Found(t *testing.T) {
|
|||||||
func TestPrivilegeHandler_GetCompanies_No_Companies_Found(t *testing.T) {
|
func TestPrivilegeHandler_GetCompanies_No_Companies_Found(t *testing.T) {
|
||||||
handler := New(WithBaseURL("base"))
|
handler := New(WithBaseURL("base"))
|
||||||
|
|
||||||
result := handler.Process(&UserAdded{
|
err := handler.Process(&UserAdded{
|
||||||
|
SequenceNo: 1,
|
||||||
Email: "jim@example.org",
|
Email: "jim@example.org",
|
||||||
CompanyID: "abc-123",
|
CompanyID: "abc-123",
|
||||||
})
|
})
|
||||||
assert.True(t, result)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
||||||
return privileges.Admin
|
return privileges.Admin
|
||||||
@@ -117,11 +178,12 @@ func TestPrivilegeHandler_GetCompanies_No_Companies_Found(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, []string{"abc-123"}, companies)
|
assert.Equal(t, []string{"abc-123"}, companies)
|
||||||
|
|
||||||
result = handler.Process(&UserRemoved{
|
err = handler.Process(&UserRemoved{
|
||||||
|
SequenceNo: 2,
|
||||||
Email: "jim@example.org",
|
Email: "jim@example.org",
|
||||||
CompanyID: "abc-123",
|
CompanyID: "abc-123",
|
||||||
})
|
})
|
||||||
assert.True(t, result)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
companies = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
companies = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
||||||
return true
|
return true
|
||||||
@@ -132,12 +194,13 @@ func TestPrivilegeHandler_GetCompanies_No_Companies_Found(t *testing.T) {
|
|||||||
func TestPrivilegeHandler_GetCompanies_Company_With_Company_Access_Found(t *testing.T) {
|
func TestPrivilegeHandler_GetCompanies_Company_With_Company_Access_Found(t *testing.T) {
|
||||||
handler := New(WithBaseURL("base"))
|
handler := New(WithBaseURL("base"))
|
||||||
|
|
||||||
result := handler.Process(&PrivilegeAdded{
|
err := handler.Process(&PrivilegeAdded{
|
||||||
|
SequenceNo: 1,
|
||||||
Email: "jim@example.org",
|
Email: "jim@example.org",
|
||||||
CompanyID: "abc-123",
|
CompanyID: "abc-123",
|
||||||
Privilege: PrivilegeCompany,
|
Privilege: PrivilegeCompany,
|
||||||
})
|
})
|
||||||
assert.True(t, result)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
||||||
return privileges.Company
|
return privileges.Company
|
||||||
@@ -149,12 +212,13 @@ func TestPrivilegeHandler_GetCompanies_Company_With_Company_Access_Found(t *test
|
|||||||
func TestPrivilegeHandler_GetCompanies_Company_With_Admin_Access_Found(t *testing.T) {
|
func TestPrivilegeHandler_GetCompanies_Company_With_Admin_Access_Found(t *testing.T) {
|
||||||
handler := New(WithBaseURL("base"))
|
handler := New(WithBaseURL("base"))
|
||||||
|
|
||||||
result := handler.Process(&PrivilegeAdded{
|
err := handler.Process(&PrivilegeAdded{
|
||||||
|
SequenceNo: 1,
|
||||||
Email: "jim@example.org",
|
Email: "jim@example.org",
|
||||||
CompanyID: "abc-123",
|
CompanyID: "abc-123",
|
||||||
Privilege: PrivilegeConsumer,
|
Privilege: PrivilegeConsumer,
|
||||||
})
|
})
|
||||||
assert.True(t, result)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
||||||
return privileges.Consumer
|
return privileges.Consumer
|
||||||
@@ -176,7 +240,8 @@ func TestPrivilegeHandler_IsAllowed_Return_False_If_No_Privileges(t *testing.T)
|
|||||||
func TestPrivilegeHandler_IsAllowed_Return_True_If_Privilege_Exists(t *testing.T) {
|
func TestPrivilegeHandler_IsAllowed_Return_True_If_Privilege_Exists(t *testing.T) {
|
||||||
handler := New(WithBaseURL("base"))
|
handler := New(WithBaseURL("base"))
|
||||||
|
|
||||||
handler.Process(&PrivilegeAdded{
|
_ = handler.Process(&PrivilegeAdded{
|
||||||
|
SequenceNo: 1,
|
||||||
Email: "jim@example.org",
|
Email: "jim@example.org",
|
||||||
CompanyID: "abc-123",
|
CompanyID: "abc-123",
|
||||||
Privilege: PrivilegeTime,
|
Privilege: PrivilegeTime,
|
||||||
@@ -188,7 +253,8 @@ func TestPrivilegeHandler_IsAllowed_Return_True_If_Privilege_Exists(t *testing.T
|
|||||||
|
|
||||||
assert.True(t, result)
|
assert.True(t, result)
|
||||||
|
|
||||||
handler.Process(&PrivilegeAdded{
|
_ = handler.Process(&PrivilegeAdded{
|
||||||
|
SequenceNo: 2,
|
||||||
Email: "jim@example.org",
|
Email: "jim@example.org",
|
||||||
CompanyID: "abc-123",
|
CompanyID: "abc-123",
|
||||||
Privilege: PrivilegeInvoicing,
|
Privilege: PrivilegeInvoicing,
|
||||||
@@ -200,7 +266,8 @@ func TestPrivilegeHandler_IsAllowed_Return_True_If_Privilege_Exists(t *testing.T
|
|||||||
|
|
||||||
assert.True(t, result)
|
assert.True(t, result)
|
||||||
|
|
||||||
handler.Process(&PrivilegeAdded{
|
_ = handler.Process(&PrivilegeAdded{
|
||||||
|
SequenceNo: 3,
|
||||||
Email: "jim@example.org",
|
Email: "jim@example.org",
|
||||||
CompanyID: "abc-123",
|
CompanyID: "abc-123",
|
||||||
Privilege: PrivilegeAccounting,
|
Privilege: PrivilegeAccounting,
|
||||||
@@ -212,7 +279,8 @@ func TestPrivilegeHandler_IsAllowed_Return_True_If_Privilege_Exists(t *testing.T
|
|||||||
|
|
||||||
assert.True(t, result)
|
assert.True(t, result)
|
||||||
|
|
||||||
handler.Process(&PrivilegeAdded{
|
_ = handler.Process(&PrivilegeAdded{
|
||||||
|
SequenceNo: 4,
|
||||||
Email: "jim@example.org",
|
Email: "jim@example.org",
|
||||||
CompanyID: "abc-123",
|
CompanyID: "abc-123",
|
||||||
Privilege: PrivilegeSupplier,
|
Privilege: PrivilegeSupplier,
|
||||||
@@ -223,6 +291,52 @@ func TestPrivilegeHandler_IsAllowed_Return_True_If_Privilege_Exists(t *testing.T
|
|||||||
})
|
})
|
||||||
|
|
||||||
assert.True(t, result)
|
assert.True(t, result)
|
||||||
|
|
||||||
|
_ = handler.Process(&PrivilegeAdded{
|
||||||
|
SequenceNo: 5,
|
||||||
|
Email: "jim@example.org",
|
||||||
|
CompanyID: "abc-123",
|
||||||
|
Privilege: PrivilegeSalary,
|
||||||
|
})
|
||||||
|
|
||||||
|
result = handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
|
||||||
|
return privileges.Salary
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.True(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Fetch_Sends_Authorization_Header_When_APIKey_Set(t *testing.T) {
|
||||||
|
var receivedAuth string
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
receivedAuth = r.Header.Get("Authorization")
|
||||||
|
_, _ = w.Write([]byte("{}"))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
handler := New(
|
||||||
|
WithBaseURL(server.URL),
|
||||||
|
WithAPIKey("my-secret-key"),
|
||||||
|
)
|
||||||
|
|
||||||
|
err := handler.Fetch()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "Bearer my-secret-key", receivedAuth)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Fetch_No_Authorization_Header_Without_APIKey(t *testing.T) {
|
||||||
|
var receivedAuth string
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
receivedAuth = r.Header.Get("Authorization")
|
||||||
|
_, _ = w.Write([]byte("{}"))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
handler := New(WithBaseURL(server.URL))
|
||||||
|
|
||||||
|
err := handler.Fetch()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, receivedAuth)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrivilegeHandler_Fetch_Error_Response(t *testing.T) {
|
func TestPrivilegeHandler_Fetch_Error_Response(t *testing.T) {
|
||||||
@@ -276,7 +390,8 @@ func TestPrivilegeHandler_Fetch_Valid(t *testing.T) {
|
|||||||
"time": true,
|
"time": true,
|
||||||
"invoicing": true,
|
"invoicing": true,
|
||||||
"accounting": false,
|
"accounting": false,
|
||||||
"supplier": false
|
"supplier": false,
|
||||||
|
"salary": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
@@ -290,7 +405,7 @@ func TestPrivilegeHandler_Fetch_Valid(t *testing.T) {
|
|||||||
|
|
||||||
err := handler.Fetch()
|
err := handler.Fetch()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
expectedPrivileges := map[string]map[string]*CompanyPrivileges{
|
expectedPrivileges := map[string]map[string]CompanyPrivileges{
|
||||||
"jim@example.org": {
|
"jim@example.org": {
|
||||||
"00010203-0405-4607-8809-0a0b0c0d0e0f": {
|
"00010203-0405-4607-8809-0a0b0c0d0e0f": {
|
||||||
Admin: false,
|
Admin: false,
|
||||||
@@ -300,8 +415,650 @@ func TestPrivilegeHandler_Fetch_Valid(t *testing.T) {
|
|||||||
Invoicing: true,
|
Invoicing: true,
|
||||||
Accounting: false,
|
Accounting: false,
|
||||||
Supplier: false,
|
Supplier: false,
|
||||||
|
Salary: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.Equal(t, expectedPrivileges, handler.privileges)
|
assert.Equal(t, expectedPrivileges, privilegesOf(handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Fetch_Concurrent_Fetches(t *testing.T) {
|
||||||
|
privileges := `
|
||||||
|
{
|
||||||
|
"jim@example.org": {
|
||||||
|
"00010203-0405-4607-8809-0a0b0c0d0e0f": {
|
||||||
|
"admin": false,
|
||||||
|
"company": true,
|
||||||
|
"consumer": false,
|
||||||
|
"time": true,
|
||||||
|
"invoicing": true,
|
||||||
|
"accounting": false,
|
||||||
|
"supplier": false,
|
||||||
|
"salary": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, _ = w.Write([]byte(privileges))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
baseURL := server.Listener.Addr().String()
|
||||||
|
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
|
||||||
|
|
||||||
|
// Run multiple Fetch calls concurrently to test thread-safety
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errors := make(chan error, 10)
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := handler.Fetch(); err != nil {
|
||||||
|
errors <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(errors)
|
||||||
|
|
||||||
|
// Check no errors occurred
|
||||||
|
for err := range errors {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify privileges were set correctly
|
||||||
|
expectedPrivileges := map[string]map[string]CompanyPrivileges{
|
||||||
|
"jim@example.org": {
|
||||||
|
"00010203-0405-4607-8809-0a0b0c0d0e0f": {
|
||||||
|
Admin: false,
|
||||||
|
Company: true,
|
||||||
|
Consumer: false,
|
||||||
|
Time: true,
|
||||||
|
Invoicing: true,
|
||||||
|
Accounting: false,
|
||||||
|
Supplier: false,
|
||||||
|
Salary: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, expectedPrivileges, privilegesOf(handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Concurrent_Fetch_And_Read(t *testing.T) {
|
||||||
|
privileges := `
|
||||||
|
{
|
||||||
|
"jim@example.org": {
|
||||||
|
"abc-123": {
|
||||||
|
"admin": true,
|
||||||
|
"company": true,
|
||||||
|
"consumer": false,
|
||||||
|
"time": false,
|
||||||
|
"invoicing": false,
|
||||||
|
"accounting": false,
|
||||||
|
"supplier": false,
|
||||||
|
"salary": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, _ = w.Write([]byte(privileges))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
baseURL := server.Listener.Addr().String()
|
||||||
|
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errors := make(chan error, 100)
|
||||||
|
|
||||||
|
// Start multiple Fetch operations
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := handler.Fetch(); err != nil {
|
||||||
|
errors <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concurrently read privileges while Fetch is running
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
_ = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
||||||
|
return privileges.Admin
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concurrently check privileges while Fetch is running
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
_ = handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
|
||||||
|
return privileges.Admin
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(errors)
|
||||||
|
|
||||||
|
// Check no errors occurred
|
||||||
|
for err := range errors {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify privileges are correct after all concurrent operations
|
||||||
|
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
||||||
|
return privileges.Admin
|
||||||
|
})
|
||||||
|
assert.Equal(t, []string{"abc-123"}, companies)
|
||||||
|
|
||||||
|
isAllowed := handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
|
||||||
|
return privileges.Admin && privileges.Company
|
||||||
|
})
|
||||||
|
assert.True(t, isAllowed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Concurrent_Process_And_Read(t *testing.T) {
|
||||||
|
handler := New(WithBaseURL("base"))
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
// Concurrently add privileges via Process
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
companyID := fmt.Sprintf("company-%d", i%10)
|
||||||
|
go func(id string) {
|
||||||
|
defer wg.Done()
|
||||||
|
_ = handler.Process(&PrivilegeAdded{
|
||||||
|
SequenceNo: 1,
|
||||||
|
Email: "jim@example.org",
|
||||||
|
CompanyID: id,
|
||||||
|
Privilege: PrivilegeAdmin,
|
||||||
|
})
|
||||||
|
}(companyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concurrently read privileges while Process is running
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
_ = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
||||||
|
return privileges.Admin
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Verify all companies were added
|
||||||
|
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
||||||
|
return privileges.Admin
|
||||||
|
})
|
||||||
|
sort.Strings(companies)
|
||||||
|
|
||||||
|
expected := make([]string, 10)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
expected[i] = fmt.Sprintf("company-%d", i)
|
||||||
|
}
|
||||||
|
sort.Strings(expected)
|
||||||
|
|
||||||
|
assert.Equal(t, expected, companies)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Concurrent_Multiple_Operations(t *testing.T) {
|
||||||
|
privileges := `
|
||||||
|
{
|
||||||
|
"jim@example.org": {
|
||||||
|
"initial-company": {
|
||||||
|
"admin": true,
|
||||||
|
"company": true,
|
||||||
|
"consumer": false,
|
||||||
|
"time": false,
|
||||||
|
"invoicing": false,
|
||||||
|
"accounting": false,
|
||||||
|
"supplier": false,
|
||||||
|
"salary": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, _ = w.Write([]byte(privileges))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
baseURL := server.Listener.Addr().String()
|
||||||
|
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
// Fetch
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
_ = handler.Fetch()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process PrivilegeAdded
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(idx int) {
|
||||||
|
defer wg.Done()
|
||||||
|
_ = handler.Process(&PrivilegeAdded{
|
||||||
|
SequenceNo: 1,
|
||||||
|
Email: "jane@example.org",
|
||||||
|
CompanyID: fmt.Sprintf("company-%d", idx%5),
|
||||||
|
Privilege: PrivilegeCompany,
|
||||||
|
})
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompaniesByUser reads
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
email := "jim@example.org"
|
||||||
|
if i%2 == 0 {
|
||||||
|
email = "jane@example.org"
|
||||||
|
}
|
||||||
|
go func(e string) {
|
||||||
|
defer wg.Done()
|
||||||
|
_ = handler.CompaniesByUser(e, func(privileges CompanyPrivileges) bool {
|
||||||
|
return privileges.Admin || privileges.Company
|
||||||
|
})
|
||||||
|
}(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAllowed reads
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
_ = handler.IsAllowed("jim@example.org", "initial-company", func(privileges CompanyPrivileges) bool {
|
||||||
|
return privileges.Admin
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Verify final state is consistent
|
||||||
|
jimCompanies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
|
||||||
|
return privileges.Admin
|
||||||
|
})
|
||||||
|
assert.Contains(t, jimCompanies, "initial-company")
|
||||||
|
|
||||||
|
janeCompanies := handler.CompaniesByUser("jane@example.org", func(privileges CompanyPrivileges) bool {
|
||||||
|
return privileges.Company
|
||||||
|
})
|
||||||
|
sort.Strings(janeCompanies)
|
||||||
|
|
||||||
|
expectedJane := []string{"company-0", "company-1", "company-2", "company-3", "company-4"}
|
||||||
|
assert.Equal(t, expectedJane, janeCompanies)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Setup(t *testing.T) {
|
||||||
|
handler := New(WithBaseURL("base"))
|
||||||
|
|
||||||
|
topology, err := goamqp.CollectTopology("some-service", handler.Setup()...)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
wiring := map[string]string{}
|
||||||
|
for _, e := range topology.Endpoints {
|
||||||
|
assert.Equal(t, spec.DirectionConsume, e.Direction)
|
||||||
|
assert.True(t, e.Ephemeral, "%s must be a per-replica consumer", e.RoutingKey)
|
||||||
|
wiring[e.RoutingKey] = e.MessageType
|
||||||
|
}
|
||||||
|
// A key wired to the wrong type could turn a revocation into a grant.
|
||||||
|
assert.Equal(t, map[string]string{
|
||||||
|
"User.Added": "client.UserAdded",
|
||||||
|
"User.Removed": "client.UserRemoved",
|
||||||
|
"Privilege.Added": "client.PrivilegeAdded",
|
||||||
|
"Privilege.Removed": "client.PrivilegeRemoved",
|
||||||
|
}, wiring)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_process(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
handler := New(WithBaseURL("base"))
|
||||||
|
admin := func(p CompanyPrivileges) bool { return p.Admin }
|
||||||
|
|
||||||
|
assert.NoError(t, process[UserAdded](handler)(ctx, spec.ConsumableEvent[UserAdded]{
|
||||||
|
Payload: UserAdded{SequenceNo: 1, Email: "jim@example.org", CompanyID: "abc-123"},
|
||||||
|
}))
|
||||||
|
assert.False(t, handler.IsAllowed("jim@example.org", "abc-123", admin))
|
||||||
|
|
||||||
|
assert.NoError(t, process[PrivilegeAdded](handler)(ctx, spec.ConsumableEvent[PrivilegeAdded]{
|
||||||
|
Payload: PrivilegeAdded{SequenceNo: 2, Email: "jim@example.org", CompanyID: "abc-123", Privilege: PrivilegeAdmin},
|
||||||
|
}))
|
||||||
|
assert.True(t, handler.IsAllowed("jim@example.org", "abc-123", admin))
|
||||||
|
|
||||||
|
assert.NoError(t, process[PrivilegeRemoved](handler)(ctx, spec.ConsumableEvent[PrivilegeRemoved]{
|
||||||
|
Payload: PrivilegeRemoved{SequenceNo: 3, Email: "jim@example.org", CompanyID: "abc-123", Privilege: PrivilegeAdmin},
|
||||||
|
}))
|
||||||
|
assert.False(t, handler.IsAllowed("jim@example.org", "abc-123", admin))
|
||||||
|
|
||||||
|
assert.NoError(t, process[UserRemoved](handler)(ctx, spec.ConsumableEvent[UserRemoved]{
|
||||||
|
Payload: UserRemoved{SequenceNo: 4, Email: "jim@example.org", CompanyID: "abc-123"},
|
||||||
|
}))
|
||||||
|
assert.Empty(t, handler.CompaniesByUser("jim@example.org", func(CompanyPrivileges) bool { return true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// privilegesOf returns the privileges of every membership in the handler.
|
||||||
|
func privilegesOf(h *PrivilegeHandler) map[string]map[string]CompanyPrivileges {
|
||||||
|
h.RLock()
|
||||||
|
defer h.RUnlock()
|
||||||
|
result := map[string]map[string]CompanyPrivileges{}
|
||||||
|
for email, companies := range h.state {
|
||||||
|
for companyID, s := range companies {
|
||||||
|
if !s.member {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if result[email] == nil {
|
||||||
|
result[email] = map[string]CompanyPrivileges{}
|
||||||
|
}
|
||||||
|
result[email][companyID] = s.privileges
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Process_Orders_By_SequenceNo(t *testing.T) {
|
||||||
|
const email, company = "jim@example.org", "abc-123"
|
||||||
|
userAdded := func(seq int) any { return &UserAdded{Email: email, CompanyID: company, SequenceNo: seq} }
|
||||||
|
userRemoved := func(seq int) any { return &UserRemoved{Email: email, CompanyID: company, SequenceNo: seq} }
|
||||||
|
added := func(seq int) any {
|
||||||
|
return &PrivilegeAdded{Email: email, CompanyID: company, Privilege: PrivilegeAdmin, SequenceNo: seq}
|
||||||
|
}
|
||||||
|
removed := func(seq int) any {
|
||||||
|
return &PrivilegeRemoved{Email: email, CompanyID: company, Privilege: PrivilegeAdmin, SequenceNo: seq}
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
delivered []any
|
||||||
|
member bool
|
||||||
|
admin bool
|
||||||
|
}{
|
||||||
|
{name: "in order", delivered: []any{userAdded(1), added(2)}, member: true, admin: true},
|
||||||
|
{name: "privilege before older user added keeps the grant", delivered: []any{added(2), userAdded(1)}, member: true, admin: true},
|
||||||
|
{name: "late privilege added after user removed", delivered: []any{userAdded(1), userRemoved(3), added(2)}, member: false, admin: false},
|
||||||
|
{name: "late privilege added after privilege removed", delivered: []any{userAdded(1), removed(3), added(2)}, member: true, admin: false},
|
||||||
|
{name: "late user added does not revive privileges removed with the user", delivered: []any{userRemoved(3), added(2), userAdded(1)}, member: false, admin: false},
|
||||||
|
{name: "re-added user starts without the removed privileges", delivered: []any{added(2), userRemoved(3), userAdded(4)}, member: true, admin: false},
|
||||||
|
{name: "privilege granted after re-add", delivered: []any{userAdded(4), userRemoved(3), added(5), added(2)}, member: true, admin: true},
|
||||||
|
{name: "late user removed does not remove a newer grant", delivered: []any{added(4), userRemoved(3)}, member: true, admin: true},
|
||||||
|
{name: "late user removed does not remove a newer membership", delivered: []any{userAdded(4), userRemoved(3)}, member: true, admin: false},
|
||||||
|
{name: "late privilege added before re-add stays revoked", delivered: []any{userAdded(1), userRemoved(3), added(2), userAdded(4)}, member: true, admin: false},
|
||||||
|
{name: "privilege removed after user removed keeps the user out", delivered: []any{userRemoved(2), removed(3)}, member: false, admin: false},
|
||||||
|
{name: "duplicate delivery is idempotent", delivered: []any{added(2), removed(3), added(2)}, member: true, admin: false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
handler := New()
|
||||||
|
for _, event := range tt.delivered {
|
||||||
|
require.NoError(t, handler.Process(event))
|
||||||
|
}
|
||||||
|
all := func(CompanyPrivileges) bool { return true }
|
||||||
|
assert.Equal(t, tt.member, handler.IsAllowed(email, company, all))
|
||||||
|
assert.Equal(t, tt.admin, handler.IsAllowed(email, company, func(p CompanyPrivileges) bool { return p.Admin }))
|
||||||
|
if tt.member {
|
||||||
|
assert.Equal(t, []string{company}, handler.CompaniesByUser(email, all))
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, []string{}, handler.CompaniesByUser(email, all))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Process_Without_SequenceNo_Fails_Closed(t *testing.T) {
|
||||||
|
const email, company = "jim@example.org", "abc-123"
|
||||||
|
admin := func(p CompanyPrivileges) bool { return p.Admin }
|
||||||
|
handler := New()
|
||||||
|
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: email, CompanyID: company, Privilege: PrivilegeAdmin}))
|
||||||
|
require.NoError(t, handler.Process(&UserAdded{Email: email, CompanyID: company}))
|
||||||
|
assert.False(t, handler.IsAllowed(email, company, func(CompanyPrivileges) bool { return true }), "an unordered addition is dropped")
|
||||||
|
require.NoError(t, handler.Process(&UserAdded{Email: email, CompanyID: company, SequenceNo: 1}))
|
||||||
|
assert.False(t, handler.IsAllowed(email, company, admin), "the dropped grant doesn't appear with a later membership")
|
||||||
|
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: email, CompanyID: company, Privilege: PrivilegeAdmin, SequenceNo: 5}))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeRemoved{Email: email, CompanyID: company, Privilege: PrivilegeAdmin}))
|
||||||
|
assert.False(t, handler.IsAllowed(email, company, admin), "an unordered privilege removal applies")
|
||||||
|
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: email, CompanyID: company, Privilege: PrivilegeTime, SequenceNo: 6}))
|
||||||
|
require.NoError(t, handler.Process(&UserRemoved{Email: email, CompanyID: company}))
|
||||||
|
assert.False(t, handler.IsAllowed(email, company, func(CompanyPrivileges) bool { return true }), "an unordered user removal applies")
|
||||||
|
}
|
||||||
|
|
||||||
|
// snapshotServer serves body as the /authz snapshot at seq; a negative seq sends no header.
|
||||||
|
func snapshotServer(t *testing.T, seq int, body string) *httptest.Server {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if seq >= 0 {
|
||||||
|
w.Header().Set(SequenceHeader, fmt.Sprint(seq))
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte(body))
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Fetch_Merges_Snapshot_By_SequenceNo(t *testing.T) {
|
||||||
|
const email = "jim@example.org"
|
||||||
|
admin := func(p CompanyPrivileges) bool { return p.Admin }
|
||||||
|
all := func(CompanyPrivileges) bool { return true }
|
||||||
|
snapshot := `{"jim@example.org": {"in-snapshot": {"admin": true}, "stale-in-snapshot": {"admin": true}}}`
|
||||||
|
|
||||||
|
handler := New(WithBaseURL(snapshotServer(t, 10, snapshot).URL))
|
||||||
|
// Delivered while the snapshot was read.
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeRemoved{Email: email, CompanyID: "in-snapshot", Privilege: PrivilegeAdmin, SequenceNo: 11}))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: email, CompanyID: "stale-in-snapshot", Privilege: PrivilegeTime, SequenceNo: 9}))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: email, CompanyID: "new", Privilege: PrivilegeAdmin, SequenceNo: 12}))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: email, CompanyID: "revoked", Privilege: PrivilegeAdmin, SequenceNo: 8}))
|
||||||
|
require.NoError(t, handler.Process(&UserAdded{Email: email, CompanyID: "revoked-later", SequenceNo: 7}))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: email, CompanyID: "revoked-later", Privilege: PrivilegeAdmin, SequenceNo: 13}))
|
||||||
|
|
||||||
|
require.NoError(t, handler.Fetch())
|
||||||
|
|
||||||
|
assert.True(t, handler.IsAllowed(email, "in-snapshot", all))
|
||||||
|
assert.False(t, handler.IsAllowed(email, "in-snapshot", admin), "a removal newer than the snapshot wins")
|
||||||
|
assert.False(t, handler.IsAllowed(email, "stale-in-snapshot", func(p CompanyPrivileges) bool { return p.Time }), "the snapshot replaces an older fact")
|
||||||
|
assert.True(t, handler.IsAllowed(email, "new", admin), "a grant newer than the snapshot is kept")
|
||||||
|
assert.False(t, handler.IsAllowed(email, "revoked", all), "a pair missing from the snapshot is removed")
|
||||||
|
assert.True(t, handler.IsAllowed(email, "revoked-later", admin), "a grant newer than the snapshot survives the removal")
|
||||||
|
|
||||||
|
// Events the snapshot already covers are stale.
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: email, CompanyID: "revoked", Privilege: PrivilegeAdmin, SequenceNo: 10}))
|
||||||
|
assert.False(t, handler.IsAllowed(email, "revoked", all), "an event at the snapshot sequence is stale")
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: email, CompanyID: "revoked", Privilege: PrivilegeAdmin, SequenceNo: 14}))
|
||||||
|
assert.True(t, handler.IsAllowed(email, "revoked", admin))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Fetch_Replaces_Pairs_Missing_From_Snapshot(t *testing.T) {
|
||||||
|
handler := New(WithBaseURL(snapshotServer(t, 10, `{"jim@example.org": {"kept": {"company": true}}}`).URL))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: "jim@example.org", CompanyID: "gone", Privilege: PrivilegeAdmin, SequenceNo: 3}))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: "jane@example.org", CompanyID: "gone", Privilege: PrivilegeAdmin, SequenceNo: 4}))
|
||||||
|
|
||||||
|
require.NoError(t, handler.Fetch())
|
||||||
|
|
||||||
|
assert.Equal(t, map[string]map[string]CompanyPrivileges{"jim@example.org": {"kept": {Company: true}}}, privilegesOf(handler))
|
||||||
|
handler.RLock()
|
||||||
|
defer handler.RUnlock()
|
||||||
|
assert.NotContains(t, handler.state, "jane@example.org", "an email without companies is dropped")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Fetch_Without_SequenceHeader_Keeps_Sequenced_Facts(t *testing.T) {
|
||||||
|
handler := New(WithBaseURL(snapshotServer(t, -1, `{"jim@example.org": {"abc-123": {"admin": true}}}`).URL))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeRemoved{Email: "jim@example.org", CompanyID: "abc-123", Privilege: PrivilegeAdmin, SequenceNo: 3}))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: "jim@example.org", CompanyID: "other", Privilege: PrivilegeAdmin, SequenceNo: 4}))
|
||||||
|
|
||||||
|
require.NoError(t, handler.Fetch())
|
||||||
|
|
||||||
|
assert.Equal(t, map[string]map[string]CompanyPrivileges{
|
||||||
|
"jim@example.org": {"abc-123": {}, "other": {Admin: true}},
|
||||||
|
}, privilegesOf(handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Fetch_Rejects_Bad_Responses(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
status int
|
||||||
|
header string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{name: "status", status: http.StatusUnauthorized, err: "fetch privileges: unexpected status 401 Unauthorized"},
|
||||||
|
{name: "invalid header", status: http.StatusOK, header: "abc", err: `fetch privileges: invalid X-Authz-Sequence header "abc"`},
|
||||||
|
{name: "negative header", status: http.StatusOK, header: "-1", err: `fetch privileges: invalid X-Authz-Sequence header "-1"`},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if tt.header != "" {
|
||||||
|
w.Header().Set(SequenceHeader, tt.header)
|
||||||
|
}
|
||||||
|
w.WriteHeader(tt.status)
|
||||||
|
_, _ = w.Write([]byte(`{"jim@example.org": {"abc-123": {"admin": true}}}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
handler := New(WithBaseURL(server.URL))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: "jane@example.org", CompanyID: "abc-123", Privilege: PrivilegeTime, SequenceNo: 1}))
|
||||||
|
|
||||||
|
assert.EqualError(t, handler.Fetch(), tt.err)
|
||||||
|
assert.Equal(t, map[string]map[string]CompanyPrivileges{"jane@example.org": {"abc-123": {Time: true}}}, privilegesOf(handler), "state is untouched")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Revocation_During_Fetch_Wins(t *testing.T) {
|
||||||
|
// The consumers are bound before Fetch, so a revocation published after authz-service
|
||||||
|
// read the snapshot is processed while the response is still on its way.
|
||||||
|
handler := New()
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// assert, not require: this runs on the server's goroutine.
|
||||||
|
assert.NoError(t, handler.Process(&UserRemoved{Email: "jim@example.org", CompanyID: "abc-123", SequenceNo: 21}))
|
||||||
|
w.Header().Set(SequenceHeader, "20")
|
||||||
|
_, _ = w.Write([]byte(`{"jim@example.org": {"abc-123": {"admin": true}}}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
handler.baseURL = server.URL
|
||||||
|
|
||||||
|
require.NoError(t, handler.Fetch())
|
||||||
|
|
||||||
|
assert.False(t, handler.IsAllowed("jim@example.org", "abc-123", func(CompanyPrivileges) bool { return true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Fetch_Ignores_Snapshot_Older_Than_Merged_One(t *testing.T) {
|
||||||
|
const email = "jim@example.org"
|
||||||
|
all := func(CompanyPrivileges) bool { return true }
|
||||||
|
handler := New(WithBaseURL(snapshotServer(t, 12, `{}`).URL))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: email, CompanyID: "abc-123", Privilege: PrivilegeAdmin, SequenceNo: 5}))
|
||||||
|
require.NoError(t, handler.Process(&UserRemoved{Email: email, CompanyID: "abc-123", SequenceNo: 11}))
|
||||||
|
require.NoError(t, handler.Fetch())
|
||||||
|
|
||||||
|
// A concurrent Fetch read at seq 10, before the removal, merges last.
|
||||||
|
handler.baseURL = snapshotServer(t, 10, `{"jim@example.org": {"abc-123": {"admin": true}}}`).URL
|
||||||
|
require.NoError(t, handler.Fetch())
|
||||||
|
assert.False(t, handler.IsAllowed(email, "abc-123", all))
|
||||||
|
|
||||||
|
// So does a snapshot without a sequence number.
|
||||||
|
handler.baseURL = snapshotServer(t, -1, `{"jim@example.org": {"abc-123": {"admin": true}}}`).URL
|
||||||
|
require.NoError(t, handler.Fetch())
|
||||||
|
assert.False(t, handler.IsAllowed(email, "abc-123", all))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Unordered_Removal_Holds_Until_Next_Snapshot(t *testing.T) {
|
||||||
|
const email, company = "jim@example.org", "abc-123"
|
||||||
|
all := func(CompanyPrivileges) bool { return true }
|
||||||
|
admin := func(p CompanyPrivileges) bool { return p.Admin }
|
||||||
|
handler := New(WithBaseURL(snapshotServer(t, 30, `{"jim@example.org": {"abc-123": {"admin": true}, "def-456": {"time": true}}}`).URL))
|
||||||
|
require.NoError(t, handler.Process(&UserAdded{Email: email, CompanyID: company, SequenceNo: 10}))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: email, CompanyID: company, Privilege: PrivilegeAdmin, SequenceNo: 11}))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: email, CompanyID: "def-456", Privilege: PrivilegeTime, SequenceNo: 12}))
|
||||||
|
|
||||||
|
require.NoError(t, handler.Process(&UserRemoved{Email: email, CompanyID: company}))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeRemoved{Email: email, CompanyID: "def-456", Privilege: PrivilegeTime}))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: email, CompanyID: company, Privilege: PrivilegeSalary, SequenceNo: 15}))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: email, CompanyID: company, Privilege: PrivilegeAdmin, SequenceNo: 16}))
|
||||||
|
require.NoError(t, handler.Process(&UserAdded{Email: email, CompanyID: company, SequenceNo: 17}))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: email, CompanyID: "def-456", Privilege: PrivilegeTime, SequenceNo: 18}))
|
||||||
|
assert.False(t, handler.IsAllowed(email, company, all), "no event overrides an unordered user removal")
|
||||||
|
assert.False(t, handler.IsAllowed(email, "def-456", func(p CompanyPrivileges) bool { return p.Time }), "no event overrides an unordered privilege removal")
|
||||||
|
|
||||||
|
require.NoError(t, handler.Fetch())
|
||||||
|
assert.True(t, handler.IsAllowed(email, company, admin), "the next snapshot replaces the unordered removal")
|
||||||
|
assert.True(t, handler.IsAllowed(email, "def-456", func(p CompanyPrivileges) bool { return p.Time }))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Fetch_Deletes_Unordered_Removal_Missing_From_Snapshot(t *testing.T) {
|
||||||
|
handler := New(WithBaseURL(snapshotServer(t, 30, `{}`).URL))
|
||||||
|
require.NoError(t, handler.Process(&UserRemoved{Email: "jim@example.org", CompanyID: "abc-123"}))
|
||||||
|
|
||||||
|
require.NoError(t, handler.Fetch())
|
||||||
|
|
||||||
|
handler.RLock()
|
||||||
|
defer handler.RUnlock()
|
||||||
|
assert.Empty(t, handler.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Process_Unknown_Privilege_Creates_No_State(t *testing.T) {
|
||||||
|
handler := New()
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeRemoved{Email: "jim@example.org", CompanyID: "abc-123", Privilege: "UNKNOWN", SequenceNo: 1}))
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: "jim@example.org", CompanyID: "abc-123", Privilege: "UNKNOWN", SequenceNo: 2}))
|
||||||
|
|
||||||
|
handler.RLock()
|
||||||
|
defer handler.RUnlock()
|
||||||
|
assert.Empty(t, handler.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Fetch_Retries_While_ReadView_Not_Ready(t *testing.T) {
|
||||||
|
var calls atomic.Int32
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if calls.Add(1) < 3 {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set(SequenceHeader, "7")
|
||||||
|
_, _ = w.Write([]byte(`{"jim@example.org": {"abc-123": {"admin": true}}}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
handler := New(WithBaseURL(server.URL))
|
||||||
|
handler.retryDelay = time.Millisecond
|
||||||
|
|
||||||
|
require.NoError(t, handler.Fetch())
|
||||||
|
|
||||||
|
assert.Equal(t, int32(3), calls.Load())
|
||||||
|
assert.True(t, handler.IsAllowed("jim@example.org", "abc-123", func(p CompanyPrivileges) bool { return p.Admin }))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Fetch_Gives_Up_When_ReadView_Stays_Behind(t *testing.T) {
|
||||||
|
var calls atomic.Int32
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
calls.Add(1)
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
handler := New(WithBaseURL(server.URL))
|
||||||
|
handler.retryDelay = time.Millisecond
|
||||||
|
handler.retries = 2
|
||||||
|
|
||||||
|
assert.EqualError(t, handler.Fetch(), "fetch privileges: authz-service read view not ready")
|
||||||
|
assert.Equal(t, int32(3), calls.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivilegeHandler_Process_Drops_Invalid_SequenceNo(t *testing.T) {
|
||||||
|
for _, seq := range []int{-1, math.MaxInt} {
|
||||||
|
handler := New()
|
||||||
|
require.NoError(t, handler.Process(&PrivilegeAdded{Email: "jim@example.org", CompanyID: "abc-123", Privilege: PrivilegeAdmin, SequenceNo: seq}))
|
||||||
|
require.NoError(t, handler.Process(&UserRemoved{Email: "jim@example.org", CompanyID: "abc-123", SequenceNo: seq}))
|
||||||
|
handler.RLock()
|
||||||
|
assert.Empty(t, handler.state, "seq %d", seq)
|
||||||
|
handler.RUnlock()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
# git-cliff ~ default configuration file
|
||||||
|
# https://git-cliff.org/docs/configuration
|
||||||
|
#
|
||||||
|
# Lines starting with "#" are comments.
|
||||||
|
# Configuration options are organized into tables and keys.
|
||||||
|
# See documentation for more information on available options.
|
||||||
|
|
||||||
|
[changelog]
|
||||||
|
# template for the changelog header
|
||||||
|
header = """
|
||||||
|
# Changelog\n
|
||||||
|
All notable changes to this project will be documented in this file.\n
|
||||||
|
"""
|
||||||
|
# template for the changelog body
|
||||||
|
# https://keats.github.io/tera/docs/#introduction
|
||||||
|
body = """
|
||||||
|
{% if version %}\
|
||||||
|
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||||
|
{% else %}\
|
||||||
|
## [unreleased]
|
||||||
|
{% endif %}\
|
||||||
|
{% for group, commits in commits | group_by(attribute="group") %}
|
||||||
|
### {{ group | striptags | trim | upper_first }}
|
||||||
|
{% for commit in commits %}
|
||||||
|
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
|
||||||
|
{% if commit.breaking %}[**breaking**] {% endif %}\
|
||||||
|
{{ commit.message | upper_first }}\
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}\n
|
||||||
|
"""
|
||||||
|
# template for the changelog footer
|
||||||
|
footer = """
|
||||||
|
<!-- generated by git-cliff -->
|
||||||
|
"""
|
||||||
|
# remove the leading and trailing s
|
||||||
|
trim = true
|
||||||
|
# postprocessors
|
||||||
|
postprocessors = [
|
||||||
|
# { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
|
||||||
|
]
|
||||||
|
# render body even when there are no releases to process
|
||||||
|
# render_always = true
|
||||||
|
# output file path
|
||||||
|
# output = "test.md"
|
||||||
|
|
||||||
|
[git]
|
||||||
|
# parse the commits based on https://www.conventionalcommits.org
|
||||||
|
conventional_commits = true
|
||||||
|
# filter out the commits that are not conventional
|
||||||
|
filter_unconventional = true
|
||||||
|
# process each line of a commit as an individual commit
|
||||||
|
split_commits = false
|
||||||
|
# regex for preprocessing the commit messages
|
||||||
|
commit_preprocessors = [
|
||||||
|
# Replace issue numbers
|
||||||
|
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
|
||||||
|
# Check spelling of the commit with https://github.com/crate-ci/typos
|
||||||
|
# If the spelling is incorrect, it will be automatically fixed.
|
||||||
|
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
|
||||||
|
]
|
||||||
|
# regex for parsing and grouping commits
|
||||||
|
commit_parsers = [
|
||||||
|
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
|
||||||
|
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
||||||
|
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
|
||||||
|
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
||||||
|
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
||||||
|
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
||||||
|
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
||||||
|
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||||
|
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
|
||||||
|
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
|
||||||
|
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
|
||||||
|
]
|
||||||
|
# filter out the commits that are not matched by commit parsers
|
||||||
|
filter_commits = false
|
||||||
|
# sort the tags topologically
|
||||||
|
topo_order = false
|
||||||
|
# sort the commits inside sections by oldest/newest order
|
||||||
|
sort_commits = "oldest"
|
||||||
|
|
||||||
|
[bump]
|
||||||
|
# Before 1.0.0, a breaking change bumps the minor version instead of the major.
|
||||||
|
breaking_always_bump_major = false
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
// UserAdded is the event sent when a new user is added to a company
|
// UserAdded is the event sent when a new user is added to a company.
|
||||||
|
// SequenceNo is authz-service's global event sequence number; it orders the events.
|
||||||
type UserAdded struct {
|
type UserAdded struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
CompanyID string `json:"companyId"`
|
CompanyID string `json:"companyId"`
|
||||||
|
SequenceNo int `json:"sequenceNo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserRemoved is the event sent when a user is removed from a company
|
// UserRemoved is the event sent when a user is removed from a company
|
||||||
type UserRemoved struct {
|
type UserRemoved struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
CompanyID string `json:"companyId"`
|
CompanyID string `json:"companyId"`
|
||||||
|
SequenceNo int `json:"sequenceNo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Privilege is an enumeration of all available privileges
|
// Privilege is an enumeration of all available privileges
|
||||||
@@ -23,6 +26,7 @@ const (
|
|||||||
PrivilegeInvoicing = "INVOICING"
|
PrivilegeInvoicing = "INVOICING"
|
||||||
PrivilegeAccounting = "ACCOUNTING"
|
PrivilegeAccounting = "ACCOUNTING"
|
||||||
PrivilegeSupplier = "SUPPLIER"
|
PrivilegeSupplier = "SUPPLIER"
|
||||||
|
PrivilegeSalary = "SALARY"
|
||||||
)
|
)
|
||||||
|
|
||||||
var AllPrivilege = []Privilege{
|
var AllPrivilege = []Privilege{
|
||||||
@@ -33,11 +37,12 @@ var AllPrivilege = []Privilege{
|
|||||||
PrivilegeInvoicing,
|
PrivilegeInvoicing,
|
||||||
PrivilegeAccounting,
|
PrivilegeAccounting,
|
||||||
PrivilegeSupplier,
|
PrivilegeSupplier,
|
||||||
|
PrivilegeSalary,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Privilege) IsValid() bool {
|
func (e Privilege) IsValid() bool {
|
||||||
switch e {
|
switch e {
|
||||||
case PrivilegeAdmin, PrivilegeCompany, PrivilegeConsumer, PrivilegeTime, PrivilegeInvoicing, PrivilegeAccounting, PrivilegeSupplier:
|
case PrivilegeAdmin, PrivilegeCompany, PrivilegeConsumer, PrivilegeTime, PrivilegeInvoicing, PrivilegeAccounting, PrivilegeSupplier, PrivilegeSalary:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -52,6 +57,7 @@ type PrivilegeAdded struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
CompanyID string `json:"companyId"`
|
CompanyID string `json:"companyId"`
|
||||||
Privilege Privilege `json:"privilege"`
|
Privilege Privilege `json:"privilege"`
|
||||||
|
SequenceNo int `json:"sequenceNo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrivilegeRemoved is the event sent when a privilege is removed
|
// PrivilegeRemoved is the event sent when a privilege is removed
|
||||||
@@ -59,4 +65,5 @@ type PrivilegeRemoved struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
CompanyID string `json:"companyId"`
|
CompanyID string `json:"companyId"`
|
||||||
Privilege Privilege `json:"privilege"`
|
Privilege Privilege `json:"privilege"`
|
||||||
|
SequenceNo int `json:"sequenceNo"`
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,31 @@
|
|||||||
module gitlab.com/unboundsoftware/shiny/authz_client
|
module gitea.unbound.se/shiny/authz_client
|
||||||
|
|
||||||
go 1.13
|
go 1.26.2
|
||||||
|
|
||||||
require github.com/stretchr/testify v1.4.0
|
require (
|
||||||
|
codeberg.org/messaging/go-messaging-amqp v0.0.6
|
||||||
|
codeberg.org/messaging/messaging v0.0.5
|
||||||
|
github.com/stretchr/testify v1.12.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/common v0.66.1 // indirect
|
||||||
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
|
github.com/rabbitmq/amqp091-go v1.15.0 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||||
|
golang.org/x/sys v0.45.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.8 // indirect
|
||||||
|
)
|
||||||
@@ -1,11 +1,72 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
codeberg.org/messaging/go-messaging-amqp v0.0.6 h1:TAPcIspjjg44MNXwbcscW+ol1kIX49lnX2N3dsh0e0s=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
codeberg.org/messaging/go-messaging-amqp v0.0.6/go.mod h1:R4Hg1w964P7OP7vQ9AsXdd4KjgN/YlQ/9+XzXKpEVp4=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
codeberg.org/messaging/messaging v0.0.5 h1:/ueH90F4RNPUeeJIwdG9oVhDW7+XjCzwOYq8xc0kfQk=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
codeberg.org/messaging/messaging v0.0.5/go.mod h1:xyWLUcfaVzcN6GWk9uaQsxPVUC2Vm2S89mYZGdiWTWg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
codeberg.org/messaging/messaging/tck v0.0.3 h1:a4Nr7ytFqEJloTOyVeAtMNgO9Fig1ZUirjW4FVlK0lc=
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
codeberg.org/messaging/messaging/tck v0.0.3/go.mod h1:RiOsKXGAhNQK2STBVYGjhN0CFtmcrsEne1qUe15n8Q4=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||||
|
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc=
|
||||||
|
github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno=
|
||||||
|
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
|
||||||
|
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
|
||||||
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
|
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||||
|
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||||
|
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||||
|
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||||
|
github.com/rabbitmq/amqp091-go v1.15.0 h1:LEQL4/yp48/Wigt6A6XOu18RQRo8ZHtB5I/KZJn+gkw=
|
||||||
|
github.com/rabbitmq/amqp091-go v1.15.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||||
|
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||||
|
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||||
|
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||||
|
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||||
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||||
|
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended"
|
||||||
|
],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"groupName": "OpenTelemetry",
|
||||||
|
"matchPackageNames": [
|
||||||
|
"go.opentelemetry.io/**",
|
||||||
|
"gitea.unbound.se/shiny/otelsetup",
|
||||||
|
"gitea.unbound.se/shiny/logging"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in new issue
Block a user