From fa36d551634573c971ca6ae79faef379b699d606 Mon Sep 17 00:00:00 2001 From: William Petit Date: Mon, 13 Mar 2023 10:44:58 +0100 Subject: [PATCH] feat: basic authorization model --- Makefile | 2 +- go.mod | 17 +- go.sum | 28 ++++ internal/agent/agent.go | 1 - internal/agent/controller/app/controller.go | 110 +++++++------ internal/agent/controller/app/server.go | 60 +++++-- internal/agent/controller/spec/controller.go | 13 +- internal/auth/middleware.go | 4 +- .../{user => thirdparty}/authenticator.go | 2 +- internal/auth/{user => thirdparty}/jwt.go | 2 +- internal/auth/{user => thirdparty}/user.go | 2 +- internal/client/delete_agent.go | 27 +++ internal/client/delete_agent_spec.go | 34 ++++ internal/command/api/agent/delete.go | 56 +++++++ internal/command/api/agent/root.go | 1 + internal/command/api/agent/spec/delete.go | 67 ++++++++ internal/command/api/agent/spec/root.go | 1 + internal/command/api/agent/spec/update.go | 4 +- internal/command/api/flag/flag.go | 4 +- internal/command/server/auth/create_token.go | 10 +- internal/datastore/sqlite/agent_repository.go | 16 +- internal/server/agent_api.go | 35 +++- internal/server/authorization.go | 155 ++++++++++++++++++ internal/server/server.go | 18 +- internal/spec/app/spec.go | 4 +- internal/spec/compare.go | 28 ++++ internal/spec/gateway/spec.go | 1 + internal/spec/uci/spec.go | 1 + 28 files changed, 589 insertions(+), 114 deletions(-) rename internal/auth/{user => thirdparty}/authenticator.go (98%) rename internal/auth/{user => thirdparty}/jwt.go (98%) rename internal/auth/{user => thirdparty}/user.go (95%) create mode 100644 internal/client/delete_agent.go create mode 100644 internal/client/delete_agent_spec.go create mode 100644 internal/command/api/agent/delete.go create mode 100644 internal/command/api/agent/spec/delete.go create mode 100644 internal/server/authorization.go create mode 100644 internal/spec/compare.go diff --git a/Makefile b/Makefile index 2c61cab..11597f5 100644 --- a/Makefile +++ b/Makefile @@ -140,4 +140,4 @@ tools/gitea-release/bin/gitea-release.sh: chmod +x tools/gitea-release/bin/gitea-release.sh .emissary-token: - $(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server auth create-token > .emissary-token" \ No newline at end of file + $(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server auth create-token --role writer > .emissary-token" \ No newline at end of file diff --git a/go.mod b/go.mod index e4ebd4d..18c6a00 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module forge.cadoles.com/Cadoles/emissary go 1.19 require ( - forge.cadoles.com/arcad/edge v0.0.0-20230303153719-6399196fe5c9 + forge.cadoles.com/arcad/edge v0.0.0-20230310133312-fd12d2ba42fa github.com/alecthomas/participle/v2 v2.0.0-beta.5 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 github.com/btcsuite/btcd/btcutil v1.1.3 @@ -29,18 +29,21 @@ require ( require ( github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692 // indirect - github.com/dop251/goja v0.0.0-20230226152633-7c93113e17ac // indirect + github.com/dop251/goja v0.0.0-20230304130813-e2f543bf4b4c // indirect github.com/dop251/goja_nodejs v0.0.0-20230226152057-060fa99b809f // indirect github.com/gabriel-vasile/mimetype v1.4.1 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/google/pprof v0.0.0-20230309165930-d61513b1440d // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/mdns v1.0.5 // indirect github.com/igm/sockjs-go/v3 v3.0.2 // indirect github.com/miekg/dns v1.1.51 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/oklog/ulid/v2 v2.1.0 // indirect github.com/orcaman/concurrent-map v1.0.0 // indirect - golang.org/x/net v0.7.0 // indirect + golang.org/x/net v0.8.0 // indirect google.golang.org/genproto v0.0.0-20220728213248-dd149ef739b9 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) @@ -84,12 +87,12 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.10.0 // indirect - golang.org/x/crypto v0.6.0 // indirect + golang.org/x/crypto v0.7.0 // indirect golang.org/x/mod v0.8.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/term v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect golang.org/x/tools v0.6.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/protobuf v1.28.1 // indirect diff --git a/go.sum b/go.sum index f6f1465..7d5af6e 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= forge.cadoles.com/arcad/edge v0.0.0-20230303153719-6399196fe5c9 h1:dleaaf/rV2GWtGvrPunRakjrKVDfXoANxAy8/1ctMVs= forge.cadoles.com/arcad/edge v0.0.0-20230303153719-6399196fe5c9/go.mod h1:pl9EMtSLSVr4wbDgQBDjr4aizwtmwasE686dm5arYPw= +forge.cadoles.com/arcad/edge v0.0.0-20230310133312-fd12d2ba42fa h1:dk0rNaBuCx0SxWTZBodg/y655tBaRtgLcW2hDqZRdxg= +forge.cadoles.com/arcad/edge v0.0.0-20230310133312-fd12d2ba42fa/go.mod h1:pl9EMtSLSVr4wbDgQBDjr4aizwtmwasE686dm5arYPw= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= @@ -236,8 +238,11 @@ github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOo github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= @@ -445,6 +450,8 @@ github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRP github.com/dop251/goja v0.0.0-20221118162653-d4bf6fde1b86/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs= github.com/dop251/goja v0.0.0-20230226152633-7c93113e17ac h1:NGu46Adk2oPN3tinGFItahy4W9l+9uhEf03ZxbwmdVE= github.com/dop251/goja v0.0.0-20230226152633-7c93113e17ac/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs= +github.com/dop251/goja v0.0.0-20230304130813-e2f543bf4b4c h1:/utv6nmTctV6OVgfk5+O6lEMEWL+6KJy4h9NZ5fnkQQ= +github.com/dop251/goja v0.0.0-20230304130813-e2f543bf4b4c/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/dop251/goja_nodejs v0.0.0-20230226152057-060fa99b809f h1:mmnNidRg3cMfcgyeNtIBSDZgjf/85lA/2pplccwSxYg= @@ -597,6 +604,8 @@ github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-migrate/migrate/v4 v4.15.2 h1:vU+M05vs6jWHKDdmE1Ecwj0BznygFc4QsdRe2E/L7kc= @@ -688,6 +697,9 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/google/pprof v0.0.0-20230309165930-d61513b1440d h1:um9/pc7tKMINFfP1eE7Wv6PRGXlcCSJkVajF7KJw3uQ= +github.com/google/pprof v0.0.0-20230309165930-d61513b1440d/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -761,6 +773,7 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/igm/sockjs-go/v3 v3.0.2 h1:2m0k53w0DBiGozeQUIEPR6snZFmpFpYvVsGnfLPNXbE= github.com/igm/sockjs-go/v3 v3.0.2/go.mod h1:UqchsOjeagIBFHvd+RZpLaVRbCwGilEC08EDHsD1jYE= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= @@ -972,6 +985,8 @@ github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -1013,6 +1028,7 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= @@ -1375,6 +1391,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1503,6 +1521,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1659,6 +1679,7 @@ golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1667,6 +1688,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1676,6 +1699,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1685,10 +1710,13 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 68b02b1..676cd81 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -30,7 +30,6 @@ func (a *Agent) Run(ctx context.Context) error { ticker := time.NewTicker(a.interval) defer ticker.Stop() - logger.Info(ctx, "generating token") token, err := agent.GenerateToken(a.privateKey, a.thumbprint) if err != nil { return errors.WithStack(err) diff --git a/internal/agent/controller/app/controller.go b/internal/agent/controller/app/controller.go index 096244c..ad927ff 100644 --- a/internal/agent/controller/app/controller.go +++ b/internal/agent/controller/app/controller.go @@ -11,16 +11,21 @@ import ( "forge.cadoles.com/Cadoles/emissary/internal/spec/app" "forge.cadoles.com/arcad/edge/pkg/bundle" "forge.cadoles.com/arcad/edge/pkg/storage/sqlite" + "github.com/mitchellh/hashstructure/v2" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/logger" ) +type serverEntry struct { + SpecHash uint64 + Server *Server +} + type Controller struct { - currentSpecRevision int - client *http.Client - downloadDir string - dataDir string - servers map[string]*Server + client *http.Client + downloadDir string + dataDir string + servers map[string]*serverEntry } // Name implements node.Controller. @@ -34,9 +39,9 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error { if err := state.GetSpec(app.NameApp, appSpec); err != nil { if errors.Is(err, agent.ErrSpecNotFound) { - logger.Info(ctx, "could not find app spec, stopping all remaining apps") + logger.Info(ctx, "could not find app spec") - c.stopAllApps(ctx) + c.stopAllApps(ctx, appSpec) return nil } @@ -46,22 +51,20 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error { logger.Info(ctx, "retrieved spec", logger.F("spec", appSpec.SpecName()), logger.F("revision", appSpec.SpecRevision())) - if c.currentSpecRevision == appSpec.SpecRevision() { - logger.Info(ctx, "spec revision did not change, doing nothing") - - return nil - } - c.updateApps(ctx, appSpec) return nil } -func (c *Controller) stopAllApps(ctx context.Context) { - for appID, server := range c.servers { +func (c *Controller) stopAllApps(ctx context.Context, spec *app.Spec) { + if len(c.servers) > 0 { + logger.Info(ctx, "stopping all apps") + } + + for appID, entry := range c.servers { logger.Info(ctx, "stopping app", logger.F("appID", appID)) - if err := server.Stop(); err != nil { + if err := entry.Server.Stop(); err != nil { logger.Error( ctx, "error while stopping app", logger.F("appID", appID), @@ -74,17 +77,15 @@ func (c *Controller) stopAllApps(ctx context.Context) { } func (c *Controller) updateApps(ctx context.Context, spec *app.Spec) { - hadError := false - // Stop and remove obsolete apps - for appID, server := range c.servers { + for appID, entry := range c.servers { if _, exists := spec.Apps[appID]; exists { continue } logger.Info(ctx, "stopping app", logger.F("appID", appID)) - if err := server.Stop(); err != nil { + if err := entry.Server.Stop(); err != nil { logger.Error( ctx, "error while stopping app", logger.F("gatewayID", appID), @@ -92,8 +93,6 @@ func (c *Controller) updateApps(ctx context.Context, spec *app.Spec) { ) delete(c.servers, appID) - - hadError = true } } @@ -103,20 +102,17 @@ func (c *Controller) updateApps(ctx context.Context, spec *app.Spec) { if err := c.updateApp(ctx, appID, appSpec); err != nil { logger.Error(appCtx, "could not update app", logger.E(errors.WithStack(err))) - - hadError = true - continue } } - - if !hadError { - c.currentSpecRevision = spec.SpecRevision() - logger.Info(ctx, "updating current spec revision", logger.F("revision", c.currentSpecRevision)) - } } -func (c *Controller) updateApp(ctx context.Context, appID string, appSpec app.AppEntry) error { +func (c *Controller) updateApp(ctx context.Context, appID string, appSpec app.AppEntry) (err error) { + newAppSpecHash, err := hashstructure.Hash(appSpec, hashstructure.FormatV2, nil) + if err != nil { + return errors.WithStack(err) + } + bundle, sha256sum, err := c.ensureAppBundle(ctx, appID, appSpec) if err != nil { return errors.Wrap(err, "could not download app bundle") @@ -127,7 +123,9 @@ func (c *Controller) updateApp(ctx context.Context, appID string, appSpec app.Ap return errors.Wrap(err, "could not retrieve app data dir") } - server, exists := c.servers[appID] + var entry *serverEntry + + entry, exists := c.servers[appID] if !exists { logger.Info(ctx, "app currently not running") } else if sha256sum != appSpec.SHA256Sum { @@ -137,35 +135,54 @@ func (c *Controller) updateApp(ctx context.Context, appID string, appSpec app.Ap logger.F("specHash", appSpec.SHA256Sum), ) - if err := server.Stop(); err != nil { + if err := entry.Server.Stop(); err != nil { return errors.Wrap(err, "could not stop app") } - server = nil + entry = nil } - if server == nil { + if entry == nil { dbFile := filepath.Join(dataDir, appID+".sqlite") db, err := sqlite.Open(dbFile) if err != nil { return errors.Wrapf(err, "could not opend database file '%s'", dbFile) } - server = NewServer(bundle, db) - c.servers[appID] = server + entry = &serverEntry{ + Server: NewServer(bundle, db), + SpecHash: 0, + } + + c.servers[appID] = entry } - logger.Info( - ctx, "starting app", - logger.F("address", appSpec.Address), - ) + specChanged := newAppSpecHash != entry.SpecHash - if err := server.Start(ctx, appSpec.Address); err != nil { + if entry.Server.Running() && !specChanged { + return nil + } + + if specChanged && entry.SpecHash != 0 { + logger.Info( + ctx, "restarting app", + logger.F("address", appSpec.Address), + ) + } else { + logger.Info( + ctx, "starting app", + logger.F("address", appSpec.Address), + ) + } + + if err := entry.Server.Start(ctx, appSpec.Address); err != nil { delete(c.servers, appID) return errors.Wrap(err, "could not start app") } + entry.SpecHash = newAppSpecHash + return nil } @@ -275,11 +292,10 @@ func NewController(funcs ...OptionFunc) *Controller { } return &Controller{ - client: opts.Client, - downloadDir: opts.DownloadDir, - dataDir: opts.DataDir, - currentSpecRevision: -1, - servers: make(map[string]*Server), + client: opts.Client, + downloadDir: opts.DownloadDir, + dataDir: opts.DataDir, + servers: make(map[string]*serverEntry), } } diff --git a/internal/agent/controller/app/server.go b/internal/agent/controller/app/server.go index 69f9a8e..cbd84f0 100644 --- a/internal/agent/controller/app/server.go +++ b/internal/agent/controller/app/server.go @@ -4,30 +4,35 @@ import ( "context" "database/sql" "net/http" + "sync" "forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/bus" "forge.cadoles.com/arcad/edge/pkg/bus/memory" edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http" "forge.cadoles.com/arcad/edge/pkg/module" + "forge.cadoles.com/arcad/edge/pkg/module/auth" "forge.cadoles.com/arcad/edge/pkg/module/cast" "forge.cadoles.com/arcad/edge/pkg/module/net" "forge.cadoles.com/arcad/edge/pkg/storage" "forge.cadoles.com/arcad/edge/pkg/storage/sqlite" + "gitlab.com/wpetit/goweb/logger" "forge.cadoles.com/arcad/edge/pkg/bundle" + "github.com/dop251/goja" "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" "github.com/pkg/errors" ) type Server struct { - bundle bundle.Bundle - db *sql.DB - server *http.Server + bundle bundle.Bundle + db *sql.DB + server *http.Server + serverMutex sync.RWMutex } -func (s *Server) Start(ctx context.Context, addr string) error { +func (s *Server) Start(ctx context.Context, addr string) (err error) { if s.server != nil { if err := s.Stop(); err != nil { return errors.WithStack(err) @@ -58,6 +63,18 @@ func (s *Server) Start(ctx context.Context, addr string) error { } go func() { + defer func() { + if recovered := recover(); recovered != nil { + if err, ok := recovered.(error); ok { + logger.Error(ctx, err.Error(), logger.E(errors.WithStack(err))) + + return + } + + panic(recovered) + } + }() + defer func() { if err := s.Stop(); err != nil { panic(errors.WithStack(err)) @@ -69,18 +86,29 @@ func (s *Server) Start(ctx context.Context, addr string) error { } }() + s.serverMutex.Lock() s.server = server + s.serverMutex.Unlock() return nil } +func (s *Server) Running() bool { + s.serverMutex.RLock() + defer s.serverMutex.RUnlock() + + return s.server != nil +} + func (s *Server) Stop() error { if s.server == nil { return nil } defer func() { + s.serverMutex.Lock() s.server = nil + s.serverMutex.Unlock() }() if err := s.server.Close(); err != nil { @@ -100,20 +128,18 @@ func (s *Server) getAppModules(bus bus.Bus, ds storage.DocumentStore, bs storage module.RPCModuleFactory(bus), module.StoreModuleFactory(ds), module.BlobModuleFactory(bus, bs), - // module.Extends( - // auth.ModuleFactory( - // auth.WithJWT(dummyKeyFunc), - // ), - // func(o *goja.Object) { - // if err := o.Set("CLAIM_ROLE", "role"); err != nil { - // panic(errors.New("could not set 'CLAIM_ROLE' property")) - // } + module.Extends( + auth.ModuleFactory(), + func(o *goja.Object) { + if err := o.Set("CLAIM_ROLE", "role"); err != nil { + panic(errors.New("could not set 'CLAIM_ROLE' property")) + } - // if err := o.Set("CLAIM_PREFERRED_USERNAME", "preferred_username"); err != nil { - // panic(errors.New("could not set 'CLAIM_PREFERRED_USERNAME' property")) - // } - // }, - // ), + if err := o.Set("CLAIM_PREFERRED_USERNAME", "preferred_username"); err != nil { + panic(errors.New("could not set 'CLAIM_PREFERRED_USERNAME' property")) + } + }, + ), } } diff --git a/internal/agent/controller/spec/controller.go b/internal/agent/controller/spec/controller.go index 583afa3..6b2a3d1 100644 --- a/internal/agent/controller/spec/controller.go +++ b/internal/agent/controller/spec/controller.go @@ -21,22 +21,15 @@ func (c *Controller) Name() string { func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error { cl := agent.Client(ctx) - agents, _, err := cl.QueryAgents( + agent, err := cl.GetAgent( ctx, - client.WithQueryAgentsLimit(1), - client.WithQueryAgentsID(state.AgentID()), + state.AgentID(), ) if err != nil { return errors.WithStack(err) } - if len(agents) == 0 { - logger.Error(ctx, "could not find remote matching agent") - - return nil - } - - if err := c.reconcileAgent(ctx, cl, state, agents[0]); err != nil { + if err := c.reconcileAgent(ctx, cl, state, agent); err != nil { return errors.WithStack(err) } diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go index debbeb6..c62ebb5 100644 --- a/internal/auth/middleware.go +++ b/internal/auth/middleware.go @@ -20,8 +20,8 @@ const ( contextKeyUser contextKey = "user" ) -func CtxUser(ctx context.Context) (*User, error) { - user, ok := ctx.Value(contextKeyUser).(*User) +func CtxUser(ctx context.Context) (User, error) { + user, ok := ctx.Value(contextKeyUser).(User) if !ok { return nil, errors.Errorf("unexpected user type: expected '%T', got '%T'", new(User), ctx.Value(contextKeyUser)) } diff --git a/internal/auth/user/authenticator.go b/internal/auth/thirdparty/authenticator.go similarity index 98% rename from internal/auth/user/authenticator.go rename to internal/auth/thirdparty/authenticator.go index 5b5f781..245e983 100644 --- a/internal/auth/user/authenticator.go +++ b/internal/auth/thirdparty/authenticator.go @@ -1,4 +1,4 @@ -package user +package thirdparty import ( "context" diff --git a/internal/auth/user/jwt.go b/internal/auth/thirdparty/jwt.go similarity index 98% rename from internal/auth/user/jwt.go rename to internal/auth/thirdparty/jwt.go index b2d8c4e..2bea305 100644 --- a/internal/auth/user/jwt.go +++ b/internal/auth/thirdparty/jwt.go @@ -1,4 +1,4 @@ -package user +package thirdparty import ( "context" diff --git a/internal/auth/user/user.go b/internal/auth/thirdparty/user.go similarity index 95% rename from internal/auth/user/user.go rename to internal/auth/thirdparty/user.go index c055918..db26da6 100644 --- a/internal/auth/user/user.go +++ b/internal/auth/thirdparty/user.go @@ -1,4 +1,4 @@ -package user +package thirdparty import "forge.cadoles.com/Cadoles/emissary/internal/auth" diff --git a/internal/client/delete_agent.go b/internal/client/delete_agent.go new file mode 100644 index 0000000..2308cce --- /dev/null +++ b/internal/client/delete_agent.go @@ -0,0 +1,27 @@ +package client + +import ( + "context" + "fmt" + + "forge.cadoles.com/Cadoles/emissary/internal/datastore" + "github.com/pkg/errors" +) + +func (c *Client) DeleteAgent(ctx context.Context, agentID datastore.AgentID, funcs ...OptionFunc) (datastore.AgentID, error) { + response := withResponse[struct { + AgentID int64 `json:"agentId"` + }]() + + path := fmt.Sprintf("/api/v1/agents/%d", agentID) + + if err := c.apiDelete(ctx, path, nil, &response, funcs...); err != nil { + return 0, errors.WithStack(err) + } + + if response.Error != nil { + return 0, errors.WithStack(response.Error) + } + + return datastore.AgentID(response.Data.AgentID), nil +} diff --git a/internal/client/delete_agent_spec.go b/internal/client/delete_agent_spec.go new file mode 100644 index 0000000..179eaa8 --- /dev/null +++ b/internal/client/delete_agent_spec.go @@ -0,0 +1,34 @@ +package client + +import ( + "context" + "fmt" + + "forge.cadoles.com/Cadoles/emissary/internal/datastore" + "forge.cadoles.com/Cadoles/emissary/internal/spec" + "github.com/pkg/errors" +) + +func (c *Client) DeleteAgentSpec(ctx context.Context, agentID datastore.AgentID, name spec.Name, funcs ...OptionFunc) (spec.Name, error) { + payload := struct { + Name spec.Name `json:"name"` + }{ + Name: name, + } + + response := withResponse[struct { + Name spec.Name `json:"name"` + }]() + + path := fmt.Sprintf("/api/v1/agents/%d/specs", agentID) + + if err := c.apiDelete(ctx, path, payload, &response, funcs...); err != nil { + return "", errors.WithStack(err) + } + + if response.Error != nil { + return "", errors.WithStack(response.Error) + } + + return response.Data.Name, nil +} diff --git a/internal/command/api/agent/delete.go b/internal/command/api/agent/delete.go new file mode 100644 index 0000000..b6133eb --- /dev/null +++ b/internal/command/api/agent/delete.go @@ -0,0 +1,56 @@ +package agent + +import ( + "os" + + "forge.cadoles.com/Cadoles/emissary/internal/client" + agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag" + "forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr" + clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag" + "forge.cadoles.com/Cadoles/emissary/internal/datastore" + "forge.cadoles.com/Cadoles/emissary/internal/format" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" +) + +func DeleteCommand() *cli.Command { + return &cli.Command{ + Name: "delete", + Usage: "Delete agent", + Flags: agentFlag.WithAgentFlags(), + Action: func(ctx *cli.Context) error { + baseFlags := clientFlag.GetBaseFlags(ctx) + + token, err := clientFlag.GetToken(baseFlags) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + agentID, err := agentFlag.AssertAgentID(ctx) + if err != nil { + return errors.WithStack(err) + } + + client := client.New(baseFlags.ServerURL, client.WithToken(token)) + + agentID, err = client.DeleteAgent(ctx.Context, agentID) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + hints := format.Hints{ + OutputMode: baseFlags.OutputMode, + } + + if err := format.Write(baseFlags.Format, os.Stdout, hints, struct { + ID datastore.AgentID `json:"id"` + }{ + ID: agentID, + }); err != nil { + return errors.WithStack(err) + } + + return nil + }, + } +} diff --git a/internal/command/api/agent/root.go b/internal/command/api/agent/root.go index 8b4b5df..d238bd6 100644 --- a/internal/command/api/agent/root.go +++ b/internal/command/api/agent/root.go @@ -14,6 +14,7 @@ func Root() *cli.Command { CountCommand(), UpdateCommand(), GetCommand(), + DeleteCommand(), spec.Root(), }, } diff --git a/internal/command/api/agent/spec/delete.go b/internal/command/api/agent/spec/delete.go new file mode 100644 index 0000000..a3693f2 --- /dev/null +++ b/internal/command/api/agent/spec/delete.go @@ -0,0 +1,67 @@ +package spec + +import ( + "os" + + "forge.cadoles.com/Cadoles/emissary/internal/client" + agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag" + "forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr" + clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag" + "forge.cadoles.com/Cadoles/emissary/internal/format" + "forge.cadoles.com/Cadoles/emissary/internal/spec" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" +) + +func DeleteCommand() *cli.Command { + return &cli.Command{ + Name: "delete", + Usage: "Delete spec", + + Flags: agentFlag.WithAgentFlags( + &cli.StringFlag{ + Name: "spec-name", + Usage: "use `NAME` as specification's name", + }, + ), + Action: func(ctx *cli.Context) error { + baseFlags := clientFlag.GetBaseFlags(ctx) + + token, err := clientFlag.GetToken(baseFlags) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + agentID, err := agentFlag.AssertAgentID(ctx) + if err != nil { + return errors.WithStack(err) + } + + specName, err := assertSpecName(ctx) + if err != nil { + return errors.WithStack(err) + } + + client := client.New(baseFlags.ServerURL, client.WithToken(token)) + + specName, err = client.DeleteAgentSpec(ctx.Context, agentID, specName) + if err != nil { + return errors.WithStack(apierr.Wrap(err)) + } + + hints := format.Hints{ + OutputMode: baseFlags.OutputMode, + } + + if err := format.Write(baseFlags.Format, os.Stdout, hints, struct { + Name spec.Name `json:"name"` + }{ + Name: specName, + }); err != nil { + return errors.WithStack(err) + } + + return nil + }, + } +} diff --git a/internal/command/api/agent/spec/root.go b/internal/command/api/agent/spec/root.go index 695add5..1f0c386 100644 --- a/internal/command/api/agent/spec/root.go +++ b/internal/command/api/agent/spec/root.go @@ -11,6 +11,7 @@ func Root() *cli.Command { Subcommands: []*cli.Command{ GetCommand(), UpdateCommand(), + DeleteCommand(), }, } } diff --git a/internal/command/api/agent/spec/update.go b/internal/command/api/agent/spec/update.go index b26e467..31eef5c 100644 --- a/internal/command/api/agent/spec/update.go +++ b/internal/command/api/agent/spec/update.go @@ -27,11 +27,11 @@ func UpdateCommand() *cli.Command { Flags: agentFlag.WithAgentFlags( &cli.StringFlag{ Name: "spec-name", - Usage: "use `NAME` as spec name", + Usage: "use `NAME` as specification's name", }, &cli.StringFlag{ Name: "spec-data", - Usage: "use `DATA` as spec data, '-' to read from STDIN", + Usage: "use `DATA` as specification's data, '-' to read from STDIN", }, &cli.BoolFlag{ Name: "no-patch", diff --git a/internal/command/api/flag/flag.go b/internal/command/api/flag/flag.go index 04771bd..5480d7b 100644 --- a/internal/command/api/flag/flag.go +++ b/internal/command/api/flag/flag.go @@ -35,11 +35,11 @@ func ComposeFlags(flags ...cli.Flag) []cli.Flag { &cli.StringFlag{ Name: "token", Aliases: []string{"t"}, - Usage: "use `TOKEN` as authentification token", + Usage: "use `TOKEN` as authentication token", }, &cli.StringFlag{ Name: "token-file", - Usage: "use `TOKEN_FILE` as file containing the authentification token", + Usage: "use `TOKEN_FILE` as file containing the authentication token", Value: ".emissary-token", TakesFile: true, }, diff --git a/internal/command/server/auth/create_token.go b/internal/command/server/auth/create_token.go index ae22d5b..a9a26d6 100644 --- a/internal/command/server/auth/create_token.go +++ b/internal/command/server/auth/create_token.go @@ -3,7 +3,7 @@ package auth import ( "fmt" - "forge.cadoles.com/Cadoles/emissary/internal/auth/user" + "forge.cadoles.com/Cadoles/emissary/internal/auth/thirdparty" "forge.cadoles.com/Cadoles/emissary/internal/command/common" "forge.cadoles.com/Cadoles/emissary/internal/jwk" "github.com/lithammer/shortuuid/v4" @@ -14,12 +14,12 @@ import ( func CreateTokenCommand() *cli.Command { return &cli.Command{ Name: "create-token", - Usage: "Create a new authentification token", + Usage: "Create a new authentication token", Flags: []cli.Flag{ &cli.StringFlag{ Name: "role", - Usage: fmt.Sprintf("associate `ROLE` to the token (available: %v)", []user.Role{user.RoleReader, user.RoleWriter}), - Value: string(user.RoleReader), + Usage: fmt.Sprintf("associate `ROLE` to the token (available: %v)", []thirdparty.Role{thirdparty.RoleReader, thirdparty.RoleWriter}), + Value: string(thirdparty.RoleReader), }, &cli.StringFlag{ Name: "subject", @@ -41,7 +41,7 @@ func CreateTokenCommand() *cli.Command { return errors.WithStack(err) } - token, err := user.GenerateToken(ctx.Context, key, string(conf.Server.Issuer), subject, user.Role(role)) + token, err := thirdparty.GenerateToken(ctx.Context, key, string(conf.Server.Issuer), subject, thirdparty.Role(role)) if err != nil { return errors.WithStack(err) } diff --git a/internal/datastore/sqlite/agent_repository.go b/internal/datastore/sqlite/agent_repository.go index 96aab19..52cff7a 100644 --- a/internal/datastore/sqlite/agent_repository.go +++ b/internal/datastore/sqlite/agent_repository.go @@ -269,9 +269,21 @@ func (r *AgentRepository) Create(ctx context.Context, thumbprint string, keySet // Delete implements datastore.AgentRepository func (r *AgentRepository) Delete(ctx context.Context, id datastore.AgentID) error { - query := `DELETE FROM agents WHERE id = $1` + err := r.withTx(ctx, func(tx *sql.Tx) error { + query := `DELETE FROM agents WHERE id = $1` + _, err := r.db.ExecContext(ctx, query, id) + if err != nil { + return errors.WithStack(err) + } - _, err := r.db.ExecContext(ctx, query, id) + query = `DELETE FROM specs WHERE agent_id = $1` + _, err = r.db.ExecContext(ctx, query, id) + if err != nil { + return errors.WithStack(err) + } + + return nil + }) if err != nil { return errors.WithStack(err) } diff --git a/internal/server/agent_api.go b/internal/server/agent_api.go index bb2167f..01d2c5a 100644 --- a/internal/server/agent_api.go +++ b/internal/server/agent_api.go @@ -16,9 +16,10 @@ import ( ) const ( - ErrCodeUnknownError api.ErrorCode = "unknown-error" - ErrCodeNotFound api.ErrorCode = "not-found" - ErrInvalidSignature api.ErrorCode = "invalid-signature" + ErrCodeUnknownError api.ErrorCode = "unknown-error" + ErrCodeNotFound api.ErrorCode = "not-found" + ErrCodeInvalidSignature api.ErrorCode = "invalid-signature" + ErrCodeConflict api.ErrorCode = "conflict" ) type registerAgentRequest struct { @@ -46,6 +47,8 @@ func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) { ctx = logger.With(ctx, logger.F("agentThumbprint", registerAgentReq.Thumbprint)) + // Validate that the existing signature validates the request + validSignature, err := jwk.Verify(keySet, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata) if err != nil { logger.Error(ctx, "could not validate signature", logger.E(errors.WithStack(err))) @@ -55,8 +58,8 @@ func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) { } if !validSignature { - logger.Error(ctx, "invalid signature", logger.F("signature", registerAgentReq.Signature)) - api.ErrorResponse(w, http.StatusBadRequest, ErrInvalidSignature, nil) + logger.Error(ctx, "conflicting signature", logger.F("signature", registerAgentReq.Signature)) + api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil) return } @@ -97,6 +100,28 @@ func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) { return } + agentID := agents[0].ID + + agent, err = s.agentRepo.Get(ctx, agentID) + if err != nil { + logger.Error( + ctx, "could not retrieve agent", + logger.E(errors.WithStack(err)), logger.F("agentID", agentID), + ) + api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil) + + return + } + + validSignature, err = jwk.Verify(agent.KeySet.Set, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata) + if err != nil { + logger.Error(ctx, "could not validate signature using previous keyset", logger.E(errors.WithStack(err))) + + api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil) + + return + } + agent, err = s.agentRepo.Update( ctx, agents[0].ID, datastore.WithAgentUpdateKeySet(keySet), diff --git a/internal/server/authorization.go b/internal/server/authorization.go new file mode 100644 index 0000000..304da36 --- /dev/null +++ b/internal/server/authorization.go @@ -0,0 +1,155 @@ +package server + +import ( + "context" + "fmt" + "net/http" + + "forge.cadoles.com/Cadoles/emissary/internal/auth" + "forge.cadoles.com/Cadoles/emissary/internal/auth/agent" + "forge.cadoles.com/Cadoles/emissary/internal/auth/thirdparty" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/api" + "gitlab.com/wpetit/goweb/logger" +) + +var ErrCodeForbidden api.ErrorCode = "forbidden" + +func assertGlobalReadAccess(h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + reqUser, ok := assertRequestUser(w, r) + if !ok { + return + } + + switch user := reqUser.(type) { + case *thirdparty.User: + role := user.Role() + if role == thirdparty.RoleReader || role == thirdparty.RoleWriter { + h.ServeHTTP(w, r) + + return + } + + case *agent.User: + // Agents dont have global read access + + default: + logUnexpectedUserType(r.Context(), reqUser) + } + + forbidden(w, r) + } + + return http.HandlerFunc(fn) +} + +func assertAgentWriteAccess(h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + reqUser, ok := assertRequestUser(w, r) + if !ok { + return + } + + agentID, ok := getAgentID(w, r) + if !ok { + return + } + + switch user := reqUser.(type) { + case *thirdparty.User: + role := user.Role() + if role == thirdparty.RoleWriter { + h.ServeHTTP(w, r) + + return + } + + case *agent.User: + if user.Agent().ID == agentID { + h.ServeHTTP(w, r) + + return + } + + default: + logUnexpectedUserType(r.Context(), reqUser) + } + + forbidden(w, r) + } + + return http.HandlerFunc(fn) +} + +func assertAgentReadAccess(h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + reqUser, ok := assertRequestUser(w, r) + if !ok { + return + } + + agentID, ok := getAgentID(w, r) + if !ok { + return + } + + switch user := reqUser.(type) { + case *thirdparty.User: + role := user.Role() + if role == thirdparty.RoleReader || role == thirdparty.RoleWriter { + h.ServeHTTP(w, r) + + return + } + + case *agent.User: + if user.Agent().ID == agentID { + h.ServeHTTP(w, r) + + return + } + + default: + logUnexpectedUserType(r.Context(), reqUser) + } + + forbidden(w, r) + } + + return http.HandlerFunc(fn) +} + +func assertRequestUser(w http.ResponseWriter, r *http.Request) (auth.User, bool) { + ctx := r.Context() + user, err := auth.CtxUser(ctx) + if err != nil { + logger.Error(ctx, "could not retrieve user", logger.E(errors.WithStack(err))) + + forbidden(w, r) + + return nil, false + } + + if user == nil { + forbidden(w, r) + + return nil, false + } + + return user, true +} + +func forbidden(w http.ResponseWriter, r *http.Request) { + logger.Warn(r.Context(), "forbidden", logger.F("path", r.URL.Path)) + + api.ErrorResponse(w, http.StatusForbidden, ErrCodeForbidden, nil) +} + +func logUnexpectedUserType(ctx context.Context, user auth.User) { + logger.Error( + ctx, "unexpected user type", + logger.F("subject", user.Subject()), + logger.F("type", fmt.Sprintf("%T", user)), + ) +} diff --git a/internal/server/server.go b/internal/server/server.go index 31a3dd5..7804d26 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -9,7 +9,7 @@ import ( "forge.cadoles.com/Cadoles/emissary/internal/auth" "forge.cadoles.com/Cadoles/emissary/internal/auth/agent" - "forge.cadoles.com/Cadoles/emissary/internal/auth/user" + "forge.cadoles.com/Cadoles/emissary/internal/auth/thirdparty" "forge.cadoles.com/Cadoles/emissary/internal/config" "forge.cadoles.com/Cadoles/emissary/internal/datastore" "forge.cadoles.com/Cadoles/emissary/internal/jwk" @@ -105,19 +105,19 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e r.Group(func(r chi.Router) { r.Use(auth.Middleware( - user.NewAuthenticator(keys, string(s.conf.Issuer)), + thirdparty.NewAuthenticator(keys, string(s.conf.Issuer)), agent.NewAuthenticator(s.agentRepo), )) r.Route("/agents", func(r chi.Router) { - r.Get("/", s.queryAgents) - r.Get("/{agentID}", s.getAgent) - r.Put("/{agentID}", s.updateAgent) - r.Delete("/{agentID}", s.deleteAgent) + r.With(assertGlobalReadAccess).Get("/", s.queryAgents) + r.With(assertAgentReadAccess).Get("/{agentID}", s.getAgent) + r.With(assertAgentWriteAccess).Put("/{agentID}", s.updateAgent) + r.With(assertAgentWriteAccess).Delete("/{agentID}", s.deleteAgent) - r.Get("/{agentID}/specs", s.getAgentSpecs) - r.Post("/{agentID}/specs", s.updateSpec) - r.Delete("/{agentID}/specs", s.deleteSpec) + r.With(assertAgentReadAccess).Get("/{agentID}/specs", s.getAgentSpecs) + r.With(assertAgentWriteAccess).Post("/{agentID}/specs", s.updateSpec) + r.With(assertAgentWriteAccess).Delete("/{agentID}/specs", s.deleteSpec) }) }) }) diff --git a/internal/spec/app/spec.go b/internal/spec/app/spec.go index 7790670..4e24199 100644 --- a/internal/spec/app/spec.go +++ b/internal/spec/app/spec.go @@ -33,7 +33,9 @@ func (s *Spec) SpecData() map[string]any { } func NewSpec() *Spec { - return &Spec{} + return &Spec{ + Revision: -1, + } } var _ spec.Spec = &Spec{} diff --git a/internal/spec/compare.go b/internal/spec/compare.go new file mode 100644 index 0000000..1b613c7 --- /dev/null +++ b/internal/spec/compare.go @@ -0,0 +1,28 @@ +package spec + +import ( + "github.com/mitchellh/hashstructure/v2" + "github.com/pkg/errors" +) + +func Equals(a Spec, b Spec) (bool, error) { + if a.SpecName() != b.SpecName() { + return false, nil + } + + if a.SpecRevision() != b.SpecRevision() { + return false, nil + } + + hashA, err := hashstructure.Hash(a.SpecData(), hashstructure.FormatV2, nil) + if err != nil { + return false, errors.WithStack(err) + } + + hashB, err := hashstructure.Hash(b.SpecData(), hashstructure.FormatV2, nil) + if err != nil { + return false, errors.WithStack(err) + } + + return hashA == hashB, nil +} diff --git a/internal/spec/gateway/spec.go b/internal/spec/gateway/spec.go index 42df8b2..82651f0 100644 --- a/internal/spec/gateway/spec.go +++ b/internal/spec/gateway/spec.go @@ -32,6 +32,7 @@ func (s *Spec) SpecData() map[string]any { func NewSpec() *Spec { return &Spec{ + Revision: -1, Gateways: make(map[ID]GatewayEntry), } } diff --git a/internal/spec/uci/spec.go b/internal/spec/uci/spec.go index b1ba45f..0142137 100644 --- a/internal/spec/uci/spec.go +++ b/internal/spec/uci/spec.go @@ -35,6 +35,7 @@ func (s *Spec) SpecData() map[string]any { func NewSpec() *Spec { return &Spec{ + Revision: -1, PostImportCommands: make([]*UCIPostImportCommand, 0), } }