Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
@@ -0,0 +1,30 @@
|
||||
name: authz_client
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- name: Run tests
|
||||
run: go test -race -coverprofile=coverage.txt ./...
|
||||
|
||||
vulnerabilities:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
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@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: stable
|
||||
- uses: actions/setup-python@v5
|
||||
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
|
||||
.claude
|
||||
/release
|
||||
coverage.txt
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ include:
|
||||
- project: unboundsoftware/ci-templates
|
||||
file: Pre-Commit-Go.gitlab-ci.yml
|
||||
|
||||
image: amd64/golang:1.25.3@sha256:69d10098be2e990bb1d987daec0e36d18ad287e139450dc7d98a0ded3498888d
|
||||
image: amd64/golang:1.25.5@sha256:ad03ba93327b8a6143b49373790b5d92c28067bdb814418509466122ee9c9e63
|
||||
|
||||
stages:
|
||||
- deps
|
||||
@@ -32,7 +32,7 @@ test:
|
||||
|
||||
vulnerabilities:
|
||||
stage: test
|
||||
image: amd64/golang:1.25.3@sha256:69d10098be2e990bb1d987daec0e36d18ad287e139450dc7d98a0ded3498888d
|
||||
image: amd64/golang:1.25.5@sha256:ad03ba93327b8a6143b49373790b5d92c28067bdb814418509466122ee9c9e63
|
||||
script:
|
||||
- go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
- govulncheck ./...
|
||||
|
||||
+3
-10
@@ -10,13 +10,6 @@ repos:
|
||||
args:
|
||||
- --allow-multiple-documents
|
||||
- id: check-added-large-files
|
||||
- repo: https://gitlab.com/devopshq/gitlab-ci-linter
|
||||
rev: v1.0.6
|
||||
hooks:
|
||||
- id: gitlab-ci-linter
|
||||
args:
|
||||
- --project
|
||||
- unboundsoftware/shiny/authz_client
|
||||
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
|
||||
rev: v9.23.0
|
||||
hooks:
|
||||
@@ -30,17 +23,17 @@ repos:
|
||||
- id: go-imports
|
||||
args:
|
||||
- -local
|
||||
- gitlab.com/unboundsoftware/shiny/authz_client
|
||||
- git.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.6.0
|
||||
rev: v2.8.0
|
||||
hooks:
|
||||
- id: golangci-lint-full
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.28.0
|
||||
rev: v8.30.0
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
|
||||
@@ -2,6 +2,47 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [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
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# 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 "git.unbound.se/shiny/authz_client"
|
||||
|
||||
// Create handler with options
|
||||
handler := client.New(client.WithBaseURL("http://authz-service"))
|
||||
|
||||
// Check user privileges
|
||||
privileges := handler.Get(email, companyID)
|
||||
if privileges.Invoicing {
|
||||
// User has invoicing privileges
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
Implements `goamqp` message handlers to receive privilege update events from the authz-service, keeping the local privilege cache up-to-date.
|
||||
@@ -1,4 +1 @@
|
||||
# Shiny authz-client
|
||||
|
||||
[](https://gitlab.com/unboundsoftware/shiny/authz_client/commits/main)
|
||||
[](https://codecov.io/gl/unboundsoftware:shiny/authz_client)
|
||||
|
||||
+283
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/sparetimecoders/goamqp"
|
||||
@@ -332,3 +333,285 @@ func TestPrivilegeHandler_Fetch_Valid(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, expectedPrivileges, handler.privileges)
|
||||
}
|
||||
|
||||
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, handler.privileges)
|
||||
}
|
||||
|
||||
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{
|
||||
Email: "jim@example.org",
|
||||
CompanyID: id,
|
||||
Privilege: PrivilegeAdmin,
|
||||
}, goamqp.Headers{})
|
||||
}(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{
|
||||
Email: "jane@example.org",
|
||||
CompanyID: fmt.Sprintf("company-%d", idx%5),
|
||||
Privilege: PrivilegeCompany,
|
||||
}, goamqp.Headers{})
|
||||
}(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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user