From 9d167fab4861eaf9ea9e151aa79521189cf5ca2c Mon Sep 17 00:00:00 2001 From: Daniel Adam Date: Thu, 8 Aug 2024 10:06:50 +0200 Subject: [PATCH] Migrate device-provisioning-service to repository --- .golangci.yml | 3 + .vscode/settings.json | 4 + Makefile | 186 +- device-provisioning-service/Makefile | 65 + device-provisioning-service/README.md | 345 ++ device-provisioning-service/cmd/main.go | 37 + .../pb/enrollmentGroup.go | 120 + .../pb/enrollmentGroup.pb.go | 876 +++++ .../pb/enrollmentGroup.proto | 87 + device-provisioning-service/pb/hub.go | 245 ++ device-provisioning-service/pb/hub.pb.go | 1460 +++++++++ device-provisioning-service/pb/hub.proto | 186 ++ .../pb/provisioningRecords.go | 286 ++ .../pb/provisioningRecords.pb.go | 2858 +++++++++++++++++ .../pb/provisioningRecords.proto | 300 ++ .../pb/service.pb.gw.go | 938 ++++++ device-provisioning-service/pb/service.proto | 129 + .../pb/service.swagger.json | 1443 +++++++++ .../pb/service_grpc.pb.go | 476 +++ .../security/oauth/clientcredentials/cache.go | 183 ++ .../oauth/clientcredentials/cache_test.go | 82 + .../oauth/clientcredentials/config.go | 60 + device-provisioning-service/service/acls.go | 177 + .../service/acls_test.go | 72 + device-provisioning-service/service/auth.go | 107 + .../service/auth_test.go | 65 + .../service/clients.go | 59 + .../service/cloudConfiguration.go | 191 ++ .../service/cloudConfiguration_test.go | 155 + device-provisioning-service/service/config.go | 416 +++ .../service/credentials.go | 231 ++ .../service/credentials_test.go | 213 ++ .../service/enrollmentGroupsCache.go | 243 ++ .../service/grpc/createEnrollmentGroup.go | 31 + .../grpc/createEnrollmentGroup_test.go | 111 + .../service/grpc/createHub.go | 33 + .../service/grpc/createHub_test.go | 76 + .../grpc/deleteEnrollmentGroup_test.go | 62 + .../service/grpc/deleteEnrollmentGroups.go | 24 + .../service/grpc/deleteHubs.go | 24 + .../service/grpc/deleteHubs_test.go | 61 + .../service/grpc/deleteProvisioningRecords.go | 28 + .../grpc/deleteProvisioningRecords_test.go | 81 + .../service/grpc/getEnrollmentGroups.go | 29 + .../service/grpc/getEnrollmentGroups_test.go | 91 + .../service/grpc/getHubs.go | 29 + .../service/grpc/getHubs_test.go | 91 + .../service/grpc/getProvisioningRecords.go | 29 + .../grpc/getProvisioningRecords_test.go | 108 + .../service/grpc/server.go | 20 + .../service/grpc/updateEnrollmentGroup.go | 67 + .../grpc/updateEnrollmentGroup_test.go | 83 + .../service/grpc/updateHub.go | 58 + .../service/grpc/updateHub_test.go | 82 + .../service/http/config.go | 34 + .../service/http/getRegistrations_test.go | 126 + .../service/http/service.go | 87 + .../service/http/uri.go | 9 + .../service/linkedHub.go | 108 + .../service/linkedHubCache.go | 280 ++ device-provisioning-service/service/log.go | 82 + .../service/memory_test.go | 138 + .../service/message.go | 14 + .../service/ownership.go | 46 + .../service/ownership_test.go | 43 + .../service/plgdTime.go | 41 + .../service/plgdTime_test.go | 41 + .../service/provisionCertificate_test.go | 519 +++ .../service/provisionDelay_test.go | 129 + .../service/provisionFail_test.go | 460 +++ .../service/provisionOwnership_test.go | 150 + .../service/provisionRecovery_test.go | 196 ++ .../service/provisionRestart_test.go | 109 + .../service/provisionRetry_test.go | 178 + .../service/provision_test.go | 556 ++++ .../service/requestHandler.go | 19 + .../service/service.go | 360 +++ .../service/service_test.go | 64 + .../service/session.go | 213 ++ .../store/enrollmentGroup.go | 14 + device-provisioning-service/store/hub.go | 7 + .../store/mongodb/bulkWriter.go | 303 ++ .../store/mongodb/config.go | 38 + .../store/mongodb/enrollmentGroups.go | 259 ++ .../store/mongodb/enrollmentGroups_test.go | 481 +++ .../store/mongodb/hubs.go | 121 + .../store/mongodb/hubs_test.go | 335 ++ .../store/mongodb/provisionedRecords.go | 118 + .../store/mongodb/provisionedRecords_test.go | 477 +++ .../store/mongodb/store.go | 117 + .../store/provisioningRecord.go | 23 + device-provisioning-service/store/store.go | 75 + .../test/onboardDpsSim.go | 289 ++ .../test/provisionHandler.go | 348 ++ device-provisioning-service/test/test.go | 457 +++ device-provisioning-service/test/util.go | 30 + device-provisioning-service/uri/uri.go | 16 + device-provisioning-service/workflow.puml | 168 + pkg/strings/strings.go | 14 + pkg/strings/strings_test.go | 44 + 100 files changed, 21213 insertions(+), 39 deletions(-) create mode 100644 device-provisioning-service/Makefile create mode 100644 device-provisioning-service/README.md create mode 100644 device-provisioning-service/cmd/main.go create mode 100644 device-provisioning-service/pb/enrollmentGroup.go create mode 100644 device-provisioning-service/pb/enrollmentGroup.pb.go create mode 100644 device-provisioning-service/pb/enrollmentGroup.proto create mode 100644 device-provisioning-service/pb/hub.go create mode 100644 device-provisioning-service/pb/hub.pb.go create mode 100644 device-provisioning-service/pb/hub.proto create mode 100644 device-provisioning-service/pb/provisioningRecords.go create mode 100644 device-provisioning-service/pb/provisioningRecords.pb.go create mode 100644 device-provisioning-service/pb/provisioningRecords.proto create mode 100644 device-provisioning-service/pb/service.pb.gw.go create mode 100644 device-provisioning-service/pb/service.proto create mode 100644 device-provisioning-service/pb/service.swagger.json create mode 100644 device-provisioning-service/pb/service_grpc.pb.go create mode 100644 device-provisioning-service/security/oauth/clientcredentials/cache.go create mode 100644 device-provisioning-service/security/oauth/clientcredentials/cache_test.go create mode 100644 device-provisioning-service/security/oauth/clientcredentials/config.go create mode 100644 device-provisioning-service/service/acls.go create mode 100644 device-provisioning-service/service/acls_test.go create mode 100644 device-provisioning-service/service/auth.go create mode 100644 device-provisioning-service/service/auth_test.go create mode 100644 device-provisioning-service/service/clients.go create mode 100644 device-provisioning-service/service/cloudConfiguration.go create mode 100644 device-provisioning-service/service/cloudConfiguration_test.go create mode 100644 device-provisioning-service/service/config.go create mode 100644 device-provisioning-service/service/credentials.go create mode 100644 device-provisioning-service/service/credentials_test.go create mode 100644 device-provisioning-service/service/enrollmentGroupsCache.go create mode 100644 device-provisioning-service/service/grpc/createEnrollmentGroup.go create mode 100644 device-provisioning-service/service/grpc/createEnrollmentGroup_test.go create mode 100644 device-provisioning-service/service/grpc/createHub.go create mode 100644 device-provisioning-service/service/grpc/createHub_test.go create mode 100644 device-provisioning-service/service/grpc/deleteEnrollmentGroup_test.go create mode 100644 device-provisioning-service/service/grpc/deleteEnrollmentGroups.go create mode 100644 device-provisioning-service/service/grpc/deleteHubs.go create mode 100644 device-provisioning-service/service/grpc/deleteHubs_test.go create mode 100644 device-provisioning-service/service/grpc/deleteProvisioningRecords.go create mode 100644 device-provisioning-service/service/grpc/deleteProvisioningRecords_test.go create mode 100644 device-provisioning-service/service/grpc/getEnrollmentGroups.go create mode 100644 device-provisioning-service/service/grpc/getEnrollmentGroups_test.go create mode 100644 device-provisioning-service/service/grpc/getHubs.go create mode 100644 device-provisioning-service/service/grpc/getHubs_test.go create mode 100644 device-provisioning-service/service/grpc/getProvisioningRecords.go create mode 100644 device-provisioning-service/service/grpc/getProvisioningRecords_test.go create mode 100644 device-provisioning-service/service/grpc/server.go create mode 100644 device-provisioning-service/service/grpc/updateEnrollmentGroup.go create mode 100644 device-provisioning-service/service/grpc/updateEnrollmentGroup_test.go create mode 100644 device-provisioning-service/service/grpc/updateHub.go create mode 100644 device-provisioning-service/service/grpc/updateHub_test.go create mode 100644 device-provisioning-service/service/http/config.go create mode 100644 device-provisioning-service/service/http/getRegistrations_test.go create mode 100644 device-provisioning-service/service/http/service.go create mode 100644 device-provisioning-service/service/http/uri.go create mode 100644 device-provisioning-service/service/linkedHub.go create mode 100644 device-provisioning-service/service/linkedHubCache.go create mode 100644 device-provisioning-service/service/log.go create mode 100644 device-provisioning-service/service/memory_test.go create mode 100644 device-provisioning-service/service/message.go create mode 100644 device-provisioning-service/service/ownership.go create mode 100644 device-provisioning-service/service/ownership_test.go create mode 100644 device-provisioning-service/service/plgdTime.go create mode 100644 device-provisioning-service/service/plgdTime_test.go create mode 100644 device-provisioning-service/service/provisionCertificate_test.go create mode 100644 device-provisioning-service/service/provisionDelay_test.go create mode 100644 device-provisioning-service/service/provisionFail_test.go create mode 100644 device-provisioning-service/service/provisionOwnership_test.go create mode 100644 device-provisioning-service/service/provisionRecovery_test.go create mode 100644 device-provisioning-service/service/provisionRestart_test.go create mode 100644 device-provisioning-service/service/provisionRetry_test.go create mode 100644 device-provisioning-service/service/provision_test.go create mode 100644 device-provisioning-service/service/requestHandler.go create mode 100644 device-provisioning-service/service/service.go create mode 100644 device-provisioning-service/service/service_test.go create mode 100644 device-provisioning-service/service/session.go create mode 100644 device-provisioning-service/store/enrollmentGroup.go create mode 100644 device-provisioning-service/store/hub.go create mode 100644 device-provisioning-service/store/mongodb/bulkWriter.go create mode 100644 device-provisioning-service/store/mongodb/config.go create mode 100644 device-provisioning-service/store/mongodb/enrollmentGroups.go create mode 100644 device-provisioning-service/store/mongodb/enrollmentGroups_test.go create mode 100644 device-provisioning-service/store/mongodb/hubs.go create mode 100644 device-provisioning-service/store/mongodb/hubs_test.go create mode 100644 device-provisioning-service/store/mongodb/provisionedRecords.go create mode 100644 device-provisioning-service/store/mongodb/provisionedRecords_test.go create mode 100644 device-provisioning-service/store/mongodb/store.go create mode 100644 device-provisioning-service/store/provisioningRecord.go create mode 100644 device-provisioning-service/store/store.go create mode 100644 device-provisioning-service/test/onboardDpsSim.go create mode 100644 device-provisioning-service/test/provisionHandler.go create mode 100644 device-provisioning-service/test/test.go create mode 100644 device-provisioning-service/test/util.go create mode 100644 device-provisioning-service/uri/uri.go create mode 100644 device-provisioning-service/workflow.puml create mode 100644 pkg/strings/strings.go create mode 100644 pkg/strings/strings_test.go diff --git a/.golangci.yml b/.golangci.yml index 058e4eecc..94e639e3b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -169,6 +169,9 @@ issues: - path: resource-aggregate/events/resourceLinks.*.go|resource-aggregate/client/sync.*.go|resource-aggregate/service/grpcApi.go|resource-aggregate/events/resource.*.go linters: - dupl + - path: device-provisioning-service/test/provisionHandler.go + linters: + - dupl # Fix found issues (if it's supported by the linter). # fix: true diff --git a/.vscode/settings.json b/.vscode/settings.json index cec7ad2e0..980a7bd52 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -39,6 +39,10 @@ "TEST_SNIPPET_SERVICE_LOG_DUMP_BODY": "false", "TEST_DATABASE": "mongoDB", "TEST_BRIDGE_DEVICE_CONFIG": "${workspaceFolder}/.tmp/bridge/config-test.yaml", + "TEST_DPS_ROOT_CA_CERT_ALT": "${workspaceFolder}/.tmp/certs/device/root_ca_alt.crt", + "TEST_DPS_ROOT_CA_KEY_ALT": "${workspaceFolder}/.tmp/certs/device/root_ca_alt.key", + "TEST_DPS_INTERMEDIATE_CA_CERT": "${workspaceFolder}/.tmp/certs/device/intermediatecacrt.pem", + "TEST_DPS_INTERMEDIATE_CA_KEY": "${workspaceFolder}/.tmp/certs/device/intermediatecakey.pem", // "TEST_LEAD_RESOURCE_TYPE_FILTER": "first", // "TEST_LEAD_RESOURCE_TYPE_REGEX_FILTER": "oic\\.wk\\..*,^/light/\\d+$", // "TEST_LEAD_RESOURCE_TYPE_USE_UUID": "true", diff --git a/Makefile b/Makefile index 874e20c1f..768966534 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ ifneq ($(BRANCH_TAG),main) endif GOPATH ?= $(shell go env GOPATH) WORKING_DIRECTORY := $(shell pwd) +CERT_PATH ?= $(WORKING_DIRECTORY)/.tmp/certs USER_ID := $(shell id -u) GROUP_ID := $(shell id -g) TEST_CHECK_RACE ?= false @@ -47,7 +48,7 @@ CERT_TOOL_SIGN_ALG ?= ECDSA-SHA256 CERT_TOOL_ELLIPTIC_CURVE ?= P256 CERT_TOOL_IMAGE = ghcr.io/plgd-dev/hub/cert-tool:vnext -SUBDIRS := bundle certificate-authority cloud2cloud-connector cloud2cloud-gateway coap-gateway grpc-gateway resource-aggregate resource-directory http-gateway identity-store snippet-service test/oauth-server tools/cert-tool +SUBDIRS := bundle certificate-authority cloud2cloud-connector cloud2cloud-gateway coap-gateway device-provisioning-service grpc-gateway resource-aggregate resource-directory http-gateway identity-store snippet-service test/oauth-server tools/cert-tool .PHONY: $(SUBDIRS) push proto/generate clean build test env mongo nats certificates hub-build http-gateway-www simulators default: build @@ -60,29 +61,37 @@ hub-test: . certificates: - mkdir -p $(WORKING_DIRECTORY)/.tmp/certs - docker run \ - --rm -v $(WORKING_DIRECTORY)/.tmp/certs:/certs \ - --user $(USER_ID):$(GROUP_ID) \ - ${CERT_TOOL_IMAGE} \ + mkdir -p $(CERT_PATH) + docker pull $(CERT_TOOL_IMAGE) + docker run --rm -v $(CERT_PATH):/certs --user $(USER_ID):$(GROUP_ID) ${CERT_TOOL_IMAGE} \ --cmd.generateRootCA --outCert=/certs/root_ca.crt --outKey=/certs/root_ca.key --cert.subject.cn=RootCA \ - --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) - docker run \ - --rm -v $(WORKING_DIRECTORY)/.tmp/certs:/certs \ - --user $(USER_ID):$(GROUP_ID) \ - ${CERT_TOOL_IMAGE} \ - --cmd.generateCertificate --outCert=/certs/http.crt --outKey=/certs/http.key --cert.subject.cn=localhost \ - --cert.san.domain=localhost --cert.san.ip=127.0.0.1 --signerCert=/certs/root_ca.crt --signerKey=/certs/root_ca.key \ - --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) - docker run \ - --rm -v $(WORKING_DIRECTORY)/.tmp/certs:/certs \ - --user $(USER_ID):$(GROUP_ID) \ - ${CERT_TOOL_IMAGE} \ + --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) \ + --cert.validFrom=2000-01-01T12:00:00Z --cert.validFor=876000h + docker run --rm -v $(CERT_PATH):/certs --user $(USER_ID):$(GROUP_ID) ${CERT_TOOL_IMAGE} \ + --cmd.generateCertificate --outCert=/certs/http.crt --outKey=/certs/http.key --cert.subject.cn=localhost \ + --cert.san.domain=localhost --cert.san.ip=127.0.0.1 --signerCert=/certs/root_ca.crt --signerKey=/certs/root_ca.key \ + --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) + docker run --rm -v $(CERT_PATH):/certs --user $(USER_ID):$(GROUP_ID) ${CERT_TOOL_IMAGE} \ --cmd.generateIdentityCertificate=$(CLOUD_SID) --outCert=/certs/coap.crt --outKey=/certs/coap.key \ - --cert.san.domain=localhost --cert.san.ip=127.0.0.1 --signerCert=/certs/root_ca.crt --signerKey=/certs/root_ca.key \ - --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) - cat $(WORKING_DIRECTORY)/.tmp/certs/http.crt > $(WORKING_DIRECTORY)/.tmp/certs/mongo.key - cat $(WORKING_DIRECTORY)/.tmp/certs/http.key >> $(WORKING_DIRECTORY)/.tmp/certs/mongo.key + --cert.san.domain=localhost --cert.san.ip=127.0.0.1 --signerCert=/certs/root_ca.crt --signerKey=/certs/root_ca.key \ + --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) + cat $(CERT_PATH)/http.crt > $(CERT_PATH)/mongo.key + cat $(CERT_PATH)/http.key >> $(CERT_PATH)/mongo.key + mkdir -p $(CERT_PATH)/device + cp $(CERT_PATH)/root_ca.crt $(CERT_PATH)/device/dpsca.pem + cp $(CERT_PATH)/root_ca.key $(CERT_PATH)/device/dpscakey.pem + docker run --rm -v $(CERT_PATH)/device:/certs --user $(USER_ID):$(GROUP_ID) ${CERT_TOOL_IMAGE} \ + --signerCert=/certs/dpsca.pem --signerKey=/certs/dpscakey.pem --outCert=/certs/intermediatecacrt.pem --outKey=/certs/intermediatecakey.pem \ + --cert.basicConstraints.maxPathLen=0 --cert.subject.cn="intermediateCA" --cmd.generateIntermediateCA \ + --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) + docker run --rm -v $(CERT_PATH)/device:/certs --user $(USER_ID):$(GROUP_ID) ${CERT_TOOL_IMAGE} \ + --signerCert=/certs/intermediatecacrt.pem --signerKey=/certs/intermediatecakey.pem --outCert=/certs/mfgcrt.pem --outKey=/certs/mfgkey.pem \ + --cert.san.domain=localhost --cert.san.ip=127.0.0.1 --cert.subject.cn="mfg" --cmd.generateCertificate \ + --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) + docker run --rm -v $(CERT_PATH)/device:/certs --user $(USER_ID):$(GROUP_ID) ${CERT_TOOL_IMAGE} \ + --cmd.generateRootCA --outCert=/certs/root_ca_alt.crt --outKey=/certs/root_ca_alt.key --cert.subject.cn=RootCA \ + --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) \ + --cert.validFrom=2000-01-01T12:00:00Z --cert.validFor=876000h privateKeys: mkdir -p $(WORKING_DIRECTORY)/.tmp/privKeys @@ -97,7 +106,7 @@ nats: certificates -d \ --network=host \ --name=nats \ - -v $(WORKING_DIRECTORY)/.tmp/certs:/certs \ + -v $(CERT_PATH):/certs \ -v $(WORKING_DIRECTORY)/.tmp/jetstream/cloud:/data \ --user $(USER_ID):$(GROUP_ID) \ nats --jetstream --store_dir /data --tls --tlsverify --tlscert=/certs/http.crt --tlskey=/certs/http.key --tlscacert=/certs/root_ca.crt @@ -105,7 +114,7 @@ nats: certificates -d \ --network=host \ --name=nats-cloud-connector \ - -v $(WORKING_DIRECTORY)/.tmp/certs:/certs \ + -v $(CERT_PATH):/certs \ -v $(WORKING_DIRECTORY)/.tmp/jetstream/cloud-connector:/data \ --user $(USER_ID):$(GROUP_ID) \ nats --jetstream --store_dir /data --port 34222 --tls --tlsverify --tlscert=/certs/http.crt --tlskey=/certs/http.key --tlscacert=/certs/root_ca.crt @@ -150,12 +159,12 @@ scylla: scylla/clean --name=scylla \ -v $(WORKING_DIRECTORY)/.tmp/scylla/etc/scylla.yaml:/etc/scylla/scylla.yaml \ -v $(WORKING_DIRECTORY)/.tmp/scylla:/var/lib/scylla \ - -v $(WORKING_DIRECTORY)/.tmp/certs:/certs \ + -v $(CERT_PATH):/certs \ scylladb/scylla --developer-mode 1 --listen-address 127.0.0.1 while true; do \ i=$$((i+1)); \ - if openssl s_client -connect 127.0.0.1:9142 -cert $(WORKING_DIRECTORY)/.tmp/certs/http.crt -key $(WORKING_DIRECTORY)/.tmp/certs/http.key <<< "Q" 2>/dev/null > /dev/null; then \ + if openssl s_client -connect 127.0.0.1:9142 -cert $(CERT_PATH)/http.crt -key $(CERT_PATH)/http.key <<< "Q" 2>/dev/null > /dev/null; then \ break; \ fi; \ echo "Try to reconnect to scylla(127.0.0.1:9142) $$i"; \ @@ -164,15 +173,55 @@ scylla: scylla/clean .PHONY: scylla -mongo: certificates - mkdir -p $(WORKING_DIRECTORY)/.tmp/mongo +# Pull latest mongo and start its in replica set +# +# Parameters: +# $(1): name, used for: +# - name of working directory for the device simulator (.tmp/$(1)) +# - name of the docker container +# $(2): listen port +define RUN-DOCKER-MONGO + mkdir -p $(WORKING_DIRECTORY)/.tmp/$(1) ; \ docker run \ -d \ --network=host \ - --name=mongo \ - -v $(WORKING_DIRECTORY)/.tmp/mongo:/data/db \ - -v $(WORKING_DIRECTORY)/.tmp/certs:/certs --user $(USER_ID):$(GROUP_ID) \ - mongo --tlsMode requireTLS --wiredTigerCacheSizeGB 1 --tlsCAFile /certs/root_ca.crt --tlsCertificateKeyFile /certs/mongo.key + --name=$(1) \ + -v $(WORKING_DIRECTORY)/.tmp/$(1):/data/db \ + -v $(CERT_PATH):/certs --user $(USER_ID):$(GROUP_ID) \ + mongo mongod -vvvvv --tlsMode requireTLS --wiredTigerCacheSizeGB 1 --tlsCAFile /certs/root_ca.crt --tlsCertificateKeyFile /certs/mongo.key \ + --replSet myReplicaSet --bind_ip localhost --port $(2) +endef + +MONGODB_REPLICA_0 := mongo0 +MONGODB_REPLICA_0_PORT := 27017 +MONGODB_REPLICA_1 := mongo1 +MONGODB_REPLICA_1_PORT := 27018 +MONGODB_REPLICA_2 := mongo2 +MONGODB_REPLICA_2_PORT := 27019 + +mongo: certificates + $(call RUN-DOCKER-MONGO,$(MONGODB_REPLICA_0),$(MONGODB_REPLICA_0_PORT)) + $(call RUN-DOCKER-MONGO,$(MONGODB_REPLICA_1),$(MONGODB_REPLICA_1_PORT)) + $(call RUN-DOCKER-MONGO,$(MONGODB_REPLICA_2),$(MONGODB_REPLICA_2_PORT)) + COUNTER=0; \ + while [[ $${COUNTER} -lt 30 ]]; do \ + echo "Checking mongodb connection ($${COUNTER}):"; \ + if docker exec $(MONGODB_REPLICA_0) mongosh --quiet --tls --tlsCAFile /certs/root_ca.crt \ + --tlsCertificateKeyFile /certs/mongo.key --eval "db.adminCommand('ping')"; then \ + break; \ + fi ; \ + sleep 1; \ + let COUNTER+=1; \ + done; \ + docker exec $(MONGODB_REPLICA_0) mongosh --tls --tlsCAFile /certs/root_ca.crt --tlsCertificateKeyFile /certs/mongo.key \ + --eval "rs.initiate({ \ + _id: \"myReplicaSet\", \ + members: [ \ + {_id: 0, host: \"localhost:$(MONGODB_REPLICA_0_PORT)\"}, \ + {_id: 1, host: \"localhost:$(MONGODB_REPLICA_1_PORT)\"}, \ + {_id: 2, host: \"localhost:$(MONGODB_REPLICA_2_PORT)\"} \ + ] \ + })" http-gateway-www: @mkdir -p $(WORKING_DIRECTORY)/.tmp/usr/local/www @@ -194,6 +243,7 @@ DEVICE_SIMULATOR_RES_OBSERVABLE_IMG := ghcr.io/iotivity/iotivity-lite/cloud-serv # - name of working directory for the device simulator (.tmp/$(1)) # - name of the docker container # - name of the simulator ("$(1)-$(SIMULATOR_NAME_SUFFIX)") +# $(2): docker image define RUN-DOCKER-DEVICE mkdir -p "$(WORKING_DIRECTORY)/.tmp/$(1)" ; \ mkdir -p "$(WORKING_DIRECTORY)/.tmp/$(1)/cloud_server_creds" ; \ @@ -210,12 +260,12 @@ define RUN-DOCKER-DEVICE endef define CLEAN-DOCKER-DEVICE - sudo rm -rf $(WORKING_DIRECTORY)/.tmp/$(1) || true + sudo rm -rf $(WORKING_DIRECTORY)/.tmp/$(1) || : endef define REMOVE-DOCKER-DEVICE - docker stop --time 300 $(1) || true - docker rm -f $(1) || true + docker stop --time 300 $(1) || : + docker rm -f $(1) || : endef simulators/remove: @@ -272,7 +322,7 @@ define RUN-BRIDGE-DOCKER-DEVICE -d \ --name=$(BRIDGE_DEVICE_NAME) \ --network=host \ - -v $(WORKING_DIRECTORY)/.tmp/certs:/certs \ + -v $(CERT_PATH):/certs \ -v $(WORKING_DIRECTORY)/.tmp/bridge:/bridge \ $(BRIDGE_DEVICE_IMAGE) -config /bridge/config-docker.yaml endef @@ -291,6 +341,55 @@ simulators/bridge/clean: simulators: simulators/bridge simulators/clean: simulators/bridge/clean +# device provisioning service +DPS_ENDPOINT ?= coaps+tcp://127.0.0.1:20030 +DPS_DEVICE_LOG_LEVEL ?= debug +DPS_DEVICE_OC_LOG_LEVEL ?= info +DPS_DEVICE_SIMULATOR_OBT_NAME := dps-devsim-obt +DPS_DEVICE_SIMULATOR_NAME := dps-devsim +# TODO: switch to iotivity-lite repository +DPS_DEVICE_SIMULATOR_IMG := ghcr.io/plgd-dev/device-provisioning-client/dps-cloud-server-debug:main + +# Pull latest DPS device simulator with given name and run it +# +# Parameters: +# $(1): name, used for: +# - name of working directory for the device simulator (.tmp/$(1)) +# - name of the docker container +# - name of the simulator ("$(1)-$(SIMULATOR_NAME_SUFFIX)") +# $(2): docker image +# $(3): endpoint +define RUN-DPS-DOCKER-DEVICE + mkdir -p "$(WORKING_DIRECTORY)/.tmp/$(1)" ; \ + docker pull $(2) ; \ + docker run \ + -d \ + --privileged \ + --name=$(1) \ + --network=host \ + -v $(WORKING_DIRECTORY)/.tmp/$(1):/tmp \ + -v $(CERT_PATH)/device:/dps/bin/pki_certs \ + $(2) \ + $(1)-$(SIMULATOR_NAME_SUFFIX) --create-conf-resource --cloud-observer-max-retry 10 --expiration-limit 10 --retry-configuration 5 \ + --log-level $(DPS_DEVICE_LOG_LEVEL) --oc-log-level $(DPS_DEVICE_OC_LOG_LEVEL) $(3) +endef + + +simulators/dps/clean: + $(call REMOVE-DOCKER-DEVICE,$(DPS_DEVICE_SIMULATOR_NAME)) + $(call CLEAN-DOCKER-DEVICE,$(DPS_DEVICE_SIMULATOR_NAME)) + $(call REMOVE-DOCKER-DEVICE,$(DPS_DEVICE_SIMULATOR_OBT_NAME)) + $(call CLEAN-DOCKER-DEVICE,$(DPS_DEVICE_SIMULATOR_OBT_NAME)) +.PHONY: simulators/dps/clean + +simulators/dps: simulators/dps/clean + $(call RUN-DPS-DOCKER-DEVICE,$(DPS_DEVICE_SIMULATOR_NAME),$(DPS_DEVICE_SIMULATOR_IMG),--wait-for-reset $(DPS_ENDPOINT)) + $(call RUN-DPS-DOCKER-DEVICE,$(DPS_DEVICE_SIMULATOR_OBT_NAME),$(DPS_DEVICE_SIMULATOR_IMG),"") +.PHONY: simulators/dps + +simulators/clean: simulators/dps/clean +simulators: simulators/dps + env/test/mem: clean certificates nats mongo privateKeys scylla .PHONY: env/test/mem @@ -301,22 +400,26 @@ define RUN-DOCKER docker run \ --rm \ --network=host \ + -v $(CERT_PATH):/certs \ -v $(WORKING_DIRECTORY)/.tmp/bridge:/bridge \ - -v $(WORKING_DIRECTORY)/.tmp/certs:/certs \ -v $(WORKING_DIRECTORY)/.tmp/coverage:/coverage \ -v $(WORKING_DIRECTORY)/.tmp/report:/report \ -v $(WORKING_DIRECTORY)/.tmp/privKeys:/privKeys \ -v $(WORKING_DIRECTORY)/.tmp/usr/local/www:/usr/local/www \ -v /var/run/docker.sock:/var/run/docker.sock \ + -e TEST_CLOUD_SID=$(CLOUD_SID) \ -e LISTEN_FILE_CA_POOL=/certs/root_ca.crt \ -e LISTEN_FILE_CERT_DIR_PATH=/certs \ -e LISTEN_FILE_CERT_NAME=http.crt \ -e LISTEN_FILE_CERT_KEY_NAME=http.key \ -e TEST_COAP_GW_CERT_FILE=/certs/coap.crt \ -e TEST_COAP_GW_KEY_FILE=/certs/coap.key \ - -e TEST_CLOUD_SID=$(CLOUD_SID) \ -e TEST_ROOT_CA_CERT=/certs/root_ca.crt \ -e TEST_ROOT_CA_KEY=/certs/root_ca.key \ + -e TEST_DPS_ROOT_CA_CERT_ALT=/certs/device/root_ca_alt.crt \ + -e TEST_DPS_ROOT_CA_KEY_ALT=/certs/device/root_ca_alt.key \ + -e TEST_DPS_INTERMEDIATE_CA_CERT=/certs/device/intermediatecacrt.pem \ + -e TEST_DPS_INTERMEDIATE_CA_KEY=/certs/device/intermediatecakey.pem \ -e TEST_OAUTH_SERVER_ID_TOKEN_PRIVATE_KEY=/privKeys/idTokenKey.pem \ -e TEST_OAUTH_SERVER_ACCESS_TOKEN_PRIVATE_KEY=/privKeys/accessTokenKey.pem \ -e M2M_OAUTH_SERVER_PRIVATE_KEY=/privKeys/m2mAccessTokenKey.pem \ @@ -456,9 +559,14 @@ $(test-targets): %: env hub-test build: $(SUBDIRS) clean: simulators/clean scylla/clean - docker rm -f mongo || true docker rm -f nats || true docker rm -f nats-cloud-connector || true + $(call REMOVE-DOCKER-DEVICE,$(MONGODB_REPLICA_0)) + $(call CLEAN-DOCKER-DEVICE,$(MONGODB_REPLICA_0)) + $(call REMOVE-DOCKER-DEVICE,$(MONGODB_REPLICA_1)) + $(call CLEAN-DOCKER-DEVICE,$(MONGODB_REPLICA_1)) + $(call REMOVE-DOCKER-DEVICE,$(MONGODB_REPLICA_2)) + $(call CLEAN-DOCKER-DEVICE,$(MONGODB_REPLICA_2)) sudo rm -rf ./.tmp/certs || true sudo rm -rf ./.tmp/mongo || true sudo rm -rf ./.tmp/home || true diff --git a/device-provisioning-service/Makefile b/device-provisioning-service/Makefile new file mode 100644 index 000000000..4745fb62d --- /dev/null +++ b/device-provisioning-service/Makefile @@ -0,0 +1,65 @@ +SHELL = /bin/bash +SERVICE_NAME = $(notdir $(CURDIR)) +LATEST_TAG ?= vnext +BRANCH_TAG ?= $(shell git rev-parse --abbrev-ref HEAD | sed 's/[^a-zA-Z0-9]/-/g') +ifneq ($(BRANCH_TAG),main) + LATEST_TAG = $(BRANCH_TAG) +endif +VERSION_TAG ?= $(LATEST_TAG)-$(shell git rev-parse --short=7 --verify HEAD) +GOPATH ?= $(shell go env GOPATH) +WORKING_DIRECTORY := $(shell pwd) +REPOSITORY_DIRECTORY := $(shell cd .. && pwd) +BUILD_COMMIT_DATE ?= $(shell date -u +%FT%TZ --date=@`git show --format='%ct' HEAD --quiet`) +BUILD_SHORT_COMMIT ?= $(shell git show --format=%h HEAD --quiet) +BUILD_DATE ?= $(shell date -u +%FT%TZ) +BUILD_VERSION ?= $(shell git tag --sort version:refname | tail -1 | sed -e "s/^v//") + +default: build + +define build-docker-image + cd .. && \ + mkdir -p .tmp/docker/$(SERVICE_NAME) && \ + awk '{gsub("@NAME@","$(SERVICE_NAME)")} {gsub("@DIRECTORY@","$(SERVICE_NAME)")} {print}' tools/docker/Dockerfile.in > .tmp/docker/$(SERVICE_NAME)/Dockerfile && \ + docker build \ + --network=host \ + --tag ghcr.io/plgd-dev/hub/$(SERVICE_NAME):$(VERSION_TAG) \ + --tag ghcr.io/plgd-dev/hub/$(SERVICE_NAME):$(LATEST_TAG) \ + --tag ghcr.io/plgd-dev/hub/$(SERVICE_NAME):$(BRANCH_TAG) \ + --build-arg COMMIT_DATE="$(BUILD_COMMIT_DATE)" \ + --build-arg SHORT_COMMIT="$(BUILD_SHORT_COMMIT)" \ + --build-arg DATE="$(BUILD_DATE)" \ + --build-arg VERSION="$(BUILD_VERSION)" \ + --target $(1) \ + -f .tmp/docker/$(SERVICE_NAME)/Dockerfile \ + . +endef + +build-servicecontainer: + $(call build-docker-image,service) + +build: build-servicecontainer + +push: build-servicecontainer + docker push plgd/$(SERVICE_NAME):$(VERSION_TAG) + docker push plgd/$(SERVICE_NAME):$(LATEST_TAG) + +GOOGLEAPIS_PATH := $(REPOSITORY_DIRECTORY)/dependency/googleapis +GRPCGATEWAY_MODULE_PATH := $(shell go list -m -f '{{.Dir}}' github.com/grpc-ecosystem/grpc-gateway/v2 | head -1) + +proto/generate: + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src --go_out=$(GOPATH)/src $(WORKING_DIRECTORY)/pb/provisioningRecords.proto + protoc-go-inject-tag -input=$(WORKING_DIRECTORY)/pb/provisioningRecords.pb.go + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src --go_out=$(GOPATH)/src $(WORKING_DIRECTORY)/pb/enrollmentGroup.proto + protoc-go-inject-tag -input=$(WORKING_DIRECTORY)/pb/enrollmentGroup.pb.go + protoc -I=. -I=$(GOPATH)/src --go_out=$(GOPATH)/src $(WORKING_DIRECTORY)/pb/hub.proto + protoc-go-inject-tag -input=$(WORKING_DIRECTORY)/pb/hub.pb.go + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src -I=$(GOOGLEAPIS_PATH) -I=$(GRPCGATEWAY_MODULE_PATH) --go-grpc_out=$(GOPATH)/src $(WORKING_DIRECTORY)/pb/service.proto + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src -I=$(GOOGLEAPIS_PATH) -I=$(GRPCGATEWAY_MODULE_PATH) --openapiv2_out=$(GOPATH)/src \ + --openapiv2_opt logtostderr=true \ + $(WORKING_DIRECTORY)/pb/service.proto + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src -I=$(GOOGLEAPIS_PATH) -I=$(GRPCGATEWAY_MODULE_PATH) --grpc-gateway_out $(GOPATH)/src \ + --grpc-gateway_opt logtostderr=true \ + --grpc-gateway_opt paths=source_relative \ + $(WORKING_DIRECTORY)/pb/service.proto + +.PHONY: build-servicecontainer build push proto/generate diff --git a/device-provisioning-service/README.md b/device-provisioning-service/README.md new file mode 100644 index 000000000..fbbed66f3 --- /dev/null +++ b/device-provisioning-service/README.md @@ -0,0 +1,345 @@ +# Device Provisioning Service + +The Device Provisioning Service provides API to provision device to the [plgd/hub](https://github.com/plgd-dev/hub). + +## Workflow + + + +![Plant UML](./workflow.puml). + +## Docker Image + +Before you use the image you need to setup [K8s access to private registry](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry). + +```bash +docker pull ghcr.io/plgd-dev/hub/device-provisioning-service:latest +``` + +## YAML Configuration + +A configuration template is available on [config.yaml](https://github.com/plgd-dev/device-provisioning-service/blob/main/config.yaml). + +### Logging + +| Property | Type | Description | Default | +| ---------- | -------- | -------------- | ------- | +| `log.dumpBody` | bool | `Set to true if you would like to dump raw messages.` | `false` | +| `log.level` | string | `Logging enabled from level.` | `"info"` | +| `log.encoding` | string | `Logging format. The supported values are: "json", "console"` | `"json"` | +| `log.stacktrace.enabled` | bool | `Log stacktrace.` | `"false` | +| `log.stacktrace.level` | string | `Stacktrace from level.` | `"warn` | +| `log.encoderConfig.timeEncoder` | string | `Time format for logs. The supported values are: "rfc3339nano", "rfc3339".` | `"rfc3339nano` | + +### CoAP API + +CoAP API as specified in the [workflow](./workflow.puml). + +| Property | Type | Description | Default | +| ---------- | -------- | -------------- | ------- | +| `apis.coap.address` | string | `Listen specification : for coap client connection.` | `"0.0.0.0:5688"` | +| `apis.coap.protocols` | []string | `Protocol for coap connection. The supported values are: "tcp", "udp" .` | `["tcp"]` | +| `apis.coap.maxMessageSize` | int | `Max message size which can be sent/received via coap. i.e. 256*1024 = 262144 bytes.` | `262144` | +| `apis.coap.messagePoolSize` | int | `Defines the maximum preallocated messages in the pool for parse/create coap messages.` | `1000` | +| `apis.coap.inactivityMonitor.timeout` | string | `Time limit to close inactive connection.` | `20s` | +| `apis.coap.blockwiseTransfer.enabled` | bool | `If true, enable blockwise transfer of coap messages.` | `true` | +| `apis.coap.blockwiseTransfer.blockSize` | int | `Size of blockwise transfer block.` | `1024` | +| `apis.coap.tls.keyFile` | string | `File path to private key in PEM format.` | `""` | +| `apis.coap.tls.certFile` | string | `File path to certificate in PEM format.` | `""` | + +### HTTP API + +The plgd device provisioning service REST API is defined by [swagger](https://raw.githubusercontent.com/plgd-dev/device-provisioning-service/main/pb/service.swagger.json). + +| Property | Type | Description | Default | +| ---------- | -------- | -------------- | ------- | +| `apis.http.enabled` | bool | `Enable HTTP API.` | `false` | +| `apis.http.address` | string | `Listen specification : for http client connection.` | `"0.0.0.0:9100"` | +| `apis.http.tls.caPool` | string | `File path to the root certificate in PEM format which might contain multiple certificates in a single file.` | `""` | +| `apis.http.tls.keyFile` | string | `File path to private key in PEM format.` | `""` | +| `apis.http.tls.certFile` | string | `File path to certificate in PEM format.` | `""` | +| `apis.http.tls.clientCertificateRequired` | bool | `If true, require client certificate.` | `true` | +| `apis.http.authorization.authority` | string | `Authority is the address of the token-issuing authentication server. Services will use this URI to find and retrieve the public key that can be used to validate the token’s signature.` | `""` | +| `apis.http.authorization.audience` | string | `Identifier of the API configured in your OAuth provider.` | `""` | +| `apis.http.authorization.http.maxIdleConns` | int | `It controls the maximum number of idle (keep-alive) connections across all hosts. Zero means no limit.` | `16` | +| `apis.http.authorization.http.maxConnsPerHost` | int | `It optionally limits the total number of connections per host, including connections in the dialing, active, and idle states. On limit violation, dials will block. Zero means no limit.` | `32` | +| `apis.http.authorization.http.maxIdleConnsPerHost` | int | `If non-zero, controls the maximum idle (keep-alive) connections to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used.` | `16` | +| `apis.http.authorization.http.idleConnTimeout` | string | `The maximum amount of time an idle (keep-alive) connection will remain idle before closing itself. Zero means no limit.` | `30s` | +| `apis.http.authorization.http.timeout` | string | `A time limit for requests made by this Client. A Timeout of zero means no timeout.` | `10s` | +| `apis.http.authorization.http.tls.caPool` | string | `File path to the root certificate in PEM format which might contain multiple certificates in a single file.` | `""` | +| `apis.http.authorization.http.tls.keyFile` | string | `File path to private key in PEM format.` | `""` | +| `apis.http.authorization.http.tls.certFile` | string | `File path to certificate in PEM format.` | `""` | +| `apis.http.authorization.http.tls.useSystemCAPool` | bool | `If true, use system certification pool.` | `false` | +| `apis.http.readTimeout` | string | `The maximum duration for reading the entire request, including the body by the server. A zero or negative value means there will be no timeout.` | `8s` | +| `apis.http.readHeaderTimeout` | string | `The amount of time allowed to read request headers by the server. If readHeaderTimeout is zero, the value of readTimeout is used. If both are zero, there is no timeout.` | `4s` | +| `apis.http.writeTimeout` | string | `The maximum duration before the server times out writing of the response. A zero or negative value means there will be no timeout.` | `16s` | +| `apis.http.idleTimeout` | string | `The maximum amount of time the server waits for the next request when keep-alives are enabled. If idleTimeout is zero, the value of readTimeout is used. If both are zero, there is no timeout.` | `30s` | + +### Open telemetry exporter + +The plgd open telemetry exporter configuration. + +| Property | Type | Description | Default | +| ---------- | -------- | -------------- | ------- | +| `clients.openTelemetryCollector.grpc.enabled` | bool | `Enable OTLP gRPC exporter` | `false` | +| `clients.openTelemetryCollector.grpc.address` | string | `The gRPC collector to which the exporter is going to send data` | `""` | +| `clients.openTelemetryCollector.grpc.keepAlive.time` | string | `After a duration of this time if the client doesn't see any activity it pings the server to see if the transport is still alive.` | `10s` | +| `clients.openTelemetryCollector.grpc.keepAlive.timeout` | string | `After having pinged for keepalive check, the client waits for a duration of Timeout and if no activity is seen even after that the connection is closed.` | `20s` | +| `clients.openTelemetryCollector.grpc.keepAlive.permitWithoutStream` | bool | `If true, client sends keepalive pings even with no active RPCs. If false, when there are no active RPCs, Time and Timeout will be ignored and no keepalive pings will be sent.` | `true` | +| `clients.openTelemetryCollector.grpc.tls.caPool` | string | `File path to the root certificate in PEM format which might contain multiple certificates in a single file.` | `""` | +| `clients.openTelemetryCollector.grpc.tls.keyFile` | string | `File path to private key in PEM format.` | `""` | +| `clients.openTelemetryCollector.grpc.tls.certFile` | string | `File path to certificate in PEM format.` | `""` | +| `clients.openTelemetryCollector.grpc.tls.useSystemCAPool` | bool | `If true, use system certification pool.` | `false` | + +### Storage + +The plgd device provisioning service uses MongoDB database. + +| Property | Type | Description | Default | +| ---------- | -------- | -------------- | ------- | +| `clients.storage.cacheExpiration` | string | `Expiration time of cached records from database.` | `"10m"` | +| `clients.storage.mongoDB.uri` | string | `URI to mongo database.` | `"mongodb://localhost:27017"` | +| `clients.storage.mongoDB.database` | string | `Name of database` | `"deviceProvisioning"` | +| `clients.storage.mongoDB.maxPoolSize` | int | `Limits number of connections.` | `16` | +| `clients.storage.mongoDB.maxConnIdleTime` | string | `Close connection when idle time reach the value.` | `4m0s` | +| `clients.storage.mongoDB.tls.caPool` | string | `File path to the root certificate in PEM format which might contain multiple certificates in a single file.` | `""` | +| `clients.storage.mongoDB.tls.keyFile` | string | `File path to private key in PEM format.` | `""` | +| `clients.storage.mongoDB.tls.certFile` | string | `File path to certificate in PEM format.` | `""` | +| `clients.storage.mongoDB.tls.useSystemCAPool` | bool | `If true, use system certification pool.` | `false` | +| `clients.storage.mongoDB.bulkWrite.timeout` | string | `A time limit for write bulk to mongodb. A Timeout of zero means no timeout.` | `1m0s` | +| `clients.storage.mongoDB.bulkWrite.throttleTime` | string | `The amount of time to wait until a record is written to mongodb. Any records collected during the throttle time will also be written. A throttle time of zero writes immediately. If recordLimit is reached, all records are written immediately.` | `500ms` | +| `clients.storage.mongoDB.bulkWrite.documentLimit` | uint16 | `The maximum number of documents to cache before an immediate write.` | `1000` | + +### Enrollment groups + +Enrollment group entry configuration. + +| Property | Type | Description | Default | +| ---------- | -------- | -------------- | ------- | +| `enrollmentGroups.[].id` | string | `Unique enrollment group id in GUID format` | `""` | +| `enrollmentGroups.[].owner` | string | `Owner of a newly provisioned device` | `""` | +| `enrollmentGroups.[].preSharedKeyFile` | string | `File path to the pre-shared key that will be stored on the device for the owner. It must be empty or have 16 characters in the preSharedKeyFile.` | `""` | +| `enrollmentGroups.[].attestationMechanism.x509.certificateChain` | string | `File path to certificate chain in PEM format.` | `""` | +| `enrollmentGroups.[].attestationMechanism.x509.expiredCertificateEnabled` | bool | `Accept device connections with an expired certificate.` | `false` | + +#### Hub + +Defines configuration of the plgd hub where the device connects after it's successfully provisioned. + +| Property | Type | Description | Default | +| ---------- | -------- | -------------- | ------- | +| `enrollmentGroups.[].hub.caPool` | string | `File path to the root certificate in PEM format. Multiple certificates in a single file are supported.` | `""` | +| `enrollmentGroups.[].hub.hubID` | string | `Uniqhe id of the plgd hub instance.` | `""` | +| `enrollmentGroups.[].hub.coapGateway` | string | `plgd hub CoAP gateway endpoint where the devices should connect to after successful provisioning.Format .` | `""` | +| `enrollmentGroups.[].hub.certificateAuthority.grpc.address` | string | `plgd hub Certificate Authority endpoint used to sign device identity CSRs. Format .` | `""` | +| `enrollmentGroups.[].hub.certificateAuthority.grpc.keepAlive.time` | string | `After a duration of this time if the client doesn't see any activity it pings the server to see if the transport is still alive.` | `10s` | +| `enrollmentGroups.[].hub.certificateAuthority.grpc.keepAlive.timeout` | string | `After having pinged for keepalive check, the client waits for a duration of Timeout and if no activity is seen even after that the connection is closed.` | `20s` | +| `enrollmentGroups.[].hub.certificateAuthority.grpc.keepAlive.permitWithoutStream` | bool | `If true, client sends keepalive pings even with no active RPCs. If false, when there are no active RPCs, Time and Timeout will be ignored and no keepalive pings will be sent.` | `true` | +| `enrollmentGroups.[].hub.certificateAuthority.grpc.tls.caPool` | string | `File path to the root certificate in PEM format which might contain multiple certificates in a single file.` | `""` | +| `enrollmentGroups.[].hub.certificateAuthority.grpc.tls.keyFile` | string | `File path to private key in PEM format.` | `""` | +| `enrollmentGroups.[].hub.certificateAuthority.grpc.tls.certFile` | string | `File path to certificate in PEM format.` | `""` | +| `enrollmentGroups.[].hub.certificateAuthority.grpc.tls.useSystemCAPool` | bool | `If true, use system certification pool.` | `false` | + +#### OAuth2.0 Client + +OAuth2.0 Client is used to obtain JWT with ownerClaim an deviceIDClaim via the client credentials flow. The JWT will be is used directly during the [SignUp operation](https://plgd.dev/architecture/component-overview/#hub-registration). + +| Property | Type | Description | Default | +| ---------- | -------- | -------------- | ------- | +| `enrollmentGroups.[].hub.authorization.ownerClaim` | string | `Claim used to identify owner of the device. If configured, your OAuth2.0 server has to set the owner id to the token as configured. OwnerClaim with sub is not supported. Custom owner claim needs to be configured also on the plgd hub instance. If used with the plgd mock OAuth Server, value https://plgd.dev/owner has to be set. **Required.**` | `""` | +| `enrollmentGroups.[].hub.authorization.deviceIDClaim` | string | `Claim used to make JWT tokens device specific. If configured, your OAuth2.0 server has to set the device id to the token as configured. If used with the plgd mock OAuth Server, value https://plgd.dev/deviceId has to be set.` | `""` | +| `enrollmentGroups.[].hub.authorization.provider.name` | string | `Provider name which is registered also on the instance of the plgd hub where the device connects after it's successfully provisioned. The grant type for this provider must to be set to ClientCredentials.` | `""` | +| `enrollmentGroups.[].hub.authorization.provider.authority` | string | `Authority is the address of the token-issuing authentication server. Services will use this URI to find token endpoint.` | `""` | +| `enrollmentGroups.[].hub.authorization.provider.clientID` | string | `OAuth Client ID.` | `""` | +| `enrollmentGroups.[].hub.authorization.provider.clientSecretFile` | string | `File path to client secret required to request an access token.` | `""` | +| `enrollmentGroups.[].hub.authorization.provider.audience` | string | `Audience of OAuth provider.` | `""` | +| `enrollmentGroups.[].hub.authorization.provider.http.maxIdleConns` | int | `Maximum number of idle (keep-alive) connections across all hosts. Zero means no limit.` | `16` | +| `enrollmentGroups.[].hub.authorization.provider.http.maxConnsPerHost` | int | `Limit the total number of connections per host, including connections in the dialing, active, and idle states. On limit violation, dials will be blocked. Zero means no limit.` | `32` | +| `enrollmentGroups.[].hub.authorization.provider.http.maxIdleConnsPerHost` | int | `If non-zero, controls the maximum idle (keep-alive) connections to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used.` | `16` | +| `enrollmentGroups.[].hub.authorization.provider.http.idleConnTimeout` | string | `The maximum time an idle (keep-alive) connection will remain idle before closing itself. Zero means no limit.` | `30s` | +| `enrollmentGroups.[].hub.authorization.provider.http.timeout` | string | `A time limit for requests made by this Client. A Timeout of zero means no timeout.` | `10s` | +| `enrollmentGroups.[].hub.authorization.provider.http.tls.caPool` | string | `File path to the root certificate in PEM format which might contain multiple certificates in a single file.` | `""` | +| `enrollmentGroups.[].hub.authorization.provider.http.tls.keyFile` | string | `File path to private key in PEM format.` | `""` | +| `enrollmentGroups.[].hub.authorization.provider.http.tls.certFile` | string | `File path to certificate in PEM format.` | `""` | +| `enrollmentGroups.[].hub.authorization.provider.http.tls.useSystemCAPool` | bool | `If true, use system certification pool.` | `false` | + +::: tip Audience +You might have one client, but multiple APIs registered in the OAuth2.0 Server. What you might want to prevent is to be able to contact all the APIs of your system with one token. This audience allows you to request the token for a specific API. If you configure it to myplgdc2c.api in the Auth0, you have to set it here if you want to also validate it. +::: + +### Task Queue + +| Property | Type | Description | Default | +| ---------- | -------- | -------------- | ------- | +| `taskQueue.goPoolSize` | int | `Maximum number of running goroutine instances.` | `1600` | +| `taskQueue.size` | int | `Size of queue. If it exhausted, submit returns error.` | `2097152` | +| `taskQueue.maxIdleTime` | string | `Sets up the interval time of cleaning up goroutines. Zero means never cleanup.` | `10m` | + +> Note that the string type related to time (i.e. timeout, idleConnTimeout, expirationTime) is decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "1.5h" or "2h45m". Valid time units are "ns", "us", "ms", "s", "m", "h". diff --git a/device-provisioning-service/cmd/main.go b/device-provisioning-service/cmd/main.go new file mode 100644 index 000000000..22d90b4e7 --- /dev/null +++ b/device-provisioning-service/cmd/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "context" + + service "github.com/plgd-dev/hub/v2/device-provisioning-service/service" + "github.com/plgd-dev/hub/v2/pkg/config" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" +) + +func main() { + var cfg service.Config + err := config.LoadAndValidateConfig(&cfg) + if err != nil { + log.Fatalf("cannot load config: %v", err) + } + logger := log.NewLogger(cfg.Log) + log.Set(logger) + log.Infof("config: %v", cfg.String()) + + fileWatcher, err := fsnotify.NewWatcher(logger) + if err != nil { + log.Fatalf("cannot create file fileWatcher: %v", err) + } + + s, err := service.New(context.Background(), cfg, fileWatcher, logger) + if err != nil { + _ = fileWatcher.Close() + log.Fatalf("cannot create service: %v", err) + } + err = s.Serve() + _ = fileWatcher.Close() + if err != nil { + log.Fatalf("cannot serve service: %v", err) + } +} diff --git a/device-provisioning-service/pb/enrollmentGroup.go b/device-provisioning-service/pb/enrollmentGroup.go new file mode 100644 index 000000000..0a9c46da5 --- /dev/null +++ b/device-provisioning-service/pb/enrollmentGroup.go @@ -0,0 +1,120 @@ +package pb + +import ( + "crypto/x509" + "errors" + "fmt" + "sort" + + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/pkg/config/property/urischeme" + pkgX509 "github.com/plgd-dev/hub/v2/pkg/security/x509" + "github.com/plgd-dev/kit/v2/security" +) + +type EnrollmentGroups []*EnrollmentGroup + +func (p EnrollmentGroups) Sort() { + sort.Slice(p, func(i, j int) bool { + return p[i].GetId() < p[j].GetId() + }) +} + +func checkLeadCertificateNameInChains(leadCertificateName string, chains [][]*x509.Certificate) bool { + for _, chain := range chains { + for _, cert := range chain { + if cert.Subject.CommonName == leadCertificateName { + return true + } + } + } + return false +} + +func (c *EnrollmentGroup) ResolvePreSharedKey() (string, bool, error) { + if c.GetPreSharedKey() == "" { + return "", false, nil + } + data, err := urischeme.URIScheme(c.GetPreSharedKey()).Read() + if err != nil { + return "", false, err + } + if len(data) < 16 { + return "", false, fmt.Errorf("at least 16 bytes are required, but %v bytes are provided", len(data)) + } + return string(data), true, nil +} + +func (c *X509Configuration) ResolveCertificateChain() ([]*x509.Certificate, error) { + data, err := urischeme.URIScheme(c.GetCertificateChain()).Read() + if err != nil { + return nil, fmt.Errorf("cannot read certificateChain('%v') - %w", c.GetCertificateChain(), err) + } + return security.ParseX509FromPEM(data) +} + +func (c *X509Configuration) Validate() error { + if c.GetCertificateChain() == "" { + return fmt.Errorf("certificateChain('%v')", c.GetCertificateChain()) + } + chain, err := c.ResolveCertificateChain() + if err != nil { + return fmt.Errorf("certificateChain('%v') - %w", c.GetCertificateChain(), err) + } + verifiedChains, err := pkgX509.Verify(chain, chain, false, x509.VerifyOptions{}) + if err != nil { + return fmt.Errorf("certificateChain('%v') - %w", c.GetCertificateChain(), err) + } + if c.GetLeadCertificateName() == "" { + c.LeadCertificateName = chain[0].Subject.CommonName + } else if !checkLeadCertificateNameInChains(c.GetLeadCertificateName(), verifiedChains) { + return fmt.Errorf("leadCertificateName('%v') - not found in certificateChain", c.GetLeadCertificateName()) + } + + return nil +} + +func (c *AttestationMechanism) Validate() error { + if c.GetX509() == nil { + return errors.New("x509 - is empty") + } + if err := c.GetX509().Validate(); err != nil { + return fmt.Errorf("x509.%w", err) + } + return nil +} + +func (c *EnrollmentGroup) Validate(owner string) error { + if _, err := uuid.Parse(c.GetId()); err != nil { + return fmt.Errorf("id('%v') - %w", c.GetId(), err) + } + if c.GetOwner() == "" { + return fmt.Errorf("owner('%v') - is empty", c.GetOwner()) + } + if owner != "" && owner != c.GetOwner() { + return fmt.Errorf("owner('%v') - expects %v", c.GetOwner(), owner) + } + _, _, err := c.ResolvePreSharedKey() + if err != nil { + return fmt.Errorf("preSharedKey('%v') - %w", c.GetPreSharedKey(), err) + } + if c.GetAttestationMechanism() == nil { + return errors.New("attestationMechanism - is empty") + } + if err := c.GetAttestationMechanism().Validate(); err != nil { + return fmt.Errorf("attestationMechanism.%w", err) + } + if c.GetName() == "" { + // set default name as id for backward compatibility + c.Name = c.GetId() + } + if len(c.GetHubIds()) == 0 { + return errors.New("hubIds - is empty") + } + for idx, hubID := range c.GetHubIds() { + if _, err := uuid.Parse(hubID); err != nil { + return fmt.Errorf("hubIds[%v]('%v') - %w", idx, hubID, err) + } + } + return nil +} diff --git a/device-provisioning-service/pb/enrollmentGroup.pb.go b/device-provisioning-service/pb/enrollmentGroup.pb.go new file mode 100644 index 000000000..f6df688e7 --- /dev/null +++ b/device-provisioning-service/pb/enrollmentGroup.pb.go @@ -0,0 +1,876 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v5.27.3 +// source: device-provisioning-service/pb/enrollmentGroup.proto + +package pb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type X509Configuration struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // chain certficates authorities: ..<-intermediateCA1<-intermediateCA<-RootCA which is used to match enrollment group. Supported formats: , + CertificateChain string `protobuf:"bytes,1,opt,name=certificate_chain,json=certificateChain,proto3" json:"certificate_chain,omitempty" bson:"certificateChain"` // @gotags: bson:"certificateChain" + // the certificate name must be one from certificate_chain, it is used to match enrollment group. If empty, the first certificate from certificate_chain is used + LeadCertificateName string `protobuf:"bytes,2,opt,name=lead_certificate_name,json=leadCertificateName,proto3" json:"lead_certificate_name,omitempty" bson:"leadCertificateName"` // @gotags: bson:"leadCertificateName" + // dont validate time during certificate verification + ExpiredCertificateEnabled bool `protobuf:"varint,3,opt,name=expired_certificate_enabled,json=expiredCertificateEnabled,proto3" json:"expired_certificate_enabled,omitempty" bson:"expiredCertificateEnabled"` // @gotags: bson:"expiredCertificateEnabled" +} + +func (x *X509Configuration) Reset() { + *x = X509Configuration{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *X509Configuration) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*X509Configuration) ProtoMessage() {} + +func (x *X509Configuration) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use X509Configuration.ProtoReflect.Descriptor instead. +func (*X509Configuration) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_enrollmentGroup_proto_rawDescGZIP(), []int{0} +} + +func (x *X509Configuration) GetCertificateChain() string { + if x != nil { + return x.CertificateChain + } + return "" +} + +func (x *X509Configuration) GetLeadCertificateName() string { + if x != nil { + return x.LeadCertificateName + } + return "" +} + +func (x *X509Configuration) GetExpiredCertificateEnabled() bool { + if x != nil { + return x.ExpiredCertificateEnabled + } + return false +} + +type AttestationMechanism struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // X509 attestation + X509 *X509Configuration `protobuf:"bytes,1,opt,name=x509,proto3" json:"x509,omitempty" bson:"x509"` // @gotags: bson:"x509" +} + +func (x *AttestationMechanism) Reset() { + *x = AttestationMechanism{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AttestationMechanism) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttestationMechanism) ProtoMessage() {} + +func (x *AttestationMechanism) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttestationMechanism.ProtoReflect.Descriptor instead. +func (*AttestationMechanism) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_enrollmentGroup_proto_rawDescGZIP(), []int{1} +} + +func (x *AttestationMechanism) GetX509() *X509Configuration { + if x != nil { + return x.X509 + } + return nil +} + +type EnrollmentGroup struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Enrollment group ID. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" bson:"_id"` // @gotags: bson:"_id" + // HUB owner of device - used for hub authorization. + Owner string `protobuf:"bytes,2,opt,name=owner,proto3" json:"owner,omitempty" bson:"owner"` // @gotags: bson:"owner" + // Attestation mechanism + AttestationMechanism *AttestationMechanism `protobuf:"bytes,3,opt,name=attestation_mechanism,json=attestationMechanism,proto3" json:"attestation_mechanism,omitempty" bson:"attestationMechanism"` // @gotags: bson:"attestationMechanism" + // Hub configuration to configure device. + HubIds []string `protobuf:"bytes,7,rep,name=hub_ids,json=hubIds,proto3" json:"hub_ids,omitempty" bson:"hubIds"` // @gotags: bson:"hubIds" + // Pre shared key for devices in enrollment group. It can be used for maintenance operations by d2d client. Supported formats: , + PreSharedKey string `protobuf:"bytes,5,opt,name=pre_shared_key,json=preSharedKey,proto3" json:"pre_shared_key,omitempty" bson:"preSharedKey"` // @gotags: bson:"preSharedKey" + // name of enrollment group + Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty" bson:"name"` // @gotags: bson:"name" +} + +func (x *EnrollmentGroup) Reset() { + *x = EnrollmentGroup{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EnrollmentGroup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EnrollmentGroup) ProtoMessage() {} + +func (x *EnrollmentGroup) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EnrollmentGroup.ProtoReflect.Descriptor instead. +func (*EnrollmentGroup) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_enrollmentGroup_proto_rawDescGZIP(), []int{2} +} + +func (x *EnrollmentGroup) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *EnrollmentGroup) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *EnrollmentGroup) GetAttestationMechanism() *AttestationMechanism { + if x != nil { + return x.AttestationMechanism + } + return nil +} + +func (x *EnrollmentGroup) GetHubIds() []string { + if x != nil { + return x.HubIds + } + return nil +} + +func (x *EnrollmentGroup) GetPreSharedKey() string { + if x != nil { + return x.PreSharedKey + } + return "" +} + +func (x *EnrollmentGroup) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type CreateEnrollmentGroupRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Attestation mechanism + AttestationMechanism *AttestationMechanism `protobuf:"bytes,2,opt,name=attestation_mechanism,json=attestationMechanism,proto3" json:"attestation_mechanism,omitempty"` + // Hub configuration to configure device. + HubIds []string `protobuf:"bytes,6,rep,name=hub_ids,json=hubIds,proto3" json:"hub_ids,omitempty"` + // Pre shared key for devices in enrollment group. It can be used for maintenance operations by d2d client. Supported formats: , + PreSharedKey string `protobuf:"bytes,4,opt,name=pre_shared_key,json=preSharedKey,proto3" json:"pre_shared_key,omitempty"` + // name of enrollment group + Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *CreateEnrollmentGroupRequest) Reset() { + *x = CreateEnrollmentGroupRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateEnrollmentGroupRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateEnrollmentGroupRequest) ProtoMessage() {} + +func (x *CreateEnrollmentGroupRequest) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateEnrollmentGroupRequest.ProtoReflect.Descriptor instead. +func (*CreateEnrollmentGroupRequest) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_enrollmentGroup_proto_rawDescGZIP(), []int{3} +} + +func (x *CreateEnrollmentGroupRequest) GetAttestationMechanism() *AttestationMechanism { + if x != nil { + return x.AttestationMechanism + } + return nil +} + +func (x *CreateEnrollmentGroupRequest) GetHubIds() []string { + if x != nil { + return x.HubIds + } + return nil +} + +func (x *CreateEnrollmentGroupRequest) GetPreSharedKey() string { + if x != nil { + return x.PreSharedKey + } + return "" +} + +func (x *CreateEnrollmentGroupRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type GetEnrollmentGroupsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Filter by id. + IdFilter []string `protobuf:"bytes,1,rep,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` + // Filter by certificates comman names in x509 attestation mechanism + AttestationMechanismX509CertificateNames []string `protobuf:"bytes,2,rep,name=attestation_mechanism_x509_certificate_names,json=attestationMechanismX509CertificateNames,proto3" json:"attestation_mechanism_x509_certificate_names,omitempty"` + // Filter by hubId. + HubIdFilter []string `protobuf:"bytes,3,rep,name=hub_id_filter,json=hubIdFilter,proto3" json:"hub_id_filter,omitempty"` +} + +func (x *GetEnrollmentGroupsRequest) Reset() { + *x = GetEnrollmentGroupsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetEnrollmentGroupsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetEnrollmentGroupsRequest) ProtoMessage() {} + +func (x *GetEnrollmentGroupsRequest) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetEnrollmentGroupsRequest.ProtoReflect.Descriptor instead. +func (*GetEnrollmentGroupsRequest) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_enrollmentGroup_proto_rawDescGZIP(), []int{4} +} + +func (x *GetEnrollmentGroupsRequest) GetIdFilter() []string { + if x != nil { + return x.IdFilter + } + return nil +} + +func (x *GetEnrollmentGroupsRequest) GetAttestationMechanismX509CertificateNames() []string { + if x != nil { + return x.AttestationMechanismX509CertificateNames + } + return nil +} + +func (x *GetEnrollmentGroupsRequest) GetHubIdFilter() []string { + if x != nil { + return x.HubIdFilter + } + return nil +} + +type UpdateEnrollmentGroup struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Attestation mechanism + AttestationMechanism *AttestationMechanism `protobuf:"bytes,2,opt,name=attestation_mechanism,json=attestationMechanism,proto3" json:"attestation_mechanism,omitempty"` + // Hub configuration to configure device. + HubIds []string `protobuf:"bytes,6,rep,name=hub_ids,json=hubIds,proto3" json:"hub_ids,omitempty"` + // Pre shared key for devices in enrollment group. It can be used for maintenance operations by d2d client. Supported formats: , + PreSharedKey string `protobuf:"bytes,4,opt,name=pre_shared_key,json=preSharedKey,proto3" json:"pre_shared_key,omitempty"` + // name of enrollment group + Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty" bson:"name"` // @gotags: bson:"name" +} + +func (x *UpdateEnrollmentGroup) Reset() { + *x = UpdateEnrollmentGroup{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateEnrollmentGroup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateEnrollmentGroup) ProtoMessage() {} + +func (x *UpdateEnrollmentGroup) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateEnrollmentGroup.ProtoReflect.Descriptor instead. +func (*UpdateEnrollmentGroup) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_enrollmentGroup_proto_rawDescGZIP(), []int{5} +} + +func (x *UpdateEnrollmentGroup) GetAttestationMechanism() *AttestationMechanism { + if x != nil { + return x.AttestationMechanism + } + return nil +} + +func (x *UpdateEnrollmentGroup) GetHubIds() []string { + if x != nil { + return x.HubIds + } + return nil +} + +func (x *UpdateEnrollmentGroup) GetPreSharedKey() string { + if x != nil { + return x.PreSharedKey + } + return "" +} + +func (x *UpdateEnrollmentGroup) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type UpdateEnrollmentGroupRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Enrollment group ID. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + EnrollmentGroup *UpdateEnrollmentGroup `protobuf:"bytes,2,opt,name=enrollment_group,json=enrollmentGroup,proto3" json:"enrollment_group,omitempty"` +} + +func (x *UpdateEnrollmentGroupRequest) Reset() { + *x = UpdateEnrollmentGroupRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateEnrollmentGroupRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateEnrollmentGroupRequest) ProtoMessage() {} + +func (x *UpdateEnrollmentGroupRequest) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateEnrollmentGroupRequest.ProtoReflect.Descriptor instead. +func (*UpdateEnrollmentGroupRequest) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_enrollmentGroup_proto_rawDescGZIP(), []int{6} +} + +func (x *UpdateEnrollmentGroupRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *UpdateEnrollmentGroupRequest) GetEnrollmentGroup() *UpdateEnrollmentGroup { + if x != nil { + return x.EnrollmentGroup + } + return nil +} + +type DeleteEnrollmentGroupsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Enrollment group ID. + IdFilter []string `protobuf:"bytes,1,rep,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` +} + +func (x *DeleteEnrollmentGroupsRequest) Reset() { + *x = DeleteEnrollmentGroupsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteEnrollmentGroupsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteEnrollmentGroupsRequest) ProtoMessage() {} + +func (x *DeleteEnrollmentGroupsRequest) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteEnrollmentGroupsRequest.ProtoReflect.Descriptor instead. +func (*DeleteEnrollmentGroupsRequest) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_enrollmentGroup_proto_rawDescGZIP(), []int{7} +} + +func (x *DeleteEnrollmentGroupsRequest) GetIdFilter() []string { + if x != nil { + return x.IdFilter + } + return nil +} + +type DeleteEnrollmentGroupsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Number of deleted records. + Count int64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` +} + +func (x *DeleteEnrollmentGroupsResponse) Reset() { + *x = DeleteEnrollmentGroupsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteEnrollmentGroupsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteEnrollmentGroupsResponse) ProtoMessage() {} + +func (x *DeleteEnrollmentGroupsResponse) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteEnrollmentGroupsResponse.ProtoReflect.Descriptor instead. +func (*DeleteEnrollmentGroupsResponse) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_enrollmentGroup_proto_rawDescGZIP(), []int{8} +} + +func (x *DeleteEnrollmentGroupsResponse) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +var File_device_provisioning_service_pb_enrollmentGroup_proto protoreflect.FileDescriptor + +var file_device_provisioning_service_pb_enrollmentGroup_proto_rawDesc = []byte{ + 0x0a, 0x34, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x2d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x70, 0x62, + 0x2f, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1c, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x2e, 0x70, 0x62, 0x22, 0xb4, 0x01, 0x0a, 0x11, 0x58, 0x35, 0x30, 0x39, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x0a, 0x11, 0x63, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x15, 0x6c, 0x65, 0x61, 0x64, 0x5f, + 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x6c, 0x65, 0x61, 0x64, 0x43, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3e, 0x0a, 0x1b, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x19, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x5b, 0x0a, 0x14, 0x41, + 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x63, 0x68, 0x61, 0x6e, + 0x69, 0x73, 0x6d, 0x12, 0x43, 0x0a, 0x04, 0x78, 0x35, 0x30, 0x39, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2f, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, + 0x2e, 0x58, 0x35, 0x30, 0x39, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x04, 0x78, 0x35, 0x30, 0x39, 0x22, 0x81, 0x02, 0x0a, 0x0f, 0x45, 0x6e, 0x72, + 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x77, 0x6e, + 0x65, 0x72, 0x12, 0x67, 0x0a, 0x15, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x6d, 0x65, 0x63, 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x32, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, + 0x2e, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x63, 0x68, + 0x61, 0x6e, 0x69, 0x73, 0x6d, 0x52, 0x14, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x4d, 0x65, 0x63, 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d, 0x12, 0x17, 0x0a, 0x07, 0x68, + 0x75, 0x62, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x68, 0x75, + 0x62, 0x49, 0x64, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x70, 0x72, 0x65, 0x5f, 0x73, 0x68, 0x61, 0x72, + 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, + 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x4a, 0x04, + 0x08, 0x04, 0x10, 0x05, 0x52, 0x06, 0x68, 0x75, 0x62, 0x5f, 0x69, 0x64, 0x22, 0xf5, 0x01, 0x0a, + 0x1c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, + 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x67, 0x0a, + 0x15, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x63, + 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, + 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x74, 0x74, 0x65, + 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x63, 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d, + 0x52, 0x14, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x63, + 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x75, 0x62, 0x5f, 0x69, 0x64, + 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x68, 0x75, 0x62, 0x49, 0x64, 0x73, 0x12, + 0x24, 0x0a, 0x0e, 0x70, 0x72, 0x65, 0x5f, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, + 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, + 0x04, 0x08, 0x03, 0x10, 0x04, 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x52, 0x06, 0x68, 0x75, + 0x62, 0x5f, 0x69, 0x64, 0x22, 0xbd, 0x01, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x72, 0x6f, + 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x12, 0x5e, 0x0a, 0x2c, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x6d, 0x65, 0x63, 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d, 0x5f, 0x78, 0x35, 0x30, 0x39, 0x5f, 0x63, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x28, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x63, 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d, 0x58, 0x35, 0x30, 0x39, + 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, + 0x12, 0x22, 0x0a, 0x0d, 0x68, 0x75, 0x62, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, + 0x72, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x68, 0x75, 0x62, 0x49, 0x64, 0x46, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x22, 0xee, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, + 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x67, + 0x0a, 0x15, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x65, + 0x63, 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, + 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x74, 0x74, + 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x63, 0x68, 0x61, 0x6e, 0x69, 0x73, + 0x6d, 0x52, 0x14, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, + 0x63, 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x75, 0x62, 0x5f, 0x69, + 0x64, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x68, 0x75, 0x62, 0x49, 0x64, 0x73, + 0x12, 0x24, 0x0a, 0x0e, 0x70, 0x72, 0x65, 0x5f, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x6b, + 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, + 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, + 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x52, 0x06, 0x68, 0x75, 0x62, 0x5f, 0x69, 0x64, 0x52, 0x05, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x22, 0x8e, 0x01, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x5e, 0x0a, 0x10, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, + 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x33, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x0f, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, + 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x22, 0x3c, 0x0a, 0x1d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x22, 0x36, 0x0a, 0x1e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, + 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x3e, 0x5a, 0x3c, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x67, 0x64, 0x2d, + 0x64, 0x65, 0x76, 0x2f, 0x68, 0x75, 0x62, 0x2f, 0x76, 0x32, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x2d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x70, 0x62, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_device_provisioning_service_pb_enrollmentGroup_proto_rawDescOnce sync.Once + file_device_provisioning_service_pb_enrollmentGroup_proto_rawDescData = file_device_provisioning_service_pb_enrollmentGroup_proto_rawDesc +) + +func file_device_provisioning_service_pb_enrollmentGroup_proto_rawDescGZIP() []byte { + file_device_provisioning_service_pb_enrollmentGroup_proto_rawDescOnce.Do(func() { + file_device_provisioning_service_pb_enrollmentGroup_proto_rawDescData = protoimpl.X.CompressGZIP(file_device_provisioning_service_pb_enrollmentGroup_proto_rawDescData) + }) + return file_device_provisioning_service_pb_enrollmentGroup_proto_rawDescData +} + +var file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_device_provisioning_service_pb_enrollmentGroup_proto_goTypes = []any{ + (*X509Configuration)(nil), // 0: deviceprovisioningservice.pb.X509Configuration + (*AttestationMechanism)(nil), // 1: deviceprovisioningservice.pb.AttestationMechanism + (*EnrollmentGroup)(nil), // 2: deviceprovisioningservice.pb.EnrollmentGroup + (*CreateEnrollmentGroupRequest)(nil), // 3: deviceprovisioningservice.pb.CreateEnrollmentGroupRequest + (*GetEnrollmentGroupsRequest)(nil), // 4: deviceprovisioningservice.pb.GetEnrollmentGroupsRequest + (*UpdateEnrollmentGroup)(nil), // 5: deviceprovisioningservice.pb.UpdateEnrollmentGroup + (*UpdateEnrollmentGroupRequest)(nil), // 6: deviceprovisioningservice.pb.UpdateEnrollmentGroupRequest + (*DeleteEnrollmentGroupsRequest)(nil), // 7: deviceprovisioningservice.pb.DeleteEnrollmentGroupsRequest + (*DeleteEnrollmentGroupsResponse)(nil), // 8: deviceprovisioningservice.pb.DeleteEnrollmentGroupsResponse +} +var file_device_provisioning_service_pb_enrollmentGroup_proto_depIdxs = []int32{ + 0, // 0: deviceprovisioningservice.pb.AttestationMechanism.x509:type_name -> deviceprovisioningservice.pb.X509Configuration + 1, // 1: deviceprovisioningservice.pb.EnrollmentGroup.attestation_mechanism:type_name -> deviceprovisioningservice.pb.AttestationMechanism + 1, // 2: deviceprovisioningservice.pb.CreateEnrollmentGroupRequest.attestation_mechanism:type_name -> deviceprovisioningservice.pb.AttestationMechanism + 1, // 3: deviceprovisioningservice.pb.UpdateEnrollmentGroup.attestation_mechanism:type_name -> deviceprovisioningservice.pb.AttestationMechanism + 5, // 4: deviceprovisioningservice.pb.UpdateEnrollmentGroupRequest.enrollment_group:type_name -> deviceprovisioningservice.pb.UpdateEnrollmentGroup + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_device_provisioning_service_pb_enrollmentGroup_proto_init() } +func file_device_provisioning_service_pb_enrollmentGroup_proto_init() { + if File_device_provisioning_service_pb_enrollmentGroup_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*X509Configuration); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*AttestationMechanism); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[2].Exporter = func(v any, i int) any { + switch v := v.(*EnrollmentGroup); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[3].Exporter = func(v any, i int) any { + switch v := v.(*CreateEnrollmentGroupRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[4].Exporter = func(v any, i int) any { + switch v := v.(*GetEnrollmentGroupsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[5].Exporter = func(v any, i int) any { + switch v := v.(*UpdateEnrollmentGroup); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[6].Exporter = func(v any, i int) any { + switch v := v.(*UpdateEnrollmentGroupRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[7].Exporter = func(v any, i int) any { + switch v := v.(*DeleteEnrollmentGroupsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes[8].Exporter = func(v any, i int) any { + switch v := v.(*DeleteEnrollmentGroupsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_device_provisioning_service_pb_enrollmentGroup_proto_rawDesc, + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_device_provisioning_service_pb_enrollmentGroup_proto_goTypes, + DependencyIndexes: file_device_provisioning_service_pb_enrollmentGroup_proto_depIdxs, + MessageInfos: file_device_provisioning_service_pb_enrollmentGroup_proto_msgTypes, + }.Build() + File_device_provisioning_service_pb_enrollmentGroup_proto = out.File + file_device_provisioning_service_pb_enrollmentGroup_proto_rawDesc = nil + file_device_provisioning_service_pb_enrollmentGroup_proto_goTypes = nil + file_device_provisioning_service_pb_enrollmentGroup_proto_depIdxs = nil +} diff --git a/device-provisioning-service/pb/enrollmentGroup.proto b/device-provisioning-service/pb/enrollmentGroup.proto new file mode 100644 index 000000000..e65067aa2 --- /dev/null +++ b/device-provisioning-service/pb/enrollmentGroup.proto @@ -0,0 +1,87 @@ +syntax = "proto3"; + +package deviceprovisioningservice.pb; + +option go_package = "github.com/plgd-dev/hub/v2/device-provisioning-service/pb;pb"; + +message X509Configuration { + // chain certficates authorities: ..<-intermediateCA1<-intermediateCA<-RootCA which is used to match enrollment group. Supported formats: , + string certificate_chain = 1; // @gotags: bson:"certificateChain" + // the certificate name must be one from certificate_chain, it is used to match enrollment group. If empty, the first certificate from certificate_chain is used + string lead_certificate_name = 2; // @gotags: bson:"leadCertificateName" + // dont validate time during certificate verification + bool expired_certificate_enabled = 3; // @gotags: bson:"expiredCertificateEnabled" +} + +message AttestationMechanism { + // X509 attestation + X509Configuration x509 = 1; // @gotags: bson:"x509" +} + +message EnrollmentGroup { + reserved 4; // string hub_id = 4; + reserved "hub_id"; + // Enrollment group ID. + string id = 1; // @gotags: bson:"_id" + // HUB owner of device - used for hub authorization. + string owner = 2; // @gotags: bson:"owner" + // Attestation mechanism + AttestationMechanism attestation_mechanism = 3; // @gotags: bson:"attestationMechanism" + // Hub configuration to configure device. + repeated string hub_ids = 7; // @gotags: bson:"hubIds" + // Pre shared key for devices in enrollment group. It can be used for maintenance operations by d2d client. Supported formats: , + string pre_shared_key = 5; // @gotags: bson:"preSharedKey" + // name of enrollment group + string name = 6; // @gotags: bson:"name" +} + +message CreateEnrollmentGroupRequest { + reserved 1,3; // string hub_id = 3; + reserved "owner","hub_id"; + // Attestation mechanism + AttestationMechanism attestation_mechanism = 2; + // Hub configuration to configure device. + repeated string hub_ids = 6; + // Pre shared key for devices in enrollment group. It can be used for maintenance operations by d2d client. Supported formats: , + string pre_shared_key = 4; + // name of enrollment group + string name = 5; +} + +message GetEnrollmentGroupsRequest { + // Filter by id. + repeated string id_filter = 1; + // Filter by certificates comman names in x509 attestation mechanism + repeated string attestation_mechanism_x509_certificate_names = 2; + // Filter by hubId. + repeated string hub_id_filter = 3; +} + +message UpdateEnrollmentGroup { + reserved 1,3; // string hub_id = 4; + reserved "hub_id","owner"; + // Attestation mechanism + AttestationMechanism attestation_mechanism = 2; + // Hub configuration to configure device. + repeated string hub_ids = 6; + // Pre shared key for devices in enrollment group. It can be used for maintenance operations by d2d client. Supported formats: , + string pre_shared_key = 4; + // name of enrollment group + string name = 5; // @gotags: bson:"name" +} + +message UpdateEnrollmentGroupRequest { + // Enrollment group ID. + string id = 1; + UpdateEnrollmentGroup enrollment_group = 2; +} + +message DeleteEnrollmentGroupsRequest { + // Enrollment group ID. + repeated string id_filter = 1; +} + +message DeleteEnrollmentGroupsResponse { + // Number of deleted records. + int64 count = 1; +} diff --git a/device-provisioning-service/pb/hub.go b/device-provisioning-service/pb/hub.go new file mode 100644 index 000000000..5f8f54e78 --- /dev/null +++ b/device-provisioning-service/pb/hub.go @@ -0,0 +1,245 @@ +package pb + +import ( + "crypto/tls" + "errors" + "fmt" + "net/url" + "sort" + "time" + + "github.com/plgd-dev/device/v2/schema" + "github.com/plgd-dev/hub/v2/device-provisioning-service/security/oauth/clientcredentials" + "github.com/plgd-dev/hub/v2/device-provisioning-service/uri" + "github.com/plgd-dev/hub/v2/pkg/config/property/urischeme" + "github.com/plgd-dev/hub/v2/pkg/net/grpc/client" + pkgHttpClient "github.com/plgd-dev/hub/v2/pkg/net/http/client" + pkgCertManagerClient "github.com/plgd-dev/hub/v2/pkg/security/certManager/client" + "github.com/plgd-dev/hub/v2/pkg/strings" + "github.com/plgd-dev/kit/v2/security" +) + +type Hubs []*Hub + +func (p Hubs) Sort() { + sort.Slice(p, func(i, j int) bool { + return p[i].GetId() < p[j].GetId() + }) +} + +func (c *TlsConfig) ToConfig() pkgCertManagerClient.Config { + return pkgCertManagerClient.Config{ + CAPool: c.GetCaPool(), + KeyFile: urischeme.URIScheme(c.GetKey()), + CertFile: urischeme.URIScheme(c.GetCert()), + UseSystemCAPool: c.GetUseSystemCaPool(), + } +} + +func (c *GrpcConnectionConfig) ToConfig() client.Config { + return client.Config{ + Addr: c.GetAddress(), + KeepAlive: client.KeepAliveConfig{ + Time: time.Duration(c.GetKeepAlive().GetTime()) * time.Nanosecond, + Timeout: time.Duration(c.GetKeepAlive().GetTimeout()) * time.Nanosecond, + PermitWithoutStream: c.GetKeepAlive().GetPermitWithoutStream(), + }, + TLS: c.GetTls().ToConfig(), + } +} + +func (c *AuthorizationProviderConfig) ToConfig() clientcredentials.Config { + return clientcredentials.Config{ + Authority: c.GetAuthority(), + ClientID: c.GetClientId(), + ClientSecretFile: urischeme.URIScheme(c.GetClientSecret()), + Scopes: c.GetScopes(), + Audience: c.GetAudience(), + HTTP: pkgHttpClient.Config{ + MaxIdleConns: int(c.GetHttp().GetMaxIdleConns()), + MaxConnsPerHost: int(c.GetHttp().GetMaxConnsPerHost()), + MaxIdleConnsPerHost: int(c.GetHttp().GetMaxIdleConnsPerHost()), + IdleConnTimeout: time.Duration(c.GetHttp().GetIdleConnTimeout()) * time.Nanosecond, + Timeout: time.Duration(c.GetHttp().GetTimeout()) * time.Nanosecond, + TLS: c.GetHttp().GetTls().ToConfig(), + }, + // TokenURL: , + } +} + +func (c *TlsConfig) validateCAPool() error { + if c.GetCaPool() == nil && !c.GetUseSystemCaPool() { + return fmt.Errorf("caPool('%v'),useSystemCAPool('%v') - are not set", c.GetCaPool(), c.GetUseSystemCaPool()) + } + if c.GetCaPool() != nil { + for i, ca := range c.GetCaPool() { + data, err := urischeme.URIScheme(ca).Read() + if err != nil { + return fmt.Errorf("caPool[%d]('%v') - %w", i, ca, err) + } + _, err = security.ParseX509FromPEM(data) + if err != nil { + return fmt.Errorf("caPool[%d]('%v') - %w", i, ca, err) + } + } + } + return nil +} + +func (c *TlsConfig) Validate() error { + if err := c.validateCAPool(); err != nil { + return err + } + if c.GetCert() != "" || c.GetKey() != "" { + certPEMBlock, err := urischeme.URIScheme(c.GetCert()).Read() + if err != nil { + return fmt.Errorf("certFile('%v') - %w", c.GetCert(), err) + } + keyPEMBlock, err := urischeme.URIScheme(c.GetKey()).Read() + if err != nil { + return fmt.Errorf("keyFile('%v') - %w", c.GetKey(), err) + } + _, err = tls.X509KeyPair(certPEMBlock, keyPEMBlock) + if err != nil { + return fmt.Errorf("certFile('%v'),keyFile('%v') - %w", c.GetCert(), c.GetKey(), err) + } + } + return nil +} + +func (c *GrpcConnectionConfig) Validate() error { + if c.GetAddress() == "" { + return fmt.Errorf("address('%v')", c.GetAddress()) + } + if c.GetTls() == nil { + return errors.New("tls - is empty") + } + if err := c.GetTls().Validate(); err != nil { + return fmt.Errorf("tls.%w", err) + } + return nil +} + +func (c *GrpcClientConfig) Validate() error { + if c.GetGrpc() == nil { + return errors.New("grpc - is empty") + } + if err := c.GetGrpc().Validate(); err != nil { + return fmt.Errorf("grpc.%w", err) + } + return nil +} + +func (c *HttpConfig) Validate() error { + if c.GetTimeout() < 0 { + return fmt.Errorf("timeout('%v')", c.GetTimeout()) + } + if c.GetIdleConnTimeout() < 0 { + return fmt.Errorf("idleConnTimeout('%v')", c.GetIdleConnTimeout()) + } + if c.GetTls() == nil { + return errors.New("tls - is empty") + } + if err := c.GetTls().Validate(); err != nil { + return fmt.Errorf("tls.%w", err) + } + return nil +} + +func (c *AuthorizationProviderConfig) Validate() error { + if c.GetName() == "" { + return fmt.Errorf("name('%v')", c.GetName()) + } + if c.GetAuthority() == "" { + return fmt.Errorf("authority('%v')", c.GetAuthority()) + } + if c.GetClientId() == "" { + return fmt.Errorf("clientId('%v')", c.GetClientId()) + } + if _, err := urischeme.URIScheme(c.GetClientSecret()).Read(); err != nil { + return fmt.Errorf("clientSecret('%v') - %w", c.GetClientSecret(), err) + } + if c.GetHttp() == nil { + return errors.New("http - is empty") + } + if err := c.GetHttp().Validate(); err != nil { + return fmt.Errorf("http.%w", err) + } + return nil +} + +func (c *AuthorizationConfig) Validate() error { + if c.GetOwnerClaim() == "" { + return fmt.Errorf("ownerClaim('%v')", c.GetOwnerClaim()) + } + if c.GetProvider() == nil { + return errors.New("provider - is empty") + } + if err := c.GetProvider().Validate(); err != nil { + return fmt.Errorf("provider.%w", err) + } + return nil +} + +func ValidateCoapGatewayURI(coapGwURI string) (string, error) { + parsedURL, err := url.Parse(coapGwURI) + if err == nil { + switch schema.Scheme(parsedURL.Scheme) { + case schema.TCPSecureScheme, schema.UDPSecureScheme, schema.TCPScheme, schema.UDPScheme: + return coapGwURI, nil + } + } + u := uri.CoAPsTCPSchemePrefix + coapGwURI + parsedURL, err = url.Parse(u) + if err != nil || parsedURL.Scheme == "" { + return "", fmt.Errorf("invalid URI('%v')", coapGwURI) + } + return u, nil +} + +func (h *Hub) Validate(owner string) error { + if h.GetId() == "" { + return fmt.Errorf("id('%v')", h.GetId()) + } + if h.GetOwner() == "" { + return fmt.Errorf("owner('%v') - is empty", h.GetOwner()) + } + if owner != "" && owner != h.GetOwner() { + return fmt.Errorf("owner('%v') - expects %v", h.GetOwner(), owner) + } + if len(h.GetGateways()) == 0 { + return errors.New("coapGateways - is empty") + } + for i, gw := range h.GetGateways() { + if gw == "" { + return fmt.Errorf("coapGateways[%d]('%v') - is empty", i, gw) + } + fixedGw, err := ValidateCoapGatewayURI(gw) + if err != nil { + return fmt.Errorf("coapGateways[%d]('%v') - %w", i, gw, err) + } + h.Gateways[i] = fixedGw + } + h.Gateways = strings.UniqueStable(h.GetGateways()) + if h.GetCertificateAuthority() == nil { + return errors.New("certificateAuthority - is empty") + } + if err := h.GetCertificateAuthority().Validate(); err != nil { + return fmt.Errorf("certificateAuthority.%w", err) + } + if h.GetAuthorization() == nil { + return errors.New("authorization - is empty") + } + if err := h.GetAuthorization().Validate(); err != nil { + return fmt.Errorf("authorization.%w", err) + } + if h.GetName() == "" { + // for backward compatibility + h.Name = h.GetId() + } + if h.GetHubId() == "" { + // for backward compatibility + h.HubId = h.GetId() + } + return nil +} diff --git a/device-provisioning-service/pb/hub.pb.go b/device-provisioning-service/pb/hub.pb.go new file mode 100644 index 000000000..c811c8e0f --- /dev/null +++ b/device-provisioning-service/pb/hub.pb.go @@ -0,0 +1,1460 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v5.27.3 +// source: github.com/plgd-dev/hub/device-provisioning-service/pb/hub.proto + +package pb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TlsConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // the root certificates. Supported formats: , + CaPool []string `protobuf:"bytes,1,rep,name=ca_pool,json=caPool,proto3" json:"ca_pool,omitempty" bson:"ca_pool"` // @gotags: bson:"ca_pool" + // private key. Supported formats: , + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty" bson:"key"` // @gotags: bson:"key" + // certificate. Supported formats: , + Cert string `protobuf:"bytes,3,opt,name=cert,proto3" json:"cert,omitempty" bson:"cert"` // @gotags: bson:"cert" + // use system certification pool + UseSystemCaPool bool `protobuf:"varint,4,opt,name=use_system_ca_pool,json=useSystemCaPool,proto3" json:"use_system_ca_pool,omitempty" bson:"useSystemCaPool"` // @gotags: bson:"useSystemCaPool" +} + +func (x *TlsConfig) Reset() { + *x = TlsConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TlsConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TlsConfig) ProtoMessage() {} + +func (x *TlsConfig) ProtoReflect() protoreflect.Message { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TlsConfig.ProtoReflect.Descriptor instead. +func (*TlsConfig) Descriptor() ([]byte, []int) { + return file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescGZIP(), []int{0} +} + +func (x *TlsConfig) GetCaPool() []string { + if x != nil { + return x.CaPool + } + return nil +} + +func (x *TlsConfig) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *TlsConfig) GetCert() string { + if x != nil { + return x.Cert + } + return "" +} + +func (x *TlsConfig) GetUseSystemCaPool() bool { + if x != nil { + return x.UseSystemCaPool + } + return false +} + +type HttpConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // MaxIdleConns controls the maximum number of idle (keep-alive) + // connections across all hosts. Zero means no limit. + MaxIdleConns uint32 `protobuf:"varint,1,opt,name=max_idle_conns,json=maxIdleConns,proto3" json:"max_idle_conns,omitempty" bson:"maxIdleConns"` // @gotags: bson:"maxIdleConns" + // MaxConnsPerHost optionally limits the total number of + // connections per host, including connections in the dialing, + // active, and idle states. On limit violation, dials will block. + // + // Zero means no limit. + MaxConnsPerHost uint32 `protobuf:"varint,2,opt,name=max_conns_per_host,json=maxConnsPerHost,proto3" json:"max_conns_per_host,omitempty" bson:"maxConnsPerHost"` // @gotags: bson:"maxConnsPerHost" + // MaxIdleConnsPerHost, if non-zero, controls the maximum idle + // (keep-alive) connections to keep per-host. If zero, + // DefaultMaxIdleConnsPerHost is used. + MaxIdleConnsPerHost uint32 `protobuf:"varint,3,opt,name=max_idle_conns_per_host,json=maxIdleConnsPerHost,proto3" json:"max_idle_conns_per_host,omitempty" bson:"maxIdleConnsPerHost"` // @gotags: bson:"maxIdleConnsPerHost" + // IdleConnTimeout is the maximum amount of time an idle + // (keep-alive) connection will remain idle before closing + // itself in nanoseconds. + // Zero means no limit. + IdleConnTimeout int64 `protobuf:"varint,4,opt,name=idle_conn_timeout,json=idleConnTimeout,proto3" json:"idle_conn_timeout,omitempty" bson:"idleConnTimeout"` // @gotags: bson:"idleConnTimeout" + // Timeout specifies a time limit for requests made by this + // Client in nanoseconds. The timeout includes connection time, any + // redirects, and reading the response body. The timer remains + // running after Get, Head, Post, or Do return and will + // interrupt reading of the Response.Body. + // + // A Timeout of zero means no timeout. + // + // The Client cancels requests to the underlying Transport + // as if the Request's Context ended. + // + // For compatibility, the Client will also use the deprecated + // CancelRequest method on Transport if found. New + // RoundTripper implementations should use the Request's Context + // for cancellation instead of implementing CancelRequest. + Timeout int64 `protobuf:"varint,5,opt,name=timeout,proto3" json:"timeout,omitempty" bson:"timeout"` // @gotags: bson:"timeout" + Tls *TlsConfig `protobuf:"bytes,6,opt,name=tls,proto3" json:"tls,omitempty" bson:"tls"` // @gotags: bson:"tls" +} + +func (x *HttpConfig) Reset() { + *x = HttpConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HttpConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpConfig) ProtoMessage() {} + +func (x *HttpConfig) ProtoReflect() protoreflect.Message { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HttpConfig.ProtoReflect.Descriptor instead. +func (*HttpConfig) Descriptor() ([]byte, []int) { + return file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescGZIP(), []int{1} +} + +func (x *HttpConfig) GetMaxIdleConns() uint32 { + if x != nil { + return x.MaxIdleConns + } + return 0 +} + +func (x *HttpConfig) GetMaxConnsPerHost() uint32 { + if x != nil { + return x.MaxConnsPerHost + } + return 0 +} + +func (x *HttpConfig) GetMaxIdleConnsPerHost() uint32 { + if x != nil { + return x.MaxIdleConnsPerHost + } + return 0 +} + +func (x *HttpConfig) GetIdleConnTimeout() int64 { + if x != nil { + return x.IdleConnTimeout + } + return 0 +} + +func (x *HttpConfig) GetTimeout() int64 { + if x != nil { + return x.Timeout + } + return 0 +} + +func (x *HttpConfig) GetTls() *TlsConfig { + if x != nil { + return x.Tls + } + return nil +} + +type AuthorizationProviderConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // the name of the provider, which is set in configuration in coap-gateway + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty" bson:"name"` // @gotags: bson:"name" + // the url to get oauth endpoints + Authority string `protobuf:"bytes,2,opt,name=authority,proto3" json:"authority,omitempty" bson:"authority"` // @gotags: bson:"authority" + // client id which is associated to the name + ClientId string `protobuf:"bytes,3,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty" bson:"clientId"` // @gotags: bson:"clientId" + // scopes will be set in token + Scopes []string `protobuf:"bytes,4,rep,name=scopes,proto3" json:"scopes,omitempty" bson:"scopes"` // @gotags: bson:"scopes" + // audience will be set in token + Audience string `protobuf:"bytes,5,opt,name=audience,proto3" json:"audience,omitempty" bson:"audience"` // @gotags: bson:"audience" + // client secret. Supported formats: , + ClientSecret string `protobuf:"bytes,6,opt,name=client_secret,json=clientSecret,proto3" json:"client_secret,omitempty" bson:"clientSecret"` // @gotags: bson:"clientSecret" + // http configuration + Http *HttpConfig `protobuf:"bytes,7,opt,name=http,proto3" json:"http,omitempty" bson:"http"` // @gotags: bson:"http" +} + +func (x *AuthorizationProviderConfig) Reset() { + *x = AuthorizationProviderConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AuthorizationProviderConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorizationProviderConfig) ProtoMessage() {} + +func (x *AuthorizationProviderConfig) ProtoReflect() protoreflect.Message { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthorizationProviderConfig.ProtoReflect.Descriptor instead. +func (*AuthorizationProviderConfig) Descriptor() ([]byte, []int) { + return file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescGZIP(), []int{2} +} + +func (x *AuthorizationProviderConfig) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AuthorizationProviderConfig) GetAuthority() string { + if x != nil { + return x.Authority + } + return "" +} + +func (x *AuthorizationProviderConfig) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *AuthorizationProviderConfig) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +func (x *AuthorizationProviderConfig) GetAudience() string { + if x != nil { + return x.Audience + } + return "" +} + +func (x *AuthorizationProviderConfig) GetClientSecret() string { + if x != nil { + return x.ClientSecret + } + return "" +} + +func (x *AuthorizationProviderConfig) GetHttp() *HttpConfig { + if x != nil { + return x.Http + } + return nil +} + +// AuthorizationConfig is used to generate the authorization code for the device when providing cloud configuration. +type AuthorizationConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // owner_claim is key where will be stored owner in JWT. + OwnerClaim string `protobuf:"bytes,1,opt,name=owner_claim,json=ownerClaim,proto3" json:"owner_claim,omitempty" bson:"ownerClaim"` // @gotags: bson:"ownerClaim" + // device_id_claim is key where will be stored deviceID in JWT(optional) + DeviceIdClaim string `protobuf:"bytes,2,opt,name=device_id_claim,json=deviceIdClaim,proto3" json:"device_id_claim,omitempty" bson:"deviceIdClaim"` // @gotags: bson:"deviceIdClaim" + Provider *AuthorizationProviderConfig `protobuf:"bytes,3,opt,name=provider,proto3" json:"provider,omitempty" bson:"provider"` // @gotags: bson:"provider" +} + +func (x *AuthorizationConfig) Reset() { + *x = AuthorizationConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AuthorizationConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorizationConfig) ProtoMessage() {} + +func (x *AuthorizationConfig) ProtoReflect() protoreflect.Message { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthorizationConfig.ProtoReflect.Descriptor instead. +func (*AuthorizationConfig) Descriptor() ([]byte, []int) { + return file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescGZIP(), []int{3} +} + +func (x *AuthorizationConfig) GetOwnerClaim() string { + if x != nil { + return x.OwnerClaim + } + return "" +} + +func (x *AuthorizationConfig) GetDeviceIdClaim() string { + if x != nil { + return x.DeviceIdClaim + } + return "" +} + +func (x *AuthorizationConfig) GetProvider() *AuthorizationProviderConfig { + if x != nil { + return x.Provider + } + return nil +} + +type GrpcKeepAliveConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // After a duration in nanoseconds of this time if the client doesn't see any activity it + // pings the server to see if the transport is still alive. + // The zero value is infinity and if it set below 10s, a minimum value of 10s will be used instead. + Time int64 `protobuf:"varint,1,opt,name=time,proto3" json:"time,omitempty" bson:"time"` // @gotags: bson:"time" + // After having pinged for keepalive check, the client waits for a duration + // of Timeout and if no activity is seen even after that the connection is + // closed. + Timeout int64 `protobuf:"varint,2,opt,name=timeout,proto3" json:"timeout,omitempty" bson:"timeout"` // @gotags: bson:"timeout" + // If true, client sends keepalive pings even with no active RPCs. If false, + // when there are no active RPCs, Time and Timeout will be ignored and no + // keepalive pings will be sent. + PermitWithoutStream bool `protobuf:"varint,3,opt,name=permit_without_stream,json=permitWithoutStream,proto3" json:"permit_without_stream,omitempty"` // @gotags: bson:"permitWithoutStream +} + +func (x *GrpcKeepAliveConfig) Reset() { + *x = GrpcKeepAliveConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GrpcKeepAliveConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GrpcKeepAliveConfig) ProtoMessage() {} + +func (x *GrpcKeepAliveConfig) ProtoReflect() protoreflect.Message { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GrpcKeepAliveConfig.ProtoReflect.Descriptor instead. +func (*GrpcKeepAliveConfig) Descriptor() ([]byte, []int) { + return file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescGZIP(), []int{4} +} + +func (x *GrpcKeepAliveConfig) GetTime() int64 { + if x != nil { + return x.Time + } + return 0 +} + +func (x *GrpcKeepAliveConfig) GetTimeout() int64 { + if x != nil { + return x.Timeout + } + return 0 +} + +func (x *GrpcKeepAliveConfig) GetPermitWithoutStream() bool { + if x != nil { + return x.PermitWithoutStream + } + return false +} + +type GrpcConnectionConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Address in format {host:port} + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty" bson:"address"` // @gotags: bson:"address" + KeepAlive *GrpcKeepAliveConfig `protobuf:"bytes,2,opt,name=keep_alive,json=keepAlive,proto3" json:"keep_alive,omitempty" bson:"keepAlive"` // @gotags: bson:"keepAlive" + Tls *TlsConfig `protobuf:"bytes,3,opt,name=tls,proto3" json:"tls,omitempty" bson:"tls"` // @gotags: bson:"tls" +} + +func (x *GrpcConnectionConfig) Reset() { + *x = GrpcConnectionConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GrpcConnectionConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GrpcConnectionConfig) ProtoMessage() {} + +func (x *GrpcConnectionConfig) ProtoReflect() protoreflect.Message { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GrpcConnectionConfig.ProtoReflect.Descriptor instead. +func (*GrpcConnectionConfig) Descriptor() ([]byte, []int) { + return file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescGZIP(), []int{5} +} + +func (x *GrpcConnectionConfig) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *GrpcConnectionConfig) GetKeepAlive() *GrpcKeepAliveConfig { + if x != nil { + return x.KeepAlive + } + return nil +} + +func (x *GrpcConnectionConfig) GetTls() *TlsConfig { + if x != nil { + return x.Tls + } + return nil +} + +type GrpcClientConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // GRPC protocol + Grpc *GrpcConnectionConfig `protobuf:"bytes,1,opt,name=grpc,proto3" json:"grpc,omitempty" bson:"grpc"` // @gotags: bson:"grpc" +} + +func (x *GrpcClientConfig) Reset() { + *x = GrpcClientConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GrpcClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GrpcClientConfig) ProtoMessage() {} + +func (x *GrpcClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GrpcClientConfig.ProtoReflect.Descriptor instead. +func (*GrpcClientConfig) Descriptor() ([]byte, []int) { + return file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescGZIP(), []int{6} +} + +func (x *GrpcClientConfig) GetGrpc() *GrpcConnectionConfig { + if x != nil { + return x.Grpc + } + return nil +} + +type Hub struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Record ID. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" bson:"_id"` // @gotags: bson:"_id" + // Address of gateway in format scheme://host:port + Gateways []string `protobuf:"bytes,8,rep,name=gateways,proto3" json:"gateways,omitempty" bson:"gateways"` // @gotags: bson:"gateways" + // Signs identity ceritificate for the device. + CertificateAuthority *GrpcClientConfig `protobuf:"bytes,4,opt,name=certificate_authority,json=certificateAuthority,proto3" json:"certificate_authority,omitempty" bson:"certificateAuthority"` // @gotags: bson:"certificateAuthority" + // Acquire HUB authorization code for the device. + Authorization *AuthorizationConfig `protobuf:"bytes,5,opt,name=authorization,proto3" json:"authorization,omitempty" bson:"authorization"` // @gotags: bson:"authorization" + // Hub name. + Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty" bson:"name"` // @gotags: bson:"name" + // Hub identifier - it must match with common name of gateway(coap-gateway) hub certificate. + HubId string `protobuf:"bytes,7,opt,name=hub_id,json=hubId,proto3" json:"hub_id,omitempty" bson:"hubId"` // @gotags: bson:"hubId" + // Owner of the hub + Owner string `protobuf:"bytes,9,opt,name=owner,proto3" json:"owner,omitempty" bson:"owner"` // @gotags: bson:"owner" +} + +func (x *Hub) Reset() { + *x = Hub{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Hub) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Hub) ProtoMessage() {} + +func (x *Hub) ProtoReflect() protoreflect.Message { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Hub.ProtoReflect.Descriptor instead. +func (*Hub) Descriptor() ([]byte, []int) { + return file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescGZIP(), []int{7} +} + +func (x *Hub) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Hub) GetGateways() []string { + if x != nil { + return x.Gateways + } + return nil +} + +func (x *Hub) GetCertificateAuthority() *GrpcClientConfig { + if x != nil { + return x.CertificateAuthority + } + return nil +} + +func (x *Hub) GetAuthorization() *AuthorizationConfig { + if x != nil { + return x.Authorization + } + return nil +} + +func (x *Hub) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Hub) GetHubId() string { + if x != nil { + return x.HubId + } + return "" +} + +func (x *Hub) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +type CreateHubRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Hub identifier - it must match with common name of gateway(coap-gateway) hub certificate. + HubId string `protobuf:"bytes,1,opt,name=hub_id,json=hubId,proto3" json:"hub_id,omitempty"` + // Address of gateways in format scheme://host:port + Gateways []string `protobuf:"bytes,7,rep,name=gateways,proto3" json:"gateways,omitempty"` + // Signs identity ceritificate for the device. + CertificateAuthority *GrpcClientConfig `protobuf:"bytes,4,opt,name=certificate_authority,json=certificateAuthority,proto3" json:"certificate_authority,omitempty"` + // Acquire HUB authorization code for the device. + Authorization *AuthorizationConfig `protobuf:"bytes,5,opt,name=authorization,proto3" json:"authorization,omitempty"` + // Hub name. + Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *CreateHubRequest) Reset() { + *x = CreateHubRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateHubRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateHubRequest) ProtoMessage() {} + +func (x *CreateHubRequest) ProtoReflect() protoreflect.Message { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateHubRequest.ProtoReflect.Descriptor instead. +func (*CreateHubRequest) Descriptor() ([]byte, []int) { + return file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescGZIP(), []int{8} +} + +func (x *CreateHubRequest) GetHubId() string { + if x != nil { + return x.HubId + } + return "" +} + +func (x *CreateHubRequest) GetGateways() []string { + if x != nil { + return x.Gateways + } + return nil +} + +func (x *CreateHubRequest) GetCertificateAuthority() *GrpcClientConfig { + if x != nil { + return x.CertificateAuthority + } + return nil +} + +func (x *CreateHubRequest) GetAuthorization() *AuthorizationConfig { + if x != nil { + return x.Authorization + } + return nil +} + +func (x *CreateHubRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type GetHubsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Filter by id. + IdFilter []string `protobuf:"bytes,1,rep,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` + // Filter by hub_id. + HubIdFilter []string `protobuf:"bytes,2,rep,name=hub_id_filter,json=hubIdFilter,proto3" json:"hub_id_filter,omitempty"` +} + +func (x *GetHubsRequest) Reset() { + *x = GetHubsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetHubsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetHubsRequest) ProtoMessage() {} + +func (x *GetHubsRequest) ProtoReflect() protoreflect.Message { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetHubsRequest.ProtoReflect.Descriptor instead. +func (*GetHubsRequest) Descriptor() ([]byte, []int) { + return file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescGZIP(), []int{9} +} + +func (x *GetHubsRequest) GetIdFilter() []string { + if x != nil { + return x.IdFilter + } + return nil +} + +func (x *GetHubsRequest) GetHubIdFilter() []string { + if x != nil { + return x.HubIdFilter + } + return nil +} + +type UpdateHub struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Address of coap-gateway in format scheme://host:port + Gateways []string `protobuf:"bytes,7,rep,name=gateways,proto3" json:"gateways,omitempty"` + // Signs identity ceritificate for the device. + CertificateAuthority *GrpcClientConfig `protobuf:"bytes,3,opt,name=certificate_authority,json=certificateAuthority,proto3" json:"certificate_authority,omitempty"` + // Acquire HUB authorization code for the device. + Authorization *AuthorizationConfig `protobuf:"bytes,4,opt,name=authorization,proto3" json:"authorization,omitempty"` + // Hub name. + Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"` + // Hub ID + HubId string `protobuf:"bytes,6,opt,name=hub_id,json=hubId,proto3" json:"hub_id,omitempty"` +} + +func (x *UpdateHub) Reset() { + *x = UpdateHub{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateHub) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateHub) ProtoMessage() {} + +func (x *UpdateHub) ProtoReflect() protoreflect.Message { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateHub.ProtoReflect.Descriptor instead. +func (*UpdateHub) Descriptor() ([]byte, []int) { + return file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescGZIP(), []int{10} +} + +func (x *UpdateHub) GetGateways() []string { + if x != nil { + return x.Gateways + } + return nil +} + +func (x *UpdateHub) GetCertificateAuthority() *GrpcClientConfig { + if x != nil { + return x.CertificateAuthority + } + return nil +} + +func (x *UpdateHub) GetAuthorization() *AuthorizationConfig { + if x != nil { + return x.Authorization + } + return nil +} + +func (x *UpdateHub) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UpdateHub) GetHubId() string { + if x != nil { + return x.HubId + } + return "" +} + +type UpdateHubRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Record ID. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Hub *UpdateHub `protobuf:"bytes,2,opt,name=hub,proto3" json:"hub,omitempty"` +} + +func (x *UpdateHubRequest) Reset() { + *x = UpdateHubRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateHubRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateHubRequest) ProtoMessage() {} + +func (x *UpdateHubRequest) ProtoReflect() protoreflect.Message { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateHubRequest.ProtoReflect.Descriptor instead. +func (*UpdateHubRequest) Descriptor() ([]byte, []int) { + return file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescGZIP(), []int{11} +} + +func (x *UpdateHubRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *UpdateHubRequest) GetHub() *UpdateHub { + if x != nil { + return x.Hub + } + return nil +} + +type DeleteHubsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Record ID. + IdFilter []string `protobuf:"bytes,1,rep,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` +} + +func (x *DeleteHubsRequest) Reset() { + *x = DeleteHubsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteHubsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteHubsRequest) ProtoMessage() {} + +func (x *DeleteHubsRequest) ProtoReflect() protoreflect.Message { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteHubsRequest.ProtoReflect.Descriptor instead. +func (*DeleteHubsRequest) Descriptor() ([]byte, []int) { + return file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescGZIP(), []int{12} +} + +func (x *DeleteHubsRequest) GetIdFilter() []string { + if x != nil { + return x.IdFilter + } + return nil +} + +type DeleteHubsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Number of deleted records. + Count int64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` +} + +func (x *DeleteHubsResponse) Reset() { + *x = DeleteHubsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteHubsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteHubsResponse) ProtoMessage() {} + +func (x *DeleteHubsResponse) ProtoReflect() protoreflect.Message { + mi := &file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteHubsResponse.ProtoReflect.Descriptor instead. +func (*DeleteHubsResponse) Descriptor() ([]byte, []int) { + return file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescGZIP(), []int{13} +} + +func (x *DeleteHubsResponse) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +var File_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto protoreflect.FileDescriptor + +var file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDesc = []byte{ + 0x0a, 0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x67, + 0x64, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x68, 0x75, 0x62, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x2d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x70, 0x62, 0x2f, 0x68, 0x75, 0x62, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x1c, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, + 0x22, 0x77, 0x0a, 0x09, 0x54, 0x6c, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x17, 0x0a, + 0x07, 0x63, 0x61, 0x5f, 0x70, 0x6f, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, + 0x63, 0x61, 0x50, 0x6f, 0x6f, 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x65, 0x72, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x65, 0x72, 0x74, 0x12, 0x2b, 0x0a, 0x12, + 0x75, 0x73, 0x65, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x61, 0x5f, 0x70, 0x6f, + 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x75, 0x73, 0x65, 0x53, 0x79, 0x73, + 0x74, 0x65, 0x6d, 0x43, 0x61, 0x50, 0x6f, 0x6f, 0x6c, 0x22, 0x96, 0x02, 0x0a, 0x0a, 0x48, 0x74, + 0x74, 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x5f, + 0x69, 0x64, 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x73, 0x12, 0x2b, + 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x73, 0x5f, 0x70, 0x65, 0x72, 0x5f, + 0x68, 0x6f, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x6d, 0x61, 0x78, 0x43, + 0x6f, 0x6e, 0x6e, 0x73, 0x50, 0x65, 0x72, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x17, 0x6d, + 0x61, 0x78, 0x5f, 0x69, 0x64, 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x73, 0x5f, 0x70, 0x65, + 0x72, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x13, 0x6d, 0x61, + 0x78, 0x49, 0x64, 0x6c, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x73, 0x50, 0x65, 0x72, 0x48, 0x6f, 0x73, + 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x69, 0x64, 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x5f, 0x74, + 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x69, 0x64, + 0x6c, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x18, 0x0a, + 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, + 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x39, 0x0a, 0x03, 0x74, 0x6c, 0x73, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x6c, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x03, 0x74, + 0x6c, 0x73, 0x22, 0x83, 0x02, 0x0a, 0x1b, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x74, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, + 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x06, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x75, 0x64, + 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x75, 0x64, + 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, + 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x3c, 0x0a, 0x04, 0x68, 0x74, + 0x74, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x48, 0x74, 0x74, 0x70, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x04, 0x68, 0x74, 0x74, 0x70, 0x22, 0xb5, 0x01, 0x0a, 0x13, 0x41, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x1f, 0x0a, 0x0b, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x43, 0x6c, 0x61, 0x69, + 0x6d, 0x12, 0x26, 0x0a, 0x0f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x63, + 0x6c, 0x61, 0x69, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x49, 0x64, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x55, 0x0a, 0x08, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x64, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x22, 0x77, 0x0a, 0x13, 0x47, 0x72, 0x70, 0x63, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, + 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x74, + 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x74, 0x69, + 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x74, 0x5f, + 0x77, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x5f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x74, 0x57, 0x69, 0x74, 0x68, + 0x6f, 0x75, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x22, 0xbd, 0x01, 0x0a, 0x14, 0x47, 0x72, + 0x70, 0x63, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x50, 0x0a, 0x0a, + 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x31, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, + 0x47, 0x72, 0x70, 0x63, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x12, 0x39, + 0x0a, 0x03, 0x74, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x64, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x6c, 0x73, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x03, 0x74, 0x6c, 0x73, 0x22, 0x5a, 0x0a, 0x10, 0x47, 0x72, 0x70, + 0x63, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x46, 0x0a, + 0x04, 0x67, 0x72, 0x70, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x64, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x72, 0x70, 0x63, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x04, 0x67, 0x72, 0x70, 0x63, 0x22, 0xd3, 0x02, 0x0a, 0x03, 0x48, 0x75, 0x62, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, + 0x08, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x08, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x12, 0x63, 0x0a, 0x15, 0x63, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x72, 0x70, 0x63, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x14, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x57, + 0x0a, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x68, + 0x75, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x68, 0x75, 0x62, + 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, + 0x08, 0x03, 0x10, 0x04, 0x52, 0x07, 0x63, 0x61, 0x5f, 0x70, 0x6f, 0x6f, 0x6c, 0x52, 0x0c, 0x63, + 0x6f, 0x61, 0x70, 0x5f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x22, 0xba, 0x02, 0x0a, 0x10, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x48, 0x75, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x15, 0x0a, 0x06, 0x68, 0x75, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x68, 0x75, 0x62, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x67, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x67, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x73, 0x12, 0x63, 0x0a, 0x15, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, + 0x62, 0x2e, 0x47, 0x72, 0x70, 0x63, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x14, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x57, 0x0a, 0x0d, 0x61, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x31, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, 0x08, 0x03, 0x10, + 0x04, 0x52, 0x07, 0x63, 0x61, 0x5f, 0x70, 0x6f, 0x6f, 0x6c, 0x52, 0x0c, 0x63, 0x6f, 0x61, 0x70, + 0x5f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x22, 0x51, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x48, + 0x75, 0x62, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x64, + 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x69, + 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x22, 0x0a, 0x0d, 0x68, 0x75, 0x62, 0x5f, 0x69, + 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, + 0x68, 0x75, 0x62, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0xb3, 0x02, 0x0a, 0x09, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x75, 0x62, 0x12, 0x1a, 0x0a, 0x08, 0x67, 0x61, 0x74, + 0x65, 0x77, 0x61, 0x79, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x67, 0x61, 0x74, + 0x65, 0x77, 0x61, 0x79, 0x73, 0x12, 0x63, 0x0a, 0x15, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x72, 0x70, 0x63, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x14, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x57, 0x0a, 0x0d, 0x61, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x31, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, + 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x68, 0x75, 0x62, 0x5f, 0x69, + 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x68, 0x75, 0x62, 0x49, 0x64, 0x4a, 0x04, + 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x52, 0x07, 0x63, 0x61, 0x5f, 0x70, + 0x6f, 0x6f, 0x6c, 0x52, 0x0c, 0x63, 0x6f, 0x61, 0x70, 0x5f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, + 0x79, 0x22, 0x5d, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x75, 0x62, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x39, 0x0a, 0x03, 0x68, 0x75, 0x62, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, + 0x62, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x75, 0x62, 0x52, 0x03, 0x68, 0x75, 0x62, + 0x22, 0x30, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x75, 0x62, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x22, 0x2a, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x75, 0x62, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x3e, + 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x67, + 0x64, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x68, 0x75, 0x62, 0x2f, 0x76, 0x32, 0x2f, 0x64, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x2d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, + 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x70, 0x62, 0x3b, 0x70, 0x62, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescOnce sync.Once + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescData = file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDesc +) + +func file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescGZIP() []byte { + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescOnce.Do(func() { + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescData = protoimpl.X.CompressGZIP(file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescData) + }) + return file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDescData +} + +var file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes = make([]protoimpl.MessageInfo, 14) +var file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_goTypes = []any{ + (*TlsConfig)(nil), // 0: deviceprovisioningservice.pb.TlsConfig + (*HttpConfig)(nil), // 1: deviceprovisioningservice.pb.HttpConfig + (*AuthorizationProviderConfig)(nil), // 2: deviceprovisioningservice.pb.AuthorizationProviderConfig + (*AuthorizationConfig)(nil), // 3: deviceprovisioningservice.pb.AuthorizationConfig + (*GrpcKeepAliveConfig)(nil), // 4: deviceprovisioningservice.pb.GrpcKeepAliveConfig + (*GrpcConnectionConfig)(nil), // 5: deviceprovisioningservice.pb.GrpcConnectionConfig + (*GrpcClientConfig)(nil), // 6: deviceprovisioningservice.pb.GrpcClientConfig + (*Hub)(nil), // 7: deviceprovisioningservice.pb.Hub + (*CreateHubRequest)(nil), // 8: deviceprovisioningservice.pb.CreateHubRequest + (*GetHubsRequest)(nil), // 9: deviceprovisioningservice.pb.GetHubsRequest + (*UpdateHub)(nil), // 10: deviceprovisioningservice.pb.UpdateHub + (*UpdateHubRequest)(nil), // 11: deviceprovisioningservice.pb.UpdateHubRequest + (*DeleteHubsRequest)(nil), // 12: deviceprovisioningservice.pb.DeleteHubsRequest + (*DeleteHubsResponse)(nil), // 13: deviceprovisioningservice.pb.DeleteHubsResponse +} +var file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_depIdxs = []int32{ + 0, // 0: deviceprovisioningservice.pb.HttpConfig.tls:type_name -> deviceprovisioningservice.pb.TlsConfig + 1, // 1: deviceprovisioningservice.pb.AuthorizationProviderConfig.http:type_name -> deviceprovisioningservice.pb.HttpConfig + 2, // 2: deviceprovisioningservice.pb.AuthorizationConfig.provider:type_name -> deviceprovisioningservice.pb.AuthorizationProviderConfig + 4, // 3: deviceprovisioningservice.pb.GrpcConnectionConfig.keep_alive:type_name -> deviceprovisioningservice.pb.GrpcKeepAliveConfig + 0, // 4: deviceprovisioningservice.pb.GrpcConnectionConfig.tls:type_name -> deviceprovisioningservice.pb.TlsConfig + 5, // 5: deviceprovisioningservice.pb.GrpcClientConfig.grpc:type_name -> deviceprovisioningservice.pb.GrpcConnectionConfig + 6, // 6: deviceprovisioningservice.pb.Hub.certificate_authority:type_name -> deviceprovisioningservice.pb.GrpcClientConfig + 3, // 7: deviceprovisioningservice.pb.Hub.authorization:type_name -> deviceprovisioningservice.pb.AuthorizationConfig + 6, // 8: deviceprovisioningservice.pb.CreateHubRequest.certificate_authority:type_name -> deviceprovisioningservice.pb.GrpcClientConfig + 3, // 9: deviceprovisioningservice.pb.CreateHubRequest.authorization:type_name -> deviceprovisioningservice.pb.AuthorizationConfig + 6, // 10: deviceprovisioningservice.pb.UpdateHub.certificate_authority:type_name -> deviceprovisioningservice.pb.GrpcClientConfig + 3, // 11: deviceprovisioningservice.pb.UpdateHub.authorization:type_name -> deviceprovisioningservice.pb.AuthorizationConfig + 10, // 12: deviceprovisioningservice.pb.UpdateHubRequest.hub:type_name -> deviceprovisioningservice.pb.UpdateHub + 13, // [13:13] is the sub-list for method output_type + 13, // [13:13] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name +} + +func init() { file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_init() } +func file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_init() { + if File_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*TlsConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*HttpConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[2].Exporter = func(v any, i int) any { + switch v := v.(*AuthorizationProviderConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[3].Exporter = func(v any, i int) any { + switch v := v.(*AuthorizationConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[4].Exporter = func(v any, i int) any { + switch v := v.(*GrpcKeepAliveConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[5].Exporter = func(v any, i int) any { + switch v := v.(*GrpcConnectionConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[6].Exporter = func(v any, i int) any { + switch v := v.(*GrpcClientConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[7].Exporter = func(v any, i int) any { + switch v := v.(*Hub); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[8].Exporter = func(v any, i int) any { + switch v := v.(*CreateHubRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[9].Exporter = func(v any, i int) any { + switch v := v.(*GetHubsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[10].Exporter = func(v any, i int) any { + switch v := v.(*UpdateHub); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[11].Exporter = func(v any, i int) any { + switch v := v.(*UpdateHubRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[12].Exporter = func(v any, i int) any { + switch v := v.(*DeleteHubsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes[13].Exporter = func(v any, i int) any { + switch v := v.(*DeleteHubsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDesc, + NumEnums: 0, + NumMessages: 14, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_goTypes, + DependencyIndexes: file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_depIdxs, + MessageInfos: file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_msgTypes, + }.Build() + File_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto = out.File + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_rawDesc = nil + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_goTypes = nil + file_github_com_plgd_dev_hub_device_provisioning_service_pb_hub_proto_depIdxs = nil +} diff --git a/device-provisioning-service/pb/hub.proto b/device-provisioning-service/pb/hub.proto new file mode 100644 index 000000000..39f17bde4 --- /dev/null +++ b/device-provisioning-service/pb/hub.proto @@ -0,0 +1,186 @@ +syntax = "proto3"; + +package deviceprovisioningservice.pb; + +option go_package = "github.com/plgd-dev/hub/v2/device-provisioning-service/pb;pb"; + + +message TlsConfig{ + // the root certificates. Supported formats: , + repeated string ca_pool = 1; // @gotags: bson:"ca_pool" + // private key. Supported formats: , + string key = 2; // @gotags: bson:"key" + // certificate. Supported formats: , + string cert = 3; // @gotags: bson:"cert" + // use system certification pool + bool use_system_ca_pool= 4; // @gotags: bson:"useSystemCaPool" +} + +message HttpConfig { + // MaxIdleConns controls the maximum number of idle (keep-alive) + // connections across all hosts. Zero means no limit. + uint32 max_idle_conns = 1; // @gotags: bson:"maxIdleConns" + + // MaxConnsPerHost optionally limits the total number of + // connections per host, including connections in the dialing, + // active, and idle states. On limit violation, dials will block. + // + // Zero means no limit. + uint32 max_conns_per_host = 2; // @gotags: bson:"maxConnsPerHost" + + // MaxIdleConnsPerHost, if non-zero, controls the maximum idle + // (keep-alive) connections to keep per-host. If zero, + // DefaultMaxIdleConnsPerHost is used. + uint32 max_idle_conns_per_host = 3; // @gotags: bson:"maxIdleConnsPerHost" + + // IdleConnTimeout is the maximum amount of time an idle + // (keep-alive) connection will remain idle before closing + // itself in nanoseconds. + // Zero means no limit. + int64 idle_conn_timeout = 4; // @gotags: bson:"idleConnTimeout" + + // Timeout specifies a time limit for requests made by this + // Client in nanoseconds. The timeout includes connection time, any + // redirects, and reading the response body. The timer remains + // running after Get, Head, Post, or Do return and will + // interrupt reading of the Response.Body. + // + // A Timeout of zero means no timeout. + // + // The Client cancels requests to the underlying Transport + // as if the Request's Context ended. + // + // For compatibility, the Client will also use the deprecated + // CancelRequest method on Transport if found. New + // RoundTripper implementations should use the Request's Context + // for cancellation instead of implementing CancelRequest. + int64 timeout = 5; // @gotags: bson:"timeout" + + TlsConfig tls = 6; // @gotags: bson:"tls" +} + +message AuthorizationProviderConfig { + // the name of the provider, which is set in configuration in coap-gateway + string name = 1; // @gotags: bson:"name" + // the url to get oauth endpoints + string authority = 2; // @gotags: bson:"authority" + // client id which is associated to the name + string client_id = 3; // @gotags: bson:"clientId" + // scopes will be set in token + repeated string scopes = 4; // @gotags: bson:"scopes" + // audience will be set in token + string audience = 5; // @gotags: bson:"audience" + // client secret. Supported formats: , + string client_secret = 6; // @gotags: bson:"clientSecret" + // http configuration + HttpConfig http = 7; // @gotags: bson:"http" +} + +// AuthorizationConfig is used to generate the authorization code for the device when providing cloud configuration. +message AuthorizationConfig { + // owner_claim is key where will be stored owner in JWT. + string owner_claim = 1; // @gotags: bson:"ownerClaim" + // device_id_claim is key where will be stored deviceID in JWT(optional) + string device_id_claim = 2; // @gotags: bson:"deviceIdClaim" + AuthorizationProviderConfig provider = 3; // @gotags: bson:"provider" +} + +message GrpcKeepAliveConfig { + // After a duration in nanoseconds of this time if the client doesn't see any activity it + // pings the server to see if the transport is still alive. + // The zero value is infinity and if it set below 10s, a minimum value of 10s will be used instead. + int64 time = 1; // @gotags: bson:"time" + // After having pinged for keepalive check, the client waits for a duration + // of Timeout and if no activity is seen even after that the connection is + // closed. + int64 timeout = 2; // @gotags: bson:"timeout" + // If true, client sends keepalive pings even with no active RPCs. If false, + // when there are no active RPCs, Time and Timeout will be ignored and no + // keepalive pings will be sent. + bool permit_without_stream = 3; // @gotags: bson:"permitWithoutStream +} + +message GrpcConnectionConfig { + // Address in format {host:port} + string address = 1; // @gotags: bson:"address" + GrpcKeepAliveConfig keep_alive = 2; // @gotags: bson:"keepAlive" + TlsConfig tls = 3; // @gotags: bson:"tls" +} + +message GrpcClientConfig { + // GRPC protocol + GrpcConnectionConfig grpc = 1; // @gotags: bson:"grpc" +} + +message Hub { + reserved 2,3; // string ca_pool = 2; string coap_gateway = 3; + reserved "ca_pool","coap_gateway"; + // Record ID. + string id = 1; // @gotags: bson:"_id" + // Address of gateway in format scheme://host:port + repeated string gateways = 8; // @gotags: bson:"gateways" + // Signs identity ceritificate for the device. + GrpcClientConfig certificate_authority = 4; // @gotags: bson:"certificateAuthority" + // Acquire HUB authorization code for the device. + AuthorizationConfig authorization = 5; // @gotags: bson:"authorization" + // Hub name. + string name = 6; // @gotags: bson:"name" + // Hub identifier - it must match with common name of gateway(coap-gateway) hub certificate. + string hub_id = 7; // @gotags: bson:"hubId" + // Owner of the hub + string owner = 9; // @gotags: bson:"owner" +} + +message CreateHubRequest { + reserved 2,3; // string ca_pool = 1; string coap_gateway = 3; + reserved "ca_pool","coap_gateway"; + // Hub identifier - it must match with common name of gateway(coap-gateway) hub certificate. + string hub_id = 1; + // Address of gateways in format scheme://host:port + repeated string gateways = 7; + // Signs identity ceritificate for the device. + GrpcClientConfig certificate_authority = 4; + // Acquire HUB authorization code for the device. + AuthorizationConfig authorization = 5; + // Hub name. + string name = 6; +} + +message GetHubsRequest { + // Filter by id. + repeated string id_filter = 1; + // Filter by hub_id. + repeated string hub_id_filter = 2; +} + +message UpdateHub { + reserved 1,2; // string ca_pool = 1; string coap_gateway = 2; + reserved "ca_pool","coap_gateway"; + + // Address of coap-gateway in format scheme://host:port + repeated string gateways = 7; + // Signs identity ceritificate for the device. + GrpcClientConfig certificate_authority = 3; + // Acquire HUB authorization code for the device. + AuthorizationConfig authorization = 4; + // Hub name. + string name = 5; + // Hub ID + string hub_id = 6; +} + +message UpdateHubRequest { + // Record ID. + string id = 1; + UpdateHub hub = 2; +} + +message DeleteHubsRequest { + // Record ID. + repeated string id_filter = 1; +} + +message DeleteHubsResponse { + // Number of deleted records. + int64 count = 1; +} diff --git a/device-provisioning-service/pb/provisioningRecords.go b/device-provisioning-service/pb/provisioningRecords.go new file mode 100644 index 000000000..a7ab4e489 --- /dev/null +++ b/device-provisioning-service/pb/provisioningRecords.go @@ -0,0 +1,286 @@ +package pb + +import ( + "sort" + + "github.com/plgd-dev/device/v2/schema/acl" + "github.com/plgd-dev/device/v2/schema/credential" +) + +type ProvisioningRecords []*ProvisioningRecord + +func (p ProvisioningRecords) Sort() { + sort.Slice(p, func(i, j int) bool { + return p[i].GetId() < p[j].GetId() + }) +} + +func AccessControlSubjectToDevicePb(subject acl.Subject) *AccessControlDeviceSubject { + if subject.Subject_Device == nil { + return nil + } + return &AccessControlDeviceSubject{ + DeviceId: subject.Subject_Device.DeviceID, + } +} + +func AccessControlSubjectToRolePb(subject acl.Subject) *AccessControlRoleSubject { + if subject.Subject_Role == nil { + return nil + } + return &AccessControlRoleSubject{ + Role: subject.Subject_Role.Role, + Authority: subject.Subject_Role.Authority, + } +} + +func AccessControlSubjectToConnectionPb(subject acl.Subject) *AccessControlConnectionSubject { + if subject.Subject_Connection == nil { + return nil + } + var v AccessControlConnectionSubject_ConnectionType + switch subject.Subject_Connection.Type { + case acl.ConnectionType_ANON_CLEAR: + v = AccessControlConnectionSubject_ANON_CLEAR + case acl.ConnectionType_AUTH_CRYPT: + v = AccessControlConnectionSubject_AUTH_CRYPT + } + return &AccessControlConnectionSubject{ + Type: v, + } +} + +func AccessControlPermissionToPb(permission acl.Permission) []AccessControl_Permission { + var res []AccessControl_Permission + if permission.Has(acl.Permission_CREATE) { + res = append(res, AccessControl_CREATE) + } + if permission.Has(acl.Permission_READ) { + res = append(res, AccessControl_READ) + } + if permission.Has(acl.Permission_WRITE) { + res = append(res, AccessControl_WRITE) + } + if permission.Has(acl.Permission_DELETE) { + res = append(res, AccessControl_DELETE) + } + if permission.Has(acl.Permission_NOTIFY) { + res = append(res, AccessControl_NOTIFY) + } + return res +} + +func AccessControlResourcesToPb(resources []acl.Resource) []*AccessControlResource { + res := make([]*AccessControlResource, 0, len(resources)) + for _, r := range resources { + wildcard := AccessControlResource_NONE + switch r.Wildcard { + case acl.ResourceWildcard_NONCFG_SEC_ENDPOINT: + wildcard = AccessControlResource_NONCFG_SEC_ENDPOINT + case acl.ResourceWildcard_NONCFG_NONSEC_ENDPOINT: + wildcard = AccessControlResource_NONCFG_NONSEC_ENDPOINT + case acl.ResourceWildcard_NONCFG_ALL: + wildcard = AccessControlResource_NONCFG_ALL + } + res = append(res, &AccessControlResource{ + Href: r.Href, + Interfaces: r.Interfaces, + ResourceTypes: r.ResourceTypes, + Wildcard: wildcard, + }) + } + return res +} + +func DeviceAccessControlToPb(ac acl.AccessControl) *AccessControl { + return &AccessControl{ + DeviceSubject: AccessControlSubjectToDevicePb(ac.Subject), + RoleSubject: AccessControlSubjectToRolePb(ac.Subject), + ConnectionSubject: AccessControlSubjectToConnectionPb(ac.Subject), + Permissions: AccessControlPermissionToPb(ac.Permission), + Resources: AccessControlResourcesToPb(ac.Resources), + } +} + +func DeviceAccessControlListToPb(acls []acl.AccessControl) []*AccessControl { + acl := make([]*AccessControl, 0, len(acls)) + for _, ac := range acls { + acl = append(acl, DeviceAccessControlToPb(ac)) + } + return acl +} + +var credentialTypes = map[credential.CredentialType]Credential_CredentialType{ + credential.CredentialType_SYMMETRIC_PAIR_WISE: Credential_SYMMETRIC_PAIR_WISE, + credential.CredentialType_SYMMETRIC_GROUP: Credential_SYMMETRIC_GROUP, + credential.CredentialType_ASYMMETRIC_SIGNING: Credential_ASYMMETRIC_SIGNING, + credential.CredentialType_ASYMMETRIC_SIGNING_WITH_CERTIFICATE: Credential_ASYMMETRIC_SIGNING_WITH_CERTIFICATE, + credential.CredentialType_PIN_OR_PASSWORD: Credential_PIN_OR_PASSWORD, + credential.CredentialType_ASYMMETRIC_ENCRYPTION_KEY: Credential_ASYMMETRIC_ENCRYPTION_KEY, +} + +func CredentialTypeToPb(t credential.CredentialType) []Credential_CredentialType { + var types []Credential_CredentialType + if t == credential.CredentialType_EMPTY { + return types + } + for key, ct := range credentialTypes { + if t.Has(key) { + types = append(types, ct) + } + } + return types +} + +var credentialUsages = map[credential.CredentialUsage]Credential_CredentialUsage{ + credential.CredentialUsage_CERT: Credential_CERT, + credential.CredentialUsage_MFG_CERT: Credential_MFG_CERT, + credential.CredentialUsage_MFG_TRUST_CA: Credential_MFG_TRUST_CA, + credential.CredentialUsage_TRUST_CA: Credential_TRUST_CA, + credential.CredentialUsage_ROLE_CERT: Credential_ROLE_CERT, +} + +func CredentialUsageToPb(u credential.CredentialUsage) Credential_CredentialUsage { + v, ok := credentialUsages[u] + if !ok { + return Credential_NONE + } + return v +} + +func CredentialRoleIDToPb(roleID *credential.CredentialRoleID) *CredentialRoleID { + if roleID == nil { + return nil + } + return &CredentialRoleID{ + Authority: roleID.Authority, + Role: roleID.Role, + } +} + +var credentialRefreshMethods = map[credential.CredentialRefreshMethod]Credential_CredentialRefreshMethod{ + credential.CredentialRefreshMethod_KEY_AGREEMENT_PROTOCOL: Credential_KEY_AGREEMENT_PROTOCOL, + credential.CredentialRefreshMethod_KEY_AGREEMENT_PROTOCOL_AND_RANDOM_PIN: Credential_KEY_AGREEMENT_PROTOCOL_AND_RANDOM_PIN, + credential.CredentialRefreshMethod_PROVISION_SERVICE: Credential_PROVISION_SERVICE, + credential.CredentialRefreshMethod_KEY_DISTRIBUTION_SERVICE: Credential_KEY_DISTRIBUTION_SERVICE, + credential.CredentialRefreshMethod_PKCS10_REQUEST_TO_CA: Credential_PKCS10_REQUEST_TO_CA, +} + +func CredentialSupportedRefreshMethodsToPb(methods []credential.CredentialRefreshMethod) []Credential_CredentialRefreshMethod { + var res []Credential_CredentialRefreshMethod + for _, m := range methods { + if v, ok := credentialRefreshMethods[m]; ok { + res = append(res, v) + } else { + res = append(res, Credential_UNKNOWN) + } + } + return res +} + +var credentialOptionalDataEncodings = map[credential.CredentialOptionalDataEncoding]CredentialOptionalData_Encoding{ + credential.CredentialOptionalDataEncoding_BASE64: CredentialOptionalData_BASE64, + credential.CredentialOptionalDataEncoding_RAW: CredentialOptionalData_RAW, + credential.CredentialOptionalDataEncoding_CWT: CredentialOptionalData_CWT, + credential.CredentialOptionalDataEncoding_JWT: CredentialOptionalData_JWT, + credential.CredentialOptionalDataEncoding_DER: CredentialOptionalData_DER, + credential.CredentialOptionalDataEncoding_PEM: CredentialOptionalData_PEM, +} + +func CredentialOptionalDataEncodingToPb(encoding credential.CredentialOptionalDataEncoding) CredentialOptionalData_Encoding { + v, ok := credentialOptionalDataEncodings[encoding] + if !ok { + return CredentialOptionalData_UNKNOWN + } + return v +} + +func CredentialOptionalDataToPb(data *credential.CredentialOptionalData) *CredentialOptionalData { + if data == nil { + return nil + } + return &CredentialOptionalData{ + Data: data.Data(), + Encoding: CredentialOptionalDataEncodingToPb(data.Encoding), + IsRevoked: data.IsRevoked, + } +} + +var credentialPrivateDataEncodings = map[credential.CredentialPrivateDataEncoding]CredentialPrivateData_Encoding{ + credential.CredentialPrivateDataEncoding_BASE64: CredentialPrivateData_BASE64, + credential.CredentialPrivateDataEncoding_RAW: CredentialPrivateData_RAW, + credential.CredentialPrivateDataEncoding_CWT: CredentialPrivateData_CWT, + credential.CredentialPrivateDataEncoding_JWT: CredentialPrivateData_JWT, + credential.CredentialPrivateDataEncoding_URI: CredentialPrivateData_URI, + credential.CredentialPrivateDataEncoding_HANDLE: CredentialPrivateData_HANDLE, +} + +func CredentialPrivateDataEncodingToPb(encoding credential.CredentialPrivateDataEncoding) CredentialPrivateData_Encoding { + v, ok := credentialPrivateDataEncodings[encoding] + if !ok { + return CredentialPrivateData_UNKNOWN + } + return v +} + +func CredentialPrivateDataToPb(data *credential.CredentialPrivateData) *CredentialPrivateData { + if data == nil { + return nil + } + return &CredentialPrivateData{ + Data: data.Data(), + Encoding: CredentialPrivateDataEncodingToPb(data.Encoding), + Handle: int64(data.Handle), + } +} + +var credentialPublicDataEncodings = map[credential.CredentialPublicDataEncoding]CredentialPublicData_Encoding{ + credential.CredentialPublicDataEncoding_BASE64: CredentialPublicData_BASE64, + credential.CredentialPublicDataEncoding_RAW: CredentialPublicData_RAW, + credential.CredentialPublicDataEncoding_CWT: CredentialPublicData_CWT, + credential.CredentialPublicDataEncoding_JWT: CredentialPublicData_JWT, + credential.CredentialPublicDataEncoding_URI: CredentialPublicData_URI, + credential.CredentialPublicDataEncoding_DER: CredentialPublicData_DER, + credential.CredentialPublicDataEncoding_PEM: CredentialPublicData_PEM, +} + +func CredentialPublicDataEncodingToPb(encoding credential.CredentialPublicDataEncoding) CredentialPublicData_Encoding { + v, ok := credentialPublicDataEncodings[encoding] + if !ok { + return CredentialPublicData_UNKNOWN + } + return v +} + +func CredentialPublicDataToPb(data *credential.CredentialPublicData) *CredentialPublicData { + if data == nil { + return nil + } + return &CredentialPublicData{ + Data: data.Data(), + Encoding: CredentialPublicDataEncodingToPb(data.Encoding), + } +} + +func CredentialToPb(cred credential.Credential) *Credential { + return &Credential{ + Id: int64(cred.ID), + Type: CredentialTypeToPb(cred.Type), + Usage: CredentialUsageToPb(cred.Usage), + Subject: cred.Subject, + Period: cred.Period, + RoleId: CredentialRoleIDToPb(cred.RoleID), + SupportedRefreshMethods: CredentialSupportedRefreshMethodsToPb(cred.SupportedRefreshMethods), + OptionalData: CredentialOptionalDataToPb(cred.OptionalData), + PrivateData: CredentialPrivateDataToPb(cred.PrivateData), + PublicData: CredentialPublicDataToPb(cred.PublicData), + } +} + +func CredentialsToPb(creds []credential.Credential) []*Credential { + pbCreds := make([]*Credential, 0, len(creds)) + for _, c := range creds { + pbCreds = append(pbCreds, CredentialToPb(c)) + } + return pbCreds +} diff --git a/device-provisioning-service/pb/provisioningRecords.pb.go b/device-provisioning-service/pb/provisioningRecords.pb.go new file mode 100644 index 000000000..c3b285139 --- /dev/null +++ b/device-provisioning-service/pb/provisioningRecords.pb.go @@ -0,0 +1,2858 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v5.27.3 +// source: device-provisioning-service/pb/provisioningRecords.proto + +package pb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CredentialOptionalData_Encoding int32 + +const ( + CredentialOptionalData_UNKNOWN CredentialOptionalData_Encoding = 0 + CredentialOptionalData_RAW CredentialOptionalData_Encoding = 1 + CredentialOptionalData_JWT CredentialOptionalData_Encoding = 2 + CredentialOptionalData_CWT CredentialOptionalData_Encoding = 3 + CredentialOptionalData_BASE64 CredentialOptionalData_Encoding = 4 + CredentialOptionalData_PEM CredentialOptionalData_Encoding = 5 + CredentialOptionalData_DER CredentialOptionalData_Encoding = 6 +) + +// Enum value maps for CredentialOptionalData_Encoding. +var ( + CredentialOptionalData_Encoding_name = map[int32]string{ + 0: "UNKNOWN", + 1: "RAW", + 2: "JWT", + 3: "CWT", + 4: "BASE64", + 5: "PEM", + 6: "DER", + } + CredentialOptionalData_Encoding_value = map[string]int32{ + "UNKNOWN": 0, + "RAW": 1, + "JWT": 2, + "CWT": 3, + "BASE64": 4, + "PEM": 5, + "DER": 6, + } +) + +func (x CredentialOptionalData_Encoding) Enum() *CredentialOptionalData_Encoding { + p := new(CredentialOptionalData_Encoding) + *p = x + return p +} + +func (x CredentialOptionalData_Encoding) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CredentialOptionalData_Encoding) Descriptor() protoreflect.EnumDescriptor { + return file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[0].Descriptor() +} + +func (CredentialOptionalData_Encoding) Type() protoreflect.EnumType { + return &file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[0] +} + +func (x CredentialOptionalData_Encoding) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CredentialOptionalData_Encoding.Descriptor instead. +func (CredentialOptionalData_Encoding) EnumDescriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{5, 0} +} + +type CredentialPrivateData_Encoding int32 + +const ( + CredentialPrivateData_UNKNOWN CredentialPrivateData_Encoding = 0 + CredentialPrivateData_RAW CredentialPrivateData_Encoding = 1 + CredentialPrivateData_JWT CredentialPrivateData_Encoding = 2 + CredentialPrivateData_CWT CredentialPrivateData_Encoding = 3 + CredentialPrivateData_BASE64 CredentialPrivateData_Encoding = 4 + CredentialPrivateData_URI CredentialPrivateData_Encoding = 5 + CredentialPrivateData_HANDLE CredentialPrivateData_Encoding = 6 +) + +// Enum value maps for CredentialPrivateData_Encoding. +var ( + CredentialPrivateData_Encoding_name = map[int32]string{ + 0: "UNKNOWN", + 1: "RAW", + 2: "JWT", + 3: "CWT", + 4: "BASE64", + 5: "URI", + 6: "HANDLE", + } + CredentialPrivateData_Encoding_value = map[string]int32{ + "UNKNOWN": 0, + "RAW": 1, + "JWT": 2, + "CWT": 3, + "BASE64": 4, + "URI": 5, + "HANDLE": 6, + } +) + +func (x CredentialPrivateData_Encoding) Enum() *CredentialPrivateData_Encoding { + p := new(CredentialPrivateData_Encoding) + *p = x + return p +} + +func (x CredentialPrivateData_Encoding) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CredentialPrivateData_Encoding) Descriptor() protoreflect.EnumDescriptor { + return file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[1].Descriptor() +} + +func (CredentialPrivateData_Encoding) Type() protoreflect.EnumType { + return &file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[1] +} + +func (x CredentialPrivateData_Encoding) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CredentialPrivateData_Encoding.Descriptor instead. +func (CredentialPrivateData_Encoding) EnumDescriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{6, 0} +} + +type CredentialPublicData_Encoding int32 + +const ( + CredentialPublicData_UNKNOWN CredentialPublicData_Encoding = 0 + CredentialPublicData_RAW CredentialPublicData_Encoding = 1 + CredentialPublicData_JWT CredentialPublicData_Encoding = 2 + CredentialPublicData_CWT CredentialPublicData_Encoding = 3 + CredentialPublicData_BASE64 CredentialPublicData_Encoding = 4 + CredentialPublicData_URI CredentialPublicData_Encoding = 5 + CredentialPublicData_PEM CredentialPublicData_Encoding = 6 + CredentialPublicData_DER CredentialPublicData_Encoding = 7 +) + +// Enum value maps for CredentialPublicData_Encoding. +var ( + CredentialPublicData_Encoding_name = map[int32]string{ + 0: "UNKNOWN", + 1: "RAW", + 2: "JWT", + 3: "CWT", + 4: "BASE64", + 5: "URI", + 6: "PEM", + 7: "DER", + } + CredentialPublicData_Encoding_value = map[string]int32{ + "UNKNOWN": 0, + "RAW": 1, + "JWT": 2, + "CWT": 3, + "BASE64": 4, + "URI": 5, + "PEM": 6, + "DER": 7, + } +) + +func (x CredentialPublicData_Encoding) Enum() *CredentialPublicData_Encoding { + p := new(CredentialPublicData_Encoding) + *p = x + return p +} + +func (x CredentialPublicData_Encoding) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CredentialPublicData_Encoding) Descriptor() protoreflect.EnumDescriptor { + return file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[2].Descriptor() +} + +func (CredentialPublicData_Encoding) Type() protoreflect.EnumType { + return &file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[2] +} + +func (x CredentialPublicData_Encoding) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CredentialPublicData_Encoding.Descriptor instead. +func (CredentialPublicData_Encoding) EnumDescriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{7, 0} +} + +type Credential_CredentialType int32 + +const ( + Credential_EMPTY Credential_CredentialType = 0 + Credential_SYMMETRIC_PAIR_WISE Credential_CredentialType = 1 + Credential_SYMMETRIC_GROUP Credential_CredentialType = 2 + Credential_ASYMMETRIC_SIGNING Credential_CredentialType = 4 + Credential_ASYMMETRIC_SIGNING_WITH_CERTIFICATE Credential_CredentialType = 8 + Credential_PIN_OR_PASSWORD Credential_CredentialType = 16 + Credential_ASYMMETRIC_ENCRYPTION_KEY Credential_CredentialType = 32 +) + +// Enum value maps for Credential_CredentialType. +var ( + Credential_CredentialType_name = map[int32]string{ + 0: "EMPTY", + 1: "SYMMETRIC_PAIR_WISE", + 2: "SYMMETRIC_GROUP", + 4: "ASYMMETRIC_SIGNING", + 8: "ASYMMETRIC_SIGNING_WITH_CERTIFICATE", + 16: "PIN_OR_PASSWORD", + 32: "ASYMMETRIC_ENCRYPTION_KEY", + } + Credential_CredentialType_value = map[string]int32{ + "EMPTY": 0, + "SYMMETRIC_PAIR_WISE": 1, + "SYMMETRIC_GROUP": 2, + "ASYMMETRIC_SIGNING": 4, + "ASYMMETRIC_SIGNING_WITH_CERTIFICATE": 8, + "PIN_OR_PASSWORD": 16, + "ASYMMETRIC_ENCRYPTION_KEY": 32, + } +) + +func (x Credential_CredentialType) Enum() *Credential_CredentialType { + p := new(Credential_CredentialType) + *p = x + return p +} + +func (x Credential_CredentialType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Credential_CredentialType) Descriptor() protoreflect.EnumDescriptor { + return file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[3].Descriptor() +} + +func (Credential_CredentialType) Type() protoreflect.EnumType { + return &file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[3] +} + +func (x Credential_CredentialType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Credential_CredentialType.Descriptor instead. +func (Credential_CredentialType) EnumDescriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{9, 0} +} + +type Credential_CredentialUsage int32 + +const ( + Credential_NONE Credential_CredentialUsage = 0 + Credential_TRUST_CA Credential_CredentialUsage = 1 + Credential_CERT Credential_CredentialUsage = 2 + Credential_ROLE_CERT Credential_CredentialUsage = 3 + Credential_MFG_TRUST_CA Credential_CredentialUsage = 4 + Credential_MFG_CERT Credential_CredentialUsage = 5 +) + +// Enum value maps for Credential_CredentialUsage. +var ( + Credential_CredentialUsage_name = map[int32]string{ + 0: "NONE", + 1: "TRUST_CA", + 2: "CERT", + 3: "ROLE_CERT", + 4: "MFG_TRUST_CA", + 5: "MFG_CERT", + } + Credential_CredentialUsage_value = map[string]int32{ + "NONE": 0, + "TRUST_CA": 1, + "CERT": 2, + "ROLE_CERT": 3, + "MFG_TRUST_CA": 4, + "MFG_CERT": 5, + } +) + +func (x Credential_CredentialUsage) Enum() *Credential_CredentialUsage { + p := new(Credential_CredentialUsage) + *p = x + return p +} + +func (x Credential_CredentialUsage) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Credential_CredentialUsage) Descriptor() protoreflect.EnumDescriptor { + return file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[4].Descriptor() +} + +func (Credential_CredentialUsage) Type() protoreflect.EnumType { + return &file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[4] +} + +func (x Credential_CredentialUsage) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Credential_CredentialUsage.Descriptor instead. +func (Credential_CredentialUsage) EnumDescriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{9, 1} +} + +type Credential_CredentialRefreshMethod int32 + +const ( + Credential_UNKNOWN Credential_CredentialRefreshMethod = 0 + Credential_PROVISION_SERVICE Credential_CredentialRefreshMethod = 1 + Credential_KEY_AGREEMENT_PROTOCOL_AND_RANDOM_PIN Credential_CredentialRefreshMethod = 2 + Credential_KEY_AGREEMENT_PROTOCOL Credential_CredentialRefreshMethod = 3 + Credential_KEY_DISTRIBUTION_SERVICE Credential_CredentialRefreshMethod = 4 + Credential_PKCS10_REQUEST_TO_CA Credential_CredentialRefreshMethod = 5 +) + +// Enum value maps for Credential_CredentialRefreshMethod. +var ( + Credential_CredentialRefreshMethod_name = map[int32]string{ + 0: "UNKNOWN", + 1: "PROVISION_SERVICE", + 2: "KEY_AGREEMENT_PROTOCOL_AND_RANDOM_PIN", + 3: "KEY_AGREEMENT_PROTOCOL", + 4: "KEY_DISTRIBUTION_SERVICE", + 5: "PKCS10_REQUEST_TO_CA", + } + Credential_CredentialRefreshMethod_value = map[string]int32{ + "UNKNOWN": 0, + "PROVISION_SERVICE": 1, + "KEY_AGREEMENT_PROTOCOL_AND_RANDOM_PIN": 2, + "KEY_AGREEMENT_PROTOCOL": 3, + "KEY_DISTRIBUTION_SERVICE": 4, + "PKCS10_REQUEST_TO_CA": 5, + } +) + +func (x Credential_CredentialRefreshMethod) Enum() *Credential_CredentialRefreshMethod { + p := new(Credential_CredentialRefreshMethod) + *p = x + return p +} + +func (x Credential_CredentialRefreshMethod) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Credential_CredentialRefreshMethod) Descriptor() protoreflect.EnumDescriptor { + return file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[5].Descriptor() +} + +func (Credential_CredentialRefreshMethod) Type() protoreflect.EnumType { + return &file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[5] +} + +func (x Credential_CredentialRefreshMethod) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Credential_CredentialRefreshMethod.Descriptor instead. +func (Credential_CredentialRefreshMethod) EnumDescriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{9, 2} +} + +type AccessControlConnectionSubject_ConnectionType int32 + +const ( + // anonymous clear-text connection TCP or UDP without encryption + AccessControlConnectionSubject_ANON_CLEAR AccessControlConnectionSubject_ConnectionType = 0 + // authenticated encrypted connection using TLS or DTLS + AccessControlConnectionSubject_AUTH_CRYPT AccessControlConnectionSubject_ConnectionType = 1 +) + +// Enum value maps for AccessControlConnectionSubject_ConnectionType. +var ( + AccessControlConnectionSubject_ConnectionType_name = map[int32]string{ + 0: "ANON_CLEAR", + 1: "AUTH_CRYPT", + } + AccessControlConnectionSubject_ConnectionType_value = map[string]int32{ + "ANON_CLEAR": 0, + "AUTH_CRYPT": 1, + } +) + +func (x AccessControlConnectionSubject_ConnectionType) Enum() *AccessControlConnectionSubject_ConnectionType { + p := new(AccessControlConnectionSubject_ConnectionType) + *p = x + return p +} + +func (x AccessControlConnectionSubject_ConnectionType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AccessControlConnectionSubject_ConnectionType) Descriptor() protoreflect.EnumDescriptor { + return file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[6].Descriptor() +} + +func (AccessControlConnectionSubject_ConnectionType) Type() protoreflect.EnumType { + return &file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[6] +} + +func (x AccessControlConnectionSubject_ConnectionType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AccessControlConnectionSubject_ConnectionType.Descriptor instead. +func (AccessControlConnectionSubject_ConnectionType) EnumDescriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{14, 0} +} + +type AccessControlResource_Wildcard int32 + +const ( + // no wildcard + AccessControlResource_NONE AccessControlResource_Wildcard = 0 + // Shall match all Discoverable Non-Configuration Resources which expose at least one Secure OCF Endpoint. + AccessControlResource_NONCFG_SEC_ENDPOINT AccessControlResource_Wildcard = 1 + // Shall match all Discoverable Non-Configuration Resources which expose at least one Unsecure OCF Endpoint. + AccessControlResource_NONCFG_NONSEC_ENDPOINT AccessControlResource_Wildcard = 2 + // Shall match all Non-Configuration Resources. + AccessControlResource_NONCFG_ALL AccessControlResource_Wildcard = 3 +) + +// Enum value maps for AccessControlResource_Wildcard. +var ( + AccessControlResource_Wildcard_name = map[int32]string{ + 0: "NONE", + 1: "NONCFG_SEC_ENDPOINT", + 2: "NONCFG_NONSEC_ENDPOINT", + 3: "NONCFG_ALL", + } + AccessControlResource_Wildcard_value = map[string]int32{ + "NONE": 0, + "NONCFG_SEC_ENDPOINT": 1, + "NONCFG_NONSEC_ENDPOINT": 2, + "NONCFG_ALL": 3, + } +) + +func (x AccessControlResource_Wildcard) Enum() *AccessControlResource_Wildcard { + p := new(AccessControlResource_Wildcard) + *p = x + return p +} + +func (x AccessControlResource_Wildcard) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AccessControlResource_Wildcard) Descriptor() protoreflect.EnumDescriptor { + return file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[7].Descriptor() +} + +func (AccessControlResource_Wildcard) Type() protoreflect.EnumType { + return &file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[7] +} + +func (x AccessControlResource_Wildcard) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AccessControlResource_Wildcard.Descriptor instead. +func (AccessControlResource_Wildcard) EnumDescriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{15, 0} +} + +type AccessControl_Permission int32 + +const ( + // create access + AccessControl_CREATE AccessControl_Permission = 0 + // read-only access + AccessControl_READ AccessControl_Permission = 1 + // read-write access + AccessControl_WRITE AccessControl_Permission = 2 + // delete access + AccessControl_DELETE AccessControl_Permission = 3 + // notify access + AccessControl_NOTIFY AccessControl_Permission = 4 +) + +// Enum value maps for AccessControl_Permission. +var ( + AccessControl_Permission_name = map[int32]string{ + 0: "CREATE", + 1: "READ", + 2: "WRITE", + 3: "DELETE", + 4: "NOTIFY", + } + AccessControl_Permission_value = map[string]int32{ + "CREATE": 0, + "READ": 1, + "WRITE": 2, + "DELETE": 3, + "NOTIFY": 4, + } +) + +func (x AccessControl_Permission) Enum() *AccessControl_Permission { + p := new(AccessControl_Permission) + *p = x + return p +} + +func (x AccessControl_Permission) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AccessControl_Permission) Descriptor() protoreflect.EnumDescriptor { + return file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[8].Descriptor() +} + +func (AccessControl_Permission) Type() protoreflect.EnumType { + return &file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes[8] +} + +func (x AccessControl_Permission) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AccessControl_Permission.Descriptor instead. +func (AccessControl_Permission) EnumDescriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{16, 0} +} + +type GetProvisioningRecordsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Filter by id. + IdFilter []string `protobuf:"bytes,1,rep,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` + // Filter by device id. + DeviceIdFilter []string `protobuf:"bytes,2,rep,name=device_id_filter,json=deviceIdFilter,proto3" json:"device_id_filter,omitempty"` + // Filter by enrollment group id. + EnrollmentGroupIdFilter []string `protobuf:"bytes,3,rep,name=enrollment_group_id_filter,json=enrollmentGroupIdFilter,proto3" json:"enrollment_group_id_filter,omitempty"` +} + +func (x *GetProvisioningRecordsRequest) Reset() { + *x = GetProvisioningRecordsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetProvisioningRecordsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProvisioningRecordsRequest) ProtoMessage() {} + +func (x *GetProvisioningRecordsRequest) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetProvisioningRecordsRequest.ProtoReflect.Descriptor instead. +func (*GetProvisioningRecordsRequest) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{0} +} + +func (x *GetProvisioningRecordsRequest) GetIdFilter() []string { + if x != nil { + return x.IdFilter + } + return nil +} + +func (x *GetProvisioningRecordsRequest) GetDeviceIdFilter() []string { + if x != nil { + return x.DeviceIdFilter + } + return nil +} + +func (x *GetProvisioningRecordsRequest) GetEnrollmentGroupIdFilter() []string { + if x != nil { + return x.EnrollmentGroupIdFilter + } + return nil +} + +type Attestation struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Last time the device successfully established a TLS connection, in unix nanoseconds timestamp format. + Date int64 `protobuf:"varint,1,opt,name=date,proto3" json:"date,omitempty" bson:"date,omitempty"` // @gotags: bson:"date,omitempty" + // X509 attestation, set if used by the device. + X509 *X509Attestation `protobuf:"bytes,2,opt,name=x509,proto3" json:"x509,omitempty" bson:"x509,omitempty"` // @gotags: bson:"x509,omitempty" +} + +func (x *Attestation) Reset() { + *x = Attestation{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Attestation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Attestation) ProtoMessage() {} + +func (x *Attestation) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Attestation.ProtoReflect.Descriptor instead. +func (*Attestation) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{1} +} + +func (x *Attestation) GetDate() int64 { + if x != nil { + return x.Date + } + return 0 +} + +func (x *Attestation) GetX509() *X509Attestation { + if x != nil { + return x.X509 + } + return nil +} + +type X509Attestation struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Last used x509 manufacturer certificate. + CertificatePem string `protobuf:"bytes,1,opt,name=certificate_pem,json=certificatePem,proto3" json:"certificate_pem,omitempty" bson:"certificate,omitempty"` // @gotags: bson:"certificate,omitempty" + CommonName string `protobuf:"bytes,2,opt,name=common_name,json=commonName,proto3" json:"common_name,omitempty" bson:"commonName,omitempty"` // @gotags: bson:"commonName,omitempty" +} + +func (x *X509Attestation) Reset() { + *x = X509Attestation{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *X509Attestation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*X509Attestation) ProtoMessage() {} + +func (x *X509Attestation) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use X509Attestation.ProtoReflect.Descriptor instead. +func (*X509Attestation) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{2} +} + +func (x *X509Attestation) GetCertificatePem() string { + if x != nil { + return x.CertificatePem + } + return "" +} + +func (x *X509Attestation) GetCommonName() string { + if x != nil { + return x.CommonName + } + return "" +} + +type ProvisionStatus struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Last time the device requested provisioning, in unix nanoseconds timestamp format. + Date int64 `protobuf:"varint,1,opt,name=date,proto3" json:"date,omitempty" bson:"date,omitempty"` // @gotags: bson:"date,omitempty" + // The CoAP code returned to the device. + CoapCode int32 `protobuf:"varint,2,opt,name=coap_code,json=coapCode,proto3" json:"coap_code,omitempty" bson:"coapCode,omitempty"` // @gotags: bson:"coapCode,omitempty" + // Error message if any. + ErrorMessage string `protobuf:"bytes,3,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty" bson:"errorMessage,omitempty"` // @gotags: bson:"errorMessage,omitempty" +} + +func (x *ProvisionStatus) Reset() { + *x = ProvisionStatus{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProvisionStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionStatus) ProtoMessage() {} + +func (x *ProvisionStatus) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionStatus.ProtoReflect.Descriptor instead. +func (*ProvisionStatus) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{3} +} + +func (x *ProvisionStatus) GetDate() int64 { + if x != nil { + return x.Date + } + return 0 +} + +func (x *ProvisionStatus) GetCoapCode() int32 { + if x != nil { + return x.CoapCode + } + return 0 +} + +func (x *ProvisionStatus) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +type PreSharedKey struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // ID used to identify the owner by the device. + SubjectId string `protobuf:"bytes,1,opt,name=subject_id,json=subjectId,proto3" json:"subject_id,omitempty" bson:"subjectId,omitempty"` // @gotags: bson:"subjectId,omitempty" + // Associated secret to the owner ID. + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty" bson:"key,omitempty"` // @gotags: bson:"key,omitempty" +} + +func (x *PreSharedKey) Reset() { + *x = PreSharedKey{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PreSharedKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PreSharedKey) ProtoMessage() {} + +func (x *PreSharedKey) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PreSharedKey.ProtoReflect.Descriptor instead. +func (*PreSharedKey) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{4} +} + +func (x *PreSharedKey) GetSubjectId() string { + if x != nil { + return x.SubjectId + } + return "" +} + +func (x *PreSharedKey) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +// Credential Type dependent - eg revocation status information +type CredentialOptionalData struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Data to be provisioned. + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + // Encoding of the data. + Encoding CredentialOptionalData_Encoding `protobuf:"varint,2,opt,name=encoding,proto3,enum=deviceprovisioningservice.pb.CredentialOptionalData_Encoding" json:"encoding,omitempty"` + // If set, the credential is revoked. + IsRevoked bool `protobuf:"varint,3,opt,name=is_revoked,json=isRevoked,proto3" json:"is_revoked,omitempty"` +} + +func (x *CredentialOptionalData) Reset() { + *x = CredentialOptionalData{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CredentialOptionalData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CredentialOptionalData) ProtoMessage() {} + +func (x *CredentialOptionalData) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CredentialOptionalData.ProtoReflect.Descriptor instead. +func (*CredentialOptionalData) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{5} +} + +func (x *CredentialOptionalData) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *CredentialOptionalData) GetEncoding() CredentialOptionalData_Encoding { + if x != nil { + return x.Encoding + } + return CredentialOptionalData_UNKNOWN +} + +func (x *CredentialOptionalData) GetIsRevoked() bool { + if x != nil { + return x.IsRevoked + } + return false +} + +// Private credential information - non-public contents. +type CredentialPrivateData struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Data to be provisioned. + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + // Encoding of the data. + Encoding CredentialPrivateData_Encoding `protobuf:"varint,2,opt,name=encoding,proto3,enum=deviceprovisioningservice.pb.CredentialPrivateData_Encoding" json:"encoding,omitempty"` + // Handle to a key storage Resource. + Handle int64 `protobuf:"varint,3,opt,name=handle,proto3" json:"handle,omitempty"` +} + +func (x *CredentialPrivateData) Reset() { + *x = CredentialPrivateData{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CredentialPrivateData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CredentialPrivateData) ProtoMessage() {} + +func (x *CredentialPrivateData) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CredentialPrivateData.ProtoReflect.Descriptor instead. +func (*CredentialPrivateData) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{6} +} + +func (x *CredentialPrivateData) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *CredentialPrivateData) GetEncoding() CredentialPrivateData_Encoding { + if x != nil { + return x.Encoding + } + return CredentialPrivateData_UNKNOWN +} + +func (x *CredentialPrivateData) GetHandle() int64 { + if x != nil { + return x.Handle + } + return 0 +} + +// Credential Type dependent - public contents. +type CredentialPublicData struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Data to be provisioned. + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + // Encoding of the data. + Encoding CredentialPublicData_Encoding `protobuf:"varint,2,opt,name=encoding,proto3,enum=deviceprovisioningservice.pb.CredentialPublicData_Encoding" json:"encoding,omitempty"` +} + +func (x *CredentialPublicData) Reset() { + *x = CredentialPublicData{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CredentialPublicData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CredentialPublicData) ProtoMessage() {} + +func (x *CredentialPublicData) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CredentialPublicData.ProtoReflect.Descriptor instead. +func (*CredentialPublicData) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{7} +} + +func (x *CredentialPublicData) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *CredentialPublicData) GetEncoding() CredentialPublicData_Encoding { + if x != nil { + return x.Encoding + } + return CredentialPublicData_UNKNOWN +} + +type CredentialRoleID struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Authority string `protobuf:"bytes,1,opt,name=authority,proto3" json:"authority,omitempty"` + Role string `protobuf:"bytes,2,opt,name=role,proto3" json:"role,omitempty"` +} + +func (x *CredentialRoleID) Reset() { + *x = CredentialRoleID{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CredentialRoleID) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CredentialRoleID) ProtoMessage() {} + +func (x *CredentialRoleID) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CredentialRoleID.ProtoReflect.Descriptor instead. +func (*CredentialRoleID) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{8} +} + +func (x *CredentialRoleID) GetAuthority() string { + if x != nil { + return x.Authority + } + return "" +} + +func (x *CredentialRoleID) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +type Credential struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Credential ID. If not set, the device will generate one. + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty" bson:"id,omitempty"` // @gotags: bson:"id,omitempty" + // Credential type. + Type []Credential_CredentialType `protobuf:"varint,2,rep,packed,name=type,proto3,enum=deviceprovisioningservice.pb.Credential_CredentialType" json:"type,omitempty" bson:"type,omitempty"` // @gotags: bson:"type,omitempty" + // Credential subject. + Subject string `protobuf:"bytes,3,opt,name=subject,proto3" json:"subject,omitempty" bson:"subject,omitempty"` // @gotags: bson:"subject,omitempty" + // Credential usage. + Usage Credential_CredentialUsage `protobuf:"varint,4,opt,name=usage,proto3,enum=deviceprovisioningservice.pb.Credential_CredentialUsage" json:"usage,omitempty" bson:"usage,omitempty"` // @gotags: bson:"usage,omitempty" + // Supported credential refresh methods. + SupportedRefreshMethods []Credential_CredentialRefreshMethod `protobuf:"varint,5,rep,packed,name=supported_refresh_methods,json=supportedRefreshMethods,proto3,enum=deviceprovisioningservice.pb.Credential_CredentialRefreshMethod" json:"supported_refresh_methods,omitempty" bson:"supportedRefreshMethods,omitempty"` // @gotags: bson:"supportedRefreshMethods,omitempty" + // Optional data. + OptionalData *CredentialOptionalData `protobuf:"bytes,6,opt,name=optional_data,json=optionalData,proto3" json:"optional_data,omitempty" bson:"optionalData,omitempty"` // @gotags: bson:"optionalData,omitempty" + // Period of validity in seconds. + Period string `protobuf:"bytes,7,opt,name=period,proto3" json:"period,omitempty" bson:"period,omitempty"` // @gotags: bson:"period,omitempty" + // Private data. + PrivateData *CredentialPrivateData `protobuf:"bytes,8,opt,name=private_data,json=privateData,proto3" json:"private_data,omitempty" bson:"privateData,omitempty"` // @gotags: bson:"privateData,omitempty" + // Public data. + PublicData *CredentialPublicData `protobuf:"bytes,9,opt,name=public_data,json=publicData,proto3" json:"public_data,omitempty" bson:"publicData,omitempty"` // @gotags: bson:"publicData,omitempty" + // Role ID. + RoleId *CredentialRoleID `protobuf:"bytes,10,opt,name=role_id,json=roleId,proto3" json:"role_id,omitempty" bson:"roleId,omitempty"` // @gotags: bson:"roleId,omitempty" +} + +func (x *Credential) Reset() { + *x = Credential{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Credential) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Credential) ProtoMessage() {} + +func (x *Credential) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Credential.ProtoReflect.Descriptor instead. +func (*Credential) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{9} +} + +func (x *Credential) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Credential) GetType() []Credential_CredentialType { + if x != nil { + return x.Type + } + return nil +} + +func (x *Credential) GetSubject() string { + if x != nil { + return x.Subject + } + return "" +} + +func (x *Credential) GetUsage() Credential_CredentialUsage { + if x != nil { + return x.Usage + } + return Credential_NONE +} + +func (x *Credential) GetSupportedRefreshMethods() []Credential_CredentialRefreshMethod { + if x != nil { + return x.SupportedRefreshMethods + } + return nil +} + +func (x *Credential) GetOptionalData() *CredentialOptionalData { + if x != nil { + return x.OptionalData + } + return nil +} + +func (x *Credential) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +func (x *Credential) GetPrivateData() *CredentialPrivateData { + if x != nil { + return x.PrivateData + } + return nil +} + +func (x *Credential) GetPublicData() *CredentialPublicData { + if x != nil { + return x.PublicData + } + return nil +} + +func (x *Credential) GetRoleId() *CredentialRoleID { + if x != nil { + return x.RoleId + } + return nil +} + +type CredentialStatus struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Status *ProvisionStatus `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty" bson:"status,omitempty"` // @gotags: bson:"status,omitempty" + // Last identity certificate issued for the device. + IdentityCertificatePem string `protobuf:"bytes,2,opt,name=identity_certificate_pem,json=identityCertificatePem,proto3" json:"identity_certificate_pem,omitempty" bson:"identityCertificate,omitempty"` // @gotags: bson:"identityCertificate,omitempty" + // Last pre shared key issued for the device. + PreSharedKey *PreSharedKey `protobuf:"bytes,3,opt,name=pre_shared_key,json=preSharedKey,proto3" json:"pre_shared_key,omitempty" bson:"preSharedKey,omitempty"` // @gotags: bson:"preSharedKey,omitempty" + Credentials []*Credential `protobuf:"bytes,4,rep,name=credentials,proto3" json:"credentials,omitempty" bson:"credentials,omitempty"` // @gotags: bson:"credentials,omitempty" +} + +func (x *CredentialStatus) Reset() { + *x = CredentialStatus{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CredentialStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CredentialStatus) ProtoMessage() {} + +func (x *CredentialStatus) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CredentialStatus.ProtoReflect.Descriptor instead. +func (*CredentialStatus) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{10} +} + +func (x *CredentialStatus) GetStatus() *ProvisionStatus { + if x != nil { + return x.Status + } + return nil +} + +func (x *CredentialStatus) GetIdentityCertificatePem() string { + if x != nil { + return x.IdentityCertificatePem + } + return "" +} + +func (x *CredentialStatus) GetPreSharedKey() *PreSharedKey { + if x != nil { + return x.PreSharedKey + } + return nil +} + +func (x *CredentialStatus) GetCredentials() []*Credential { + if x != nil { + return x.Credentials + } + return nil +} + +type OwnershipStatus struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Status *ProvisionStatus `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty" bson:"status,omitempty"` // @gotags: bson:"status,omitempty" + // Last provisioned owner to the device. + Owner string `protobuf:"bytes,2,opt,name=owner,proto3" json:"owner,omitempty" bson:"owner,omitempty"` // @gotags: bson:"owner,omitempty" +} + +func (x *OwnershipStatus) Reset() { + *x = OwnershipStatus{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OwnershipStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OwnershipStatus) ProtoMessage() {} + +func (x *OwnershipStatus) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OwnershipStatus.ProtoReflect.Descriptor instead. +func (*OwnershipStatus) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{11} +} + +func (x *OwnershipStatus) GetStatus() *ProvisionStatus { + if x != nil { + return x.Status + } + return nil +} + +func (x *OwnershipStatus) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +type AccessControlDeviceSubject struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DeviceId string `protobuf:"bytes,1,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty"` +} + +func (x *AccessControlDeviceSubject) Reset() { + *x = AccessControlDeviceSubject{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AccessControlDeviceSubject) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccessControlDeviceSubject) ProtoMessage() {} + +func (x *AccessControlDeviceSubject) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AccessControlDeviceSubject.ProtoReflect.Descriptor instead. +func (*AccessControlDeviceSubject) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{12} +} + +func (x *AccessControlDeviceSubject) GetDeviceId() string { + if x != nil { + return x.DeviceId + } + return "" +} + +type AccessControlRoleSubject struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Authority string `protobuf:"bytes,1,opt,name=authority,proto3" json:"authority,omitempty"` + Role string `protobuf:"bytes,2,opt,name=role,proto3" json:"role,omitempty"` +} + +func (x *AccessControlRoleSubject) Reset() { + *x = AccessControlRoleSubject{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AccessControlRoleSubject) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccessControlRoleSubject) ProtoMessage() {} + +func (x *AccessControlRoleSubject) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AccessControlRoleSubject.ProtoReflect.Descriptor instead. +func (*AccessControlRoleSubject) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{13} +} + +func (x *AccessControlRoleSubject) GetAuthority() string { + if x != nil { + return x.Authority + } + return "" +} + +func (x *AccessControlRoleSubject) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +type AccessControlConnectionSubject struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type AccessControlConnectionSubject_ConnectionType `protobuf:"varint,1,opt,name=type,proto3,enum=deviceprovisioningservice.pb.AccessControlConnectionSubject_ConnectionType" json:"type,omitempty"` +} + +func (x *AccessControlConnectionSubject) Reset() { + *x = AccessControlConnectionSubject{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AccessControlConnectionSubject) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccessControlConnectionSubject) ProtoMessage() {} + +func (x *AccessControlConnectionSubject) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AccessControlConnectionSubject.ProtoReflect.Descriptor instead. +func (*AccessControlConnectionSubject) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{14} +} + +func (x *AccessControlConnectionSubject) GetType() AccessControlConnectionSubject_ConnectionType { + if x != nil { + return x.Type + } + return AccessControlConnectionSubject_ANON_CLEAR +} + +type AccessControlResource struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Resource href. + Href string `protobuf:"bytes,1,opt,name=href,proto3" json:"href,omitempty"` + // Resource type. + ResourceTypes []string `protobuf:"bytes,2,rep,name=resource_types,json=resourceTypes,proto3" json:"resource_types,omitempty"` + // Resource interface. + Interfaces []string `protobuf:"bytes,3,rep,name=interfaces,proto3" json:"interfaces,omitempty"` + // Resource wildcard. + Wildcard AccessControlResource_Wildcard `protobuf:"varint,4,opt,name=wildcard,proto3,enum=deviceprovisioningservice.pb.AccessControlResource_Wildcard" json:"wildcard,omitempty"` +} + +func (x *AccessControlResource) Reset() { + *x = AccessControlResource{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AccessControlResource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccessControlResource) ProtoMessage() {} + +func (x *AccessControlResource) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AccessControlResource.ProtoReflect.Descriptor instead. +func (*AccessControlResource) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{15} +} + +func (x *AccessControlResource) GetHref() string { + if x != nil { + return x.Href + } + return "" +} + +func (x *AccessControlResource) GetResourceTypes() []string { + if x != nil { + return x.ResourceTypes + } + return nil +} + +func (x *AccessControlResource) GetInterfaces() []string { + if x != nil { + return x.Interfaces + } + return nil +} + +func (x *AccessControlResource) GetWildcard() AccessControlResource_Wildcard { + if x != nil { + return x.Wildcard + } + return AccessControlResource_NONE +} + +type AccessControl struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Subject of the ACL defines the entity to which the permissions are granted. Only one subject must be defined per ACL. + DeviceSubject *AccessControlDeviceSubject `protobuf:"bytes,1,opt,name=device_subject,json=deviceSubject,proto3" json:"device_subject,omitempty" bson:"deviceSubject,omitempty"` // @gotags: bson:"deviceSubject,omitempty" + RoleSubject *AccessControlRoleSubject `protobuf:"bytes,2,opt,name=role_subject,json=roleSubject,proto3" json:"role_subject,omitempty" bson:"roleSubject,omitempty"` // @gotags: bson:"roleSubject,omitempty" + ConnectionSubject *AccessControlConnectionSubject `protobuf:"bytes,3,opt,name=connection_subject,json=connectionSubject,proto3" json:"connection_subject,omitempty" bson:"connectionSubject,omitempty"` // @gotags: bson:"connectionSubject,omitempty" + // Permissions granted to the subject. + Permissions []AccessControl_Permission `protobuf:"varint,4,rep,packed,name=permissions,proto3,enum=deviceprovisioningservice.pb.AccessControl_Permission" json:"permissions,omitempty"` + // Resources to which the permissions apply. + Resources []*AccessControlResource `protobuf:"bytes,5,rep,name=resources,proto3" json:"resources,omitempty"` +} + +func (x *AccessControl) Reset() { + *x = AccessControl{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AccessControl) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccessControl) ProtoMessage() {} + +func (x *AccessControl) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[16] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AccessControl.ProtoReflect.Descriptor instead. +func (*AccessControl) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{16} +} + +func (x *AccessControl) GetDeviceSubject() *AccessControlDeviceSubject { + if x != nil { + return x.DeviceSubject + } + return nil +} + +func (x *AccessControl) GetRoleSubject() *AccessControlRoleSubject { + if x != nil { + return x.RoleSubject + } + return nil +} + +func (x *AccessControl) GetConnectionSubject() *AccessControlConnectionSubject { + if x != nil { + return x.ConnectionSubject + } + return nil +} + +func (x *AccessControl) GetPermissions() []AccessControl_Permission { + if x != nil { + return x.Permissions + } + return nil +} + +func (x *AccessControl) GetResources() []*AccessControlResource { + if x != nil { + return x.Resources + } + return nil +} + +type ACLStatus struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Status *ProvisionStatus `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty" bson:"status,omitempty"` // @gotags: bson:"status,omitempty" + // Last ACL list provisioned to the device. + AccessControlList []*AccessControl `protobuf:"bytes,2,rep,name=access_control_list,json=accessControlList,proto3" json:"access_control_list,omitempty" bson:"accessControlList,omitempty"` // @gotags: bson:"accessControlList,omitempty" +} + +func (x *ACLStatus) Reset() { + *x = ACLStatus{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ACLStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ACLStatus) ProtoMessage() {} + +func (x *ACLStatus) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[17] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ACLStatus.ProtoReflect.Descriptor instead. +func (*ACLStatus) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{17} +} + +func (x *ACLStatus) GetStatus() *ProvisionStatus { + if x != nil { + return x.Status + } + return nil +} + +func (x *ACLStatus) GetAccessControlList() []*AccessControl { + if x != nil { + return x.AccessControlList + } + return nil +} + +type CloudStatus struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Status *ProvisionStatus `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty" bson:"status,omitempty"` // @gotags: bson:"status,omitempty" + // Last provider name used to authenticate the device to the cloud. + ProviderName string `protobuf:"bytes,3,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty" bson:"providerName,omitempty"` // @gotags: bson:"providerName,omitempty" + // Last provisioned gateways to the device. + Gateways []*CloudStatus_Gateway `protobuf:"bytes,5,rep,name=gateways,proto3" json:"gateways,omitempty" bson:"gateways,omitempty"` // @gotags: bson:"gateways,omitempty" + SelectedGateway int32 `protobuf:"varint,6,opt,name=selected_gateway,json=selectedGateway,proto3" json:"selected_gateway,omitempty" bson:"selectedGateway,omitempty"` // @gotags: bson:"selectedGateway,omitempty" +} + +func (x *CloudStatus) Reset() { + *x = CloudStatus{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CloudStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloudStatus) ProtoMessage() {} + +func (x *CloudStatus) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[18] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloudStatus.ProtoReflect.Descriptor instead. +func (*CloudStatus) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{18} +} + +func (x *CloudStatus) GetStatus() *ProvisionStatus { + if x != nil { + return x.Status + } + return nil +} + +func (x *CloudStatus) GetProviderName() string { + if x != nil { + return x.ProviderName + } + return "" +} + +func (x *CloudStatus) GetGateways() []*CloudStatus_Gateway { + if x != nil { + return x.Gateways + } + return nil +} + +func (x *CloudStatus) GetSelectedGateway() int32 { + if x != nil { + return x.SelectedGateway + } + return 0 +} + +type ProvisioningRecord struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Registration id, calculated from the manufacturer certificate public key info. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" bson:"_id,omitempty"` // @gotags: bson:"_id,omitempty" + // ID of the device to which this record belongs to. + DeviceId string `protobuf:"bytes,2,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty" bson:"deviceId,omitempty"` // @gotags: bson:"deviceId,omitempty" + // Assigned enrollment group. + EnrollmentGroupId string `protobuf:"bytes,3,opt,name=enrollment_group_id,json=enrollmentGroupId,proto3" json:"enrollment_group_id,omitempty" bson:"enrollmentGroupId,omitempty"` // @gotags: bson:"enrollmentGroupId,omitempty" + // Record creation date, in unix nanoseconds timestamp format. + CreationDate int64 `protobuf:"varint,4,opt,name=creation_date,json=creationDate,proto3" json:"creation_date,omitempty" bson:"creationDate,omitempty"` // @gotags: bson:"creationDate,omitempty" + // Last device attestation overview. + Attestation *Attestation `protobuf:"bytes,5,opt,name=attestation,proto3" json:"attestation,omitempty" bson:"attestation,omitempty"` // @gotags: bson:"attestation,omitempty" + // Last credential provision overview. + Credential *CredentialStatus `protobuf:"bytes,6,opt,name=credential,proto3" json:"credential,omitempty" bson:"credential,omitempty"` // @gotags: bson:"credential,omitempty" + // Last ACL provision overview. + Acl *ACLStatus `protobuf:"bytes,7,opt,name=acl,proto3" json:"acl,omitempty" bson:"acl,omitempty"` // @gotags: bson:"acl,omitempty" + // Last cloud provision overview. + Cloud *CloudStatus `protobuf:"bytes,8,opt,name=cloud,proto3" json:"cloud,omitempty" bson:"cloud,omitempty"` // @gotags: bson:"cloud,omitempty" + // Last ownership provision overview. + Ownership *OwnershipStatus `protobuf:"bytes,9,opt,name=ownership,proto3" json:"ownership,omitempty" bson:"ownership,omitempty"` // @gotags: bson:"ownership,omitempty" + // Last plgd-time provision overview. + PlgdTime *ProvisionStatus `protobuf:"bytes,10,opt,name=plgd_time,json=plgdTime,proto3" json:"plgd_time,omitempty" bson:"plgdTime,omitempty"` // @gotags: bson:"plgdTime,omitempty" + // Last local endpoints + LocalEndpoints []string `protobuf:"bytes,11,rep,name=local_endpoints,json=localEndpoints,proto3" json:"local_endpoints,omitempty" bson:"localEndpoints,omitempty"` // @gotags: bson:"localEndpoints,omitempty" + // Owner ID. + Owner string `protobuf:"bytes,12,opt,name=owner,proto3" json:"owner,omitempty" bson:"owner,omitempty"` // @gotags: bson:"owner,omitempty" +} + +func (x *ProvisioningRecord) Reset() { + *x = ProvisioningRecord{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProvisioningRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisioningRecord) ProtoMessage() {} + +func (x *ProvisioningRecord) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[19] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisioningRecord.ProtoReflect.Descriptor instead. +func (*ProvisioningRecord) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{19} +} + +func (x *ProvisioningRecord) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ProvisioningRecord) GetDeviceId() string { + if x != nil { + return x.DeviceId + } + return "" +} + +func (x *ProvisioningRecord) GetEnrollmentGroupId() string { + if x != nil { + return x.EnrollmentGroupId + } + return "" +} + +func (x *ProvisioningRecord) GetCreationDate() int64 { + if x != nil { + return x.CreationDate + } + return 0 +} + +func (x *ProvisioningRecord) GetAttestation() *Attestation { + if x != nil { + return x.Attestation + } + return nil +} + +func (x *ProvisioningRecord) GetCredential() *CredentialStatus { + if x != nil { + return x.Credential + } + return nil +} + +func (x *ProvisioningRecord) GetAcl() *ACLStatus { + if x != nil { + return x.Acl + } + return nil +} + +func (x *ProvisioningRecord) GetCloud() *CloudStatus { + if x != nil { + return x.Cloud + } + return nil +} + +func (x *ProvisioningRecord) GetOwnership() *OwnershipStatus { + if x != nil { + return x.Ownership + } + return nil +} + +func (x *ProvisioningRecord) GetPlgdTime() *ProvisionStatus { + if x != nil { + return x.PlgdTime + } + return nil +} + +func (x *ProvisioningRecord) GetLocalEndpoints() []string { + if x != nil { + return x.LocalEndpoints + } + return nil +} + +func (x *ProvisioningRecord) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +type DeleteProvisioningRecordsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Filter by id. + IdFilter []string `protobuf:"bytes,1,rep,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` + // Filter by device id. + DeviceIdFilter []string `protobuf:"bytes,2,rep,name=device_id_filter,json=deviceIdFilter,proto3" json:"device_id_filter,omitempty"` + // Filter by enrollment group id. + EnrollmentGroupIdFilter []string `protobuf:"bytes,3,rep,name=enrollment_group_id_filter,json=enrollmentGroupIdFilter,proto3" json:"enrollment_group_id_filter,omitempty"` +} + +func (x *DeleteProvisioningRecordsRequest) Reset() { + *x = DeleteProvisioningRecordsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteProvisioningRecordsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteProvisioningRecordsRequest) ProtoMessage() {} + +func (x *DeleteProvisioningRecordsRequest) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[20] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteProvisioningRecordsRequest.ProtoReflect.Descriptor instead. +func (*DeleteProvisioningRecordsRequest) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{20} +} + +func (x *DeleteProvisioningRecordsRequest) GetIdFilter() []string { + if x != nil { + return x.IdFilter + } + return nil +} + +func (x *DeleteProvisioningRecordsRequest) GetDeviceIdFilter() []string { + if x != nil { + return x.DeviceIdFilter + } + return nil +} + +func (x *DeleteProvisioningRecordsRequest) GetEnrollmentGroupIdFilter() []string { + if x != nil { + return x.EnrollmentGroupIdFilter + } + return nil +} + +type DeleteProvisioningRecordsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Number of deleted records. + Count int64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` +} + +func (x *DeleteProvisioningRecordsResponse) Reset() { + *x = DeleteProvisioningRecordsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteProvisioningRecordsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteProvisioningRecordsResponse) ProtoMessage() {} + +func (x *DeleteProvisioningRecordsResponse) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[21] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteProvisioningRecordsResponse.ProtoReflect.Descriptor instead. +func (*DeleteProvisioningRecordsResponse) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{21} +} + +func (x *DeleteProvisioningRecordsResponse) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +type CloudStatus_Gateway struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Gateway endpoint in format ://: + Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty" bson:"uri,omitempty"` // @gotags: bson:"uri,omitempty" + // UUID of the gateway. + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty" bson:"id,omitempty"` // @gotags: bson:"id,omitempty" +} + +func (x *CloudStatus_Gateway) Reset() { + *x = CloudStatus_Gateway{} + if protoimpl.UnsafeEnabled { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CloudStatus_Gateway) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloudStatus_Gateway) ProtoMessage() {} + +func (x *CloudStatus_Gateway) ProtoReflect() protoreflect.Message { + mi := &file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[22] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloudStatus_Gateway.ProtoReflect.Descriptor instead. +func (*CloudStatus_Gateway) Descriptor() ([]byte, []int) { + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP(), []int{18, 0} +} + +func (x *CloudStatus_Gateway) GetUri() string { + if x != nil { + return x.Uri + } + return "" +} + +func (x *CloudStatus_Gateway) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +var File_device_provisioning_service_pb_provisioningRecords_proto protoreflect.FileDescriptor + +var file_device_provisioning_service_pb_provisioningRecords_proto_rawDesc = []byte{ + 0x0a, 0x38, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x2d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x70, 0x62, + 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, + 0x6f, 0x72, 0x64, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1c, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x22, 0xa3, 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x6f, + 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x64, + 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x69, + 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, + 0x72, 0x12, 0x3b, 0x0a, 0x1a, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x5f, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x17, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, + 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x64, + 0x0a, 0x0b, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, + 0x04, 0x64, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x64, 0x61, 0x74, + 0x65, 0x12, 0x41, 0x0a, 0x04, 0x78, 0x35, 0x30, 0x39, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x2d, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x58, + 0x35, 0x30, 0x39, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x04, + 0x78, 0x35, 0x30, 0x39, 0x22, 0x5b, 0x0a, 0x0f, 0x58, 0x35, 0x30, 0x39, 0x41, 0x74, 0x74, 0x65, + 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0e, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6d, + 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, + 0x65, 0x22, 0x67, 0x0a, 0x0f, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x04, 0x64, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x61, 0x70, + 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x63, 0x6f, 0x61, + 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x3f, 0x0a, 0x0c, 0x50, 0x72, + 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0xf8, 0x01, 0x0a, 0x16, + 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x61, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x59, 0x0a, 0x08, 0x65, 0x6e, + 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x3d, 0x2e, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, + 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x44, 0x61, + 0x74, 0x61, 0x2e, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x08, 0x65, 0x6e, 0x63, + 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x72, 0x65, 0x76, 0x6f, + 0x6b, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x52, 0x65, 0x76, + 0x6f, 0x6b, 0x65, 0x64, 0x22, 0x50, 0x0a, 0x08, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, + 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, + 0x03, 0x52, 0x41, 0x57, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x4a, 0x57, 0x54, 0x10, 0x02, 0x12, + 0x07, 0x0a, 0x03, 0x43, 0x57, 0x54, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x41, 0x53, 0x45, + 0x36, 0x34, 0x10, 0x04, 0x12, 0x07, 0x0a, 0x03, 0x50, 0x45, 0x4d, 0x10, 0x05, 0x12, 0x07, 0x0a, + 0x03, 0x44, 0x45, 0x52, 0x10, 0x06, 0x22, 0xf2, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, + 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, + 0x64, 0x61, 0x74, 0x61, 0x12, 0x58, 0x0a, 0x08, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x3c, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x2e, 0x45, 0x6e, 0x63, 0x6f, + 0x64, 0x69, 0x6e, 0x67, 0x52, 0x08, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x16, + 0x0a, 0x06, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, + 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x22, 0x53, 0x0a, 0x08, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, + 0x6e, 0x67, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, + 0x07, 0x0a, 0x03, 0x52, 0x41, 0x57, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x4a, 0x57, 0x54, 0x10, + 0x02, 0x12, 0x07, 0x0a, 0x03, 0x43, 0x57, 0x54, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x41, + 0x53, 0x45, 0x36, 0x34, 0x10, 0x04, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x52, 0x49, 0x10, 0x05, 0x12, + 0x0a, 0x0a, 0x06, 0x48, 0x41, 0x4e, 0x44, 0x4c, 0x45, 0x10, 0x06, 0x22, 0xde, 0x01, 0x0a, 0x14, + 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x44, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x57, 0x0a, 0x08, 0x65, 0x6e, 0x63, 0x6f, + 0x64, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x3b, 0x2e, 0x64, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x44, 0x61, 0x74, 0x61, 0x2e, 0x45, + 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x08, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, + 0x67, 0x22, 0x59, 0x0a, 0x08, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x0b, 0x0a, + 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x52, 0x41, + 0x57, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x4a, 0x57, 0x54, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, + 0x43, 0x57, 0x54, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x41, 0x53, 0x45, 0x36, 0x34, 0x10, + 0x04, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x52, 0x49, 0x10, 0x05, 0x12, 0x07, 0x0a, 0x03, 0x50, 0x45, + 0x4d, 0x10, 0x06, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x45, 0x52, 0x10, 0x07, 0x22, 0x44, 0x0a, 0x10, + 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, 0x6f, 0x6c, 0x65, 0x49, 0x44, + 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x12, + 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, + 0x6c, 0x65, 0x22, 0x9e, 0x09, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, + 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, + 0x64, 0x12, 0x4b, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0e, 0x32, + 0x37, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18, + 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x4e, 0x0a, 0x05, 0x75, 0x73, 0x61, 0x67, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x38, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, + 0x6c, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x55, 0x73, 0x61, 0x67, + 0x65, 0x52, 0x05, 0x75, 0x73, 0x61, 0x67, 0x65, 0x12, 0x7c, 0x0a, 0x19, 0x73, 0x75, 0x70, 0x70, + 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x6d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x40, 0x2e, 0x64, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x17, 0x73, + 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x12, 0x59, 0x0a, 0x0d, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x61, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, + 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x44, + 0x61, 0x74, 0x61, 0x52, 0x0c, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x44, 0x61, 0x74, + 0x61, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x12, 0x56, 0x0a, 0x0c, 0x70, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x33, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x44, 0x61, 0x74, 0x61, 0x52, 0x0b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, + 0x61, 0x12, 0x53, 0x0a, 0x0b, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0a, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x44, 0x61, 0x74, 0x61, 0x12, 0x47, 0x0a, 0x07, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x69, + 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, + 0x6c, 0x52, 0x6f, 0x6c, 0x65, 0x49, 0x44, 0x52, 0x06, 0x72, 0x6f, 0x6c, 0x65, 0x49, 0x64, 0x22, + 0xbe, 0x01, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x4d, 0x50, 0x54, 0x59, 0x10, 0x00, 0x12, 0x17, 0x0a, + 0x13, 0x53, 0x59, 0x4d, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, 0x50, 0x41, 0x49, 0x52, 0x5f, + 0x57, 0x49, 0x53, 0x45, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x59, 0x4d, 0x4d, 0x45, 0x54, + 0x52, 0x49, 0x43, 0x5f, 0x47, 0x52, 0x4f, 0x55, 0x50, 0x10, 0x02, 0x12, 0x16, 0x0a, 0x12, 0x41, + 0x53, 0x59, 0x4d, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, 0x53, 0x49, 0x47, 0x4e, 0x49, 0x4e, + 0x47, 0x10, 0x04, 0x12, 0x27, 0x0a, 0x23, 0x41, 0x53, 0x59, 0x4d, 0x4d, 0x45, 0x54, 0x52, 0x49, + 0x43, 0x5f, 0x53, 0x49, 0x47, 0x4e, 0x49, 0x4e, 0x47, 0x5f, 0x57, 0x49, 0x54, 0x48, 0x5f, 0x43, + 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x10, 0x08, 0x12, 0x13, 0x0a, 0x0f, + 0x50, 0x49, 0x4e, 0x5f, 0x4f, 0x52, 0x5f, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x10, + 0x10, 0x12, 0x1d, 0x0a, 0x19, 0x41, 0x53, 0x59, 0x4d, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, + 0x45, 0x4e, 0x43, 0x52, 0x59, 0x50, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x20, + 0x22, 0x62, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x55, 0x73, + 0x61, 0x67, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x0c, 0x0a, + 0x08, 0x54, 0x52, 0x55, 0x53, 0x54, 0x5f, 0x43, 0x41, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x43, + 0x45, 0x52, 0x54, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x43, 0x45, + 0x52, 0x54, 0x10, 0x03, 0x12, 0x10, 0x0a, 0x0c, 0x4d, 0x46, 0x47, 0x5f, 0x54, 0x52, 0x55, 0x53, + 0x54, 0x5f, 0x43, 0x41, 0x10, 0x04, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x46, 0x47, 0x5f, 0x43, 0x45, + 0x52, 0x54, 0x10, 0x05, 0x22, 0xbc, 0x01, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x15, 0x0a, + 0x11, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, + 0x43, 0x45, 0x10, 0x01, 0x12, 0x29, 0x0a, 0x25, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x47, 0x52, 0x45, + 0x45, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x43, 0x4f, 0x4c, 0x5f, 0x41, + 0x4e, 0x44, 0x5f, 0x52, 0x41, 0x4e, 0x44, 0x4f, 0x4d, 0x5f, 0x50, 0x49, 0x4e, 0x10, 0x02, 0x12, + 0x1a, 0x0a, 0x16, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x47, 0x52, 0x45, 0x45, 0x4d, 0x45, 0x4e, 0x54, + 0x5f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x43, 0x4f, 0x4c, 0x10, 0x03, 0x12, 0x1c, 0x0a, 0x18, 0x4b, + 0x45, 0x59, 0x5f, 0x44, 0x49, 0x53, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x49, 0x4f, 0x4e, 0x5f, + 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x10, 0x04, 0x12, 0x18, 0x0a, 0x14, 0x50, 0x4b, 0x43, + 0x53, 0x31, 0x30, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x5f, 0x54, 0x4f, 0x5f, 0x43, + 0x41, 0x10, 0x05, 0x22, 0xb1, 0x02, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x45, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x38, 0x0a, 0x18, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x16, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x43, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6d, 0x12, 0x50, 0x0a, 0x0e, 0x70, 0x72, 0x65, + 0x5f, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2a, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, + 0x2e, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x0c, 0x70, + 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x4a, 0x0a, 0x0b, 0x63, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x28, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, + 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x22, 0x6e, 0x0a, 0x0f, 0x4f, 0x77, 0x6e, 0x65, 0x72, + 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x45, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x64, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x22, 0x39, 0x0a, 0x1a, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x49, 0x64, 0x22, 0x4c, 0x0a, 0x18, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x52, 0x6f, 0x6c, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1c, + 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04, + 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, + 0x22, 0xb3, 0x01, 0x0a, 0x1e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x12, 0x5f, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x4b, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, + 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x2e, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x22, 0x30, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x0a, 0x41, 0x4e, 0x4f, 0x4e, 0x5f, 0x43, + 0x4c, 0x45, 0x41, 0x52, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x43, + 0x52, 0x59, 0x50, 0x54, 0x10, 0x01, 0x22, 0xa7, 0x02, 0x0a, 0x15, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x68, 0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x68, 0x72, 0x65, 0x66, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x12, 0x58, 0x0a, 0x08, 0x77, + 0x69, 0x6c, 0x64, 0x63, 0x61, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x3c, 0x2e, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, + 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x57, 0x69, 0x6c, 0x64, 0x63, 0x61, 0x72, 0x64, 0x52, 0x08, 0x77, 0x69, 0x6c, + 0x64, 0x63, 0x61, 0x72, 0x64, 0x22, 0x59, 0x0a, 0x08, 0x57, 0x69, 0x6c, 0x64, 0x63, 0x61, 0x72, + 0x64, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x4e, + 0x4f, 0x4e, 0x43, 0x46, 0x47, 0x5f, 0x53, 0x45, 0x43, 0x5f, 0x45, 0x4e, 0x44, 0x50, 0x4f, 0x49, + 0x4e, 0x54, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x4e, 0x4f, 0x4e, 0x43, 0x46, 0x47, 0x5f, 0x4e, + 0x4f, 0x4e, 0x53, 0x45, 0x43, 0x5f, 0x45, 0x4e, 0x44, 0x50, 0x4f, 0x49, 0x4e, 0x54, 0x10, 0x02, + 0x12, 0x0e, 0x0a, 0x0a, 0x4e, 0x4f, 0x4e, 0x43, 0x46, 0x47, 0x5f, 0x41, 0x4c, 0x4c, 0x10, 0x03, + 0x22, 0xac, 0x04, 0x0a, 0x0d, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x12, 0x5f, 0x0a, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x64, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x52, 0x0d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x12, 0x59, 0x0a, 0x0c, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x73, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x43, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x6f, 0x6c, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x52, 0x0b, 0x72, 0x6f, 0x6c, 0x65, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x6b, + 0x0a, 0x12, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x64, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x58, 0x0a, 0x0b, 0x70, + 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0e, + 0x32, 0x36, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, + 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x2e, 0x50, 0x65, + 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x51, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x43, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x0a, 0x50, 0x65, 0x72, 0x6d, + 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, + 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x45, 0x41, 0x44, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, + 0x57, 0x52, 0x49, 0x54, 0x45, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, 0x45, 0x54, + 0x45, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x4e, 0x4f, 0x54, 0x49, 0x46, 0x59, 0x10, 0x04, 0x22, + 0xaf, 0x01, 0x0a, 0x09, 0x41, 0x43, 0x4c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x45, 0x0a, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, + 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x5b, 0x0a, 0x13, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x2b, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, + 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x11, + 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x4c, 0x69, 0x73, + 0x74, 0x22, 0xba, 0x02, 0x0a, 0x0b, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x45, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2d, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, + 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x4d, 0x0a, + 0x08, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x31, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, + 0x6c, 0x6f, 0x75, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x52, 0x08, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x12, 0x29, 0x0a, 0x10, + 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x1a, 0x2b, 0x0a, 0x07, 0x47, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x75, 0x72, 0x69, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x02, 0x69, 0x64, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, + 0x52, 0x0c, 0x63, 0x6f, 0x61, 0x70, 0x5f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x22, 0x87, + 0x05, 0x0a, 0x12, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x52, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x13, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, + 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x11, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, + 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x61, 0x74, 0x74, 0x65, 0x73, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, + 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x74, 0x74, 0x65, + 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4e, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x61, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x61, 0x6c, 0x12, 0x39, 0x0a, 0x03, 0x61, 0x63, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x27, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, + 0x2e, 0x41, 0x43, 0x4c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x03, 0x61, 0x63, 0x6c, 0x12, + 0x3f, 0x0a, 0x05, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, + 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6c, + 0x6f, 0x75, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x05, 0x63, 0x6c, 0x6f, 0x75, 0x64, + 0x12, 0x4b, 0x0a, 0x09, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, + 0x70, 0x62, 0x2e, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x52, 0x09, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x12, 0x4a, 0x0a, + 0x09, 0x70, 0x6c, 0x67, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x2d, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, + 0x08, 0x70, 0x6c, 0x67, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x6c, 0x6f, 0x63, + 0x61, 0x6c, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x0b, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x22, 0xa6, 0x01, 0x0a, 0x20, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x52, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, + 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x10, 0x64, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x46, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x12, 0x3b, 0x0a, 0x1a, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, + 0x6e, 0x74, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x17, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, + 0x6d, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, + 0x72, 0x22, 0x39, 0x0a, 0x21, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x3e, 0x5a, 0x3c, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x67, 0x64, 0x2d, + 0x64, 0x65, 0x76, 0x2f, 0x68, 0x75, 0x62, 0x2f, 0x76, 0x32, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x2d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x70, 0x62, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_device_provisioning_service_pb_provisioningRecords_proto_rawDescOnce sync.Once + file_device_provisioning_service_pb_provisioningRecords_proto_rawDescData = file_device_provisioning_service_pb_provisioningRecords_proto_rawDesc +) + +func file_device_provisioning_service_pb_provisioningRecords_proto_rawDescGZIP() []byte { + file_device_provisioning_service_pb_provisioningRecords_proto_rawDescOnce.Do(func() { + file_device_provisioning_service_pb_provisioningRecords_proto_rawDescData = protoimpl.X.CompressGZIP(file_device_provisioning_service_pb_provisioningRecords_proto_rawDescData) + }) + return file_device_provisioning_service_pb_provisioningRecords_proto_rawDescData +} + +var file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes = make([]protoimpl.EnumInfo, 9) +var file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes = make([]protoimpl.MessageInfo, 23) +var file_device_provisioning_service_pb_provisioningRecords_proto_goTypes = []any{ + (CredentialOptionalData_Encoding)(0), // 0: deviceprovisioningservice.pb.CredentialOptionalData.Encoding + (CredentialPrivateData_Encoding)(0), // 1: deviceprovisioningservice.pb.CredentialPrivateData.Encoding + (CredentialPublicData_Encoding)(0), // 2: deviceprovisioningservice.pb.CredentialPublicData.Encoding + (Credential_CredentialType)(0), // 3: deviceprovisioningservice.pb.Credential.CredentialType + (Credential_CredentialUsage)(0), // 4: deviceprovisioningservice.pb.Credential.CredentialUsage + (Credential_CredentialRefreshMethod)(0), // 5: deviceprovisioningservice.pb.Credential.CredentialRefreshMethod + (AccessControlConnectionSubject_ConnectionType)(0), // 6: deviceprovisioningservice.pb.AccessControlConnectionSubject.ConnectionType + (AccessControlResource_Wildcard)(0), // 7: deviceprovisioningservice.pb.AccessControlResource.Wildcard + (AccessControl_Permission)(0), // 8: deviceprovisioningservice.pb.AccessControl.Permission + (*GetProvisioningRecordsRequest)(nil), // 9: deviceprovisioningservice.pb.GetProvisioningRecordsRequest + (*Attestation)(nil), // 10: deviceprovisioningservice.pb.Attestation + (*X509Attestation)(nil), // 11: deviceprovisioningservice.pb.X509Attestation + (*ProvisionStatus)(nil), // 12: deviceprovisioningservice.pb.ProvisionStatus + (*PreSharedKey)(nil), // 13: deviceprovisioningservice.pb.PreSharedKey + (*CredentialOptionalData)(nil), // 14: deviceprovisioningservice.pb.CredentialOptionalData + (*CredentialPrivateData)(nil), // 15: deviceprovisioningservice.pb.CredentialPrivateData + (*CredentialPublicData)(nil), // 16: deviceprovisioningservice.pb.CredentialPublicData + (*CredentialRoleID)(nil), // 17: deviceprovisioningservice.pb.CredentialRoleID + (*Credential)(nil), // 18: deviceprovisioningservice.pb.Credential + (*CredentialStatus)(nil), // 19: deviceprovisioningservice.pb.CredentialStatus + (*OwnershipStatus)(nil), // 20: deviceprovisioningservice.pb.OwnershipStatus + (*AccessControlDeviceSubject)(nil), // 21: deviceprovisioningservice.pb.AccessControlDeviceSubject + (*AccessControlRoleSubject)(nil), // 22: deviceprovisioningservice.pb.AccessControlRoleSubject + (*AccessControlConnectionSubject)(nil), // 23: deviceprovisioningservice.pb.AccessControlConnectionSubject + (*AccessControlResource)(nil), // 24: deviceprovisioningservice.pb.AccessControlResource + (*AccessControl)(nil), // 25: deviceprovisioningservice.pb.AccessControl + (*ACLStatus)(nil), // 26: deviceprovisioningservice.pb.ACLStatus + (*CloudStatus)(nil), // 27: deviceprovisioningservice.pb.CloudStatus + (*ProvisioningRecord)(nil), // 28: deviceprovisioningservice.pb.ProvisioningRecord + (*DeleteProvisioningRecordsRequest)(nil), // 29: deviceprovisioningservice.pb.DeleteProvisioningRecordsRequest + (*DeleteProvisioningRecordsResponse)(nil), // 30: deviceprovisioningservice.pb.DeleteProvisioningRecordsResponse + (*CloudStatus_Gateway)(nil), // 31: deviceprovisioningservice.pb.CloudStatus.Gateway +} +var file_device_provisioning_service_pb_provisioningRecords_proto_depIdxs = []int32{ + 11, // 0: deviceprovisioningservice.pb.Attestation.x509:type_name -> deviceprovisioningservice.pb.X509Attestation + 0, // 1: deviceprovisioningservice.pb.CredentialOptionalData.encoding:type_name -> deviceprovisioningservice.pb.CredentialOptionalData.Encoding + 1, // 2: deviceprovisioningservice.pb.CredentialPrivateData.encoding:type_name -> deviceprovisioningservice.pb.CredentialPrivateData.Encoding + 2, // 3: deviceprovisioningservice.pb.CredentialPublicData.encoding:type_name -> deviceprovisioningservice.pb.CredentialPublicData.Encoding + 3, // 4: deviceprovisioningservice.pb.Credential.type:type_name -> deviceprovisioningservice.pb.Credential.CredentialType + 4, // 5: deviceprovisioningservice.pb.Credential.usage:type_name -> deviceprovisioningservice.pb.Credential.CredentialUsage + 5, // 6: deviceprovisioningservice.pb.Credential.supported_refresh_methods:type_name -> deviceprovisioningservice.pb.Credential.CredentialRefreshMethod + 14, // 7: deviceprovisioningservice.pb.Credential.optional_data:type_name -> deviceprovisioningservice.pb.CredentialOptionalData + 15, // 8: deviceprovisioningservice.pb.Credential.private_data:type_name -> deviceprovisioningservice.pb.CredentialPrivateData + 16, // 9: deviceprovisioningservice.pb.Credential.public_data:type_name -> deviceprovisioningservice.pb.CredentialPublicData + 17, // 10: deviceprovisioningservice.pb.Credential.role_id:type_name -> deviceprovisioningservice.pb.CredentialRoleID + 12, // 11: deviceprovisioningservice.pb.CredentialStatus.status:type_name -> deviceprovisioningservice.pb.ProvisionStatus + 13, // 12: deviceprovisioningservice.pb.CredentialStatus.pre_shared_key:type_name -> deviceprovisioningservice.pb.PreSharedKey + 18, // 13: deviceprovisioningservice.pb.CredentialStatus.credentials:type_name -> deviceprovisioningservice.pb.Credential + 12, // 14: deviceprovisioningservice.pb.OwnershipStatus.status:type_name -> deviceprovisioningservice.pb.ProvisionStatus + 6, // 15: deviceprovisioningservice.pb.AccessControlConnectionSubject.type:type_name -> deviceprovisioningservice.pb.AccessControlConnectionSubject.ConnectionType + 7, // 16: deviceprovisioningservice.pb.AccessControlResource.wildcard:type_name -> deviceprovisioningservice.pb.AccessControlResource.Wildcard + 21, // 17: deviceprovisioningservice.pb.AccessControl.device_subject:type_name -> deviceprovisioningservice.pb.AccessControlDeviceSubject + 22, // 18: deviceprovisioningservice.pb.AccessControl.role_subject:type_name -> deviceprovisioningservice.pb.AccessControlRoleSubject + 23, // 19: deviceprovisioningservice.pb.AccessControl.connection_subject:type_name -> deviceprovisioningservice.pb.AccessControlConnectionSubject + 8, // 20: deviceprovisioningservice.pb.AccessControl.permissions:type_name -> deviceprovisioningservice.pb.AccessControl.Permission + 24, // 21: deviceprovisioningservice.pb.AccessControl.resources:type_name -> deviceprovisioningservice.pb.AccessControlResource + 12, // 22: deviceprovisioningservice.pb.ACLStatus.status:type_name -> deviceprovisioningservice.pb.ProvisionStatus + 25, // 23: deviceprovisioningservice.pb.ACLStatus.access_control_list:type_name -> deviceprovisioningservice.pb.AccessControl + 12, // 24: deviceprovisioningservice.pb.CloudStatus.status:type_name -> deviceprovisioningservice.pb.ProvisionStatus + 31, // 25: deviceprovisioningservice.pb.CloudStatus.gateways:type_name -> deviceprovisioningservice.pb.CloudStatus.Gateway + 10, // 26: deviceprovisioningservice.pb.ProvisioningRecord.attestation:type_name -> deviceprovisioningservice.pb.Attestation + 19, // 27: deviceprovisioningservice.pb.ProvisioningRecord.credential:type_name -> deviceprovisioningservice.pb.CredentialStatus + 26, // 28: deviceprovisioningservice.pb.ProvisioningRecord.acl:type_name -> deviceprovisioningservice.pb.ACLStatus + 27, // 29: deviceprovisioningservice.pb.ProvisioningRecord.cloud:type_name -> deviceprovisioningservice.pb.CloudStatus + 20, // 30: deviceprovisioningservice.pb.ProvisioningRecord.ownership:type_name -> deviceprovisioningservice.pb.OwnershipStatus + 12, // 31: deviceprovisioningservice.pb.ProvisioningRecord.plgd_time:type_name -> deviceprovisioningservice.pb.ProvisionStatus + 32, // [32:32] is the sub-list for method output_type + 32, // [32:32] is the sub-list for method input_type + 32, // [32:32] is the sub-list for extension type_name + 32, // [32:32] is the sub-list for extension extendee + 0, // [0:32] is the sub-list for field type_name +} + +func init() { file_device_provisioning_service_pb_provisioningRecords_proto_init() } +func file_device_provisioning_service_pb_provisioningRecords_proto_init() { + if File_device_provisioning_service_pb_provisioningRecords_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*GetProvisioningRecordsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*Attestation); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[2].Exporter = func(v any, i int) any { + switch v := v.(*X509Attestation); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[3].Exporter = func(v any, i int) any { + switch v := v.(*ProvisionStatus); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[4].Exporter = func(v any, i int) any { + switch v := v.(*PreSharedKey); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[5].Exporter = func(v any, i int) any { + switch v := v.(*CredentialOptionalData); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[6].Exporter = func(v any, i int) any { + switch v := v.(*CredentialPrivateData); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[7].Exporter = func(v any, i int) any { + switch v := v.(*CredentialPublicData); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[8].Exporter = func(v any, i int) any { + switch v := v.(*CredentialRoleID); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[9].Exporter = func(v any, i int) any { + switch v := v.(*Credential); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[10].Exporter = func(v any, i int) any { + switch v := v.(*CredentialStatus); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[11].Exporter = func(v any, i int) any { + switch v := v.(*OwnershipStatus); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[12].Exporter = func(v any, i int) any { + switch v := v.(*AccessControlDeviceSubject); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[13].Exporter = func(v any, i int) any { + switch v := v.(*AccessControlRoleSubject); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[14].Exporter = func(v any, i int) any { + switch v := v.(*AccessControlConnectionSubject); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[15].Exporter = func(v any, i int) any { + switch v := v.(*AccessControlResource); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[16].Exporter = func(v any, i int) any { + switch v := v.(*AccessControl); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[17].Exporter = func(v any, i int) any { + switch v := v.(*ACLStatus); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[18].Exporter = func(v any, i int) any { + switch v := v.(*CloudStatus); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[19].Exporter = func(v any, i int) any { + switch v := v.(*ProvisioningRecord); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[20].Exporter = func(v any, i int) any { + switch v := v.(*DeleteProvisioningRecordsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[21].Exporter = func(v any, i int) any { + switch v := v.(*DeleteProvisioningRecordsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes[22].Exporter = func(v any, i int) any { + switch v := v.(*CloudStatus_Gateway); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_device_provisioning_service_pb_provisioningRecords_proto_rawDesc, + NumEnums: 9, + NumMessages: 23, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_device_provisioning_service_pb_provisioningRecords_proto_goTypes, + DependencyIndexes: file_device_provisioning_service_pb_provisioningRecords_proto_depIdxs, + EnumInfos: file_device_provisioning_service_pb_provisioningRecords_proto_enumTypes, + MessageInfos: file_device_provisioning_service_pb_provisioningRecords_proto_msgTypes, + }.Build() + File_device_provisioning_service_pb_provisioningRecords_proto = out.File + file_device_provisioning_service_pb_provisioningRecords_proto_rawDesc = nil + file_device_provisioning_service_pb_provisioningRecords_proto_goTypes = nil + file_device_provisioning_service_pb_provisioningRecords_proto_depIdxs = nil +} diff --git a/device-provisioning-service/pb/provisioningRecords.proto b/device-provisioning-service/pb/provisioningRecords.proto new file mode 100644 index 000000000..6d26b6ea6 --- /dev/null +++ b/device-provisioning-service/pb/provisioningRecords.proto @@ -0,0 +1,300 @@ +syntax = "proto3"; + +package deviceprovisioningservice.pb; + +option go_package = "github.com/plgd-dev/hub/v2/device-provisioning-service/pb;pb"; + +message GetProvisioningRecordsRequest { + // Filter by id. + repeated string id_filter = 1; + // Filter by device id. + repeated string device_id_filter = 2; + // Filter by enrollment group id. + repeated string enrollment_group_id_filter = 3; + +} + +message Attestation { + // Last time the device successfully established a TLS connection, in unix nanoseconds timestamp format. + int64 date = 1; // @gotags: bson:"date,omitempty" + // X509 attestation, set if used by the device. + X509Attestation x509 = 2; // @gotags: bson:"x509,omitempty" +} + +message X509Attestation { + // Last used x509 manufacturer certificate. + string certificate_pem = 1; // @gotags: bson:"certificate,omitempty" + string common_name = 2; // @gotags: bson:"commonName,omitempty" +} + +message ProvisionStatus { + // Last time the device requested provisioning, in unix nanoseconds timestamp format. + int64 date = 1; // @gotags: bson:"date,omitempty" + // The CoAP code returned to the device. + int32 coap_code = 2; // @gotags: bson:"coapCode,omitempty" + // Error message if any. + string error_message = 3; // @gotags: bson:"errorMessage,omitempty" +} + +message PreSharedKey { + // ID used to identify the owner by the device. + string subject_id = 1; // @gotags: bson:"subjectId,omitempty" + // Associated secret to the owner ID. + string key = 2; // @gotags: bson:"key,omitempty" +} + +// Credential Type dependent - eg revocation status information +message CredentialOptionalData { + enum Encoding { + UNKNOWN = 0; + RAW = 1; + JWT = 2; + CWT = 3; + BASE64 = 4; + PEM = 5; + DER = 6; + } + // Data to be provisioned. + bytes data = 1; + // Encoding of the data. + Encoding encoding = 2; + // If set, the credential is revoked. + bool is_revoked = 3; +} + +// Private credential information - non-public contents. +message CredentialPrivateData { + enum Encoding { + UNKNOWN = 0; + RAW = 1; + JWT = 2; + CWT = 3; + BASE64 = 4; + URI = 5; + HANDLE = 6; + } + // Data to be provisioned. + bytes data = 1; + // Encoding of the data. + Encoding encoding = 2; + // Handle to a key storage Resource. + int64 handle = 3; +} + +// Credential Type dependent - public contents. +message CredentialPublicData { + enum Encoding { + UNKNOWN = 0; + RAW = 1; + JWT = 2; + CWT = 3; + BASE64 = 4; + URI = 5; + PEM = 6; + DER = 7; + } + // Data to be provisioned. + bytes data = 1; + // Encoding of the data. + Encoding encoding = 2; +} + +message CredentialRoleID { + string authority = 1; + string role = 2; +} + +message Credential { + enum CredentialType { + EMPTY = 0; + SYMMETRIC_PAIR_WISE = 1; + SYMMETRIC_GROUP = 2; + ASYMMETRIC_SIGNING = 4; + ASYMMETRIC_SIGNING_WITH_CERTIFICATE = 8; + PIN_OR_PASSWORD = 16; + ASYMMETRIC_ENCRYPTION_KEY = 32; + } + + enum CredentialUsage { + NONE = 0; + TRUST_CA = 1; + CERT = 2; + ROLE_CERT = 3; + MFG_TRUST_CA = 4; + MFG_CERT = 5; + } + + enum CredentialRefreshMethod { + UNKNOWN = 0; + PROVISION_SERVICE = 1; + KEY_AGREEMENT_PROTOCOL_AND_RANDOM_PIN = 2; + KEY_AGREEMENT_PROTOCOL = 3; + KEY_DISTRIBUTION_SERVICE = 4; + PKCS10_REQUEST_TO_CA = 5; + } + + // Credential ID. If not set, the device will generate one. + int64 id = 1; // @gotags: bson:"id,omitempty" + // Credential type. + repeated CredentialType type = 2; // @gotags: bson:"type,omitempty" + // Credential subject. + string subject = 3; // @gotags: bson:"subject,omitempty" + // Credential usage. + CredentialUsage usage = 4; // @gotags: bson:"usage,omitempty" + // Supported credential refresh methods. + repeated CredentialRefreshMethod supported_refresh_methods = 5; // @gotags: bson:"supportedRefreshMethods,omitempty" + // Optional data. + CredentialOptionalData optional_data = 6; // @gotags: bson:"optionalData,omitempty" + // Period of validity in seconds. + string period = 7; // @gotags: bson:"period,omitempty" + // Private data. + CredentialPrivateData private_data = 8; // @gotags: bson:"privateData,omitempty" + // Public data. + CredentialPublicData public_data = 9; // @gotags: bson:"publicData,omitempty" + // Role ID. + CredentialRoleID role_id = 10; // @gotags: bson:"roleId,omitempty" +} + +message CredentialStatus { + ProvisionStatus status = 1; // @gotags: bson:"status,omitempty" + // Last identity certificate issued for the device. + string identity_certificate_pem = 2; // @gotags: bson:"identityCertificate,omitempty" + // Last pre shared key issued for the device. + PreSharedKey pre_shared_key = 3; // @gotags: bson:"preSharedKey,omitempty" + repeated Credential credentials = 4; // @gotags: bson:"credentials,omitempty" +} + +message OwnershipStatus { + ProvisionStatus status = 1; // @gotags: bson:"status,omitempty" + // Last provisioned owner to the device. + string owner = 2; // @gotags: bson:"owner,omitempty" +} + +message AccessControlDeviceSubject { + string device_id = 1; +} + +message AccessControlRoleSubject { + string authority = 1; + string role = 2; +} + +message AccessControlConnectionSubject { + enum ConnectionType { + // anonymous clear-text connection TCP or UDP without encryption + ANON_CLEAR = 0; + // authenticated encrypted connection using TLS or DTLS + AUTH_CRYPT = 1; + } + ConnectionType type = 1; +} + +message AccessControlResource { + enum Wildcard { + // no wildcard + NONE = 0; + // Shall match all Discoverable Non-Configuration Resources which expose at least one Secure OCF Endpoint. + NONCFG_SEC_ENDPOINT = 1; + // Shall match all Discoverable Non-Configuration Resources which expose at least one Unsecure OCF Endpoint. + NONCFG_NONSEC_ENDPOINT = 2; + // Shall match all Non-Configuration Resources. + NONCFG_ALL = 3; + } + // Resource href. + string href = 1; + // Resource type. + repeated string resource_types = 2; + // Resource interface. + repeated string interfaces = 3; + // Resource wildcard. + Wildcard wildcard = 4; +} + +message AccessControl { + enum Permission { + // create access + CREATE = 0; + // read-only access + READ = 1; + // read-write access + WRITE = 2; + // delete access + DELETE = 3; + // notify access + NOTIFY = 4; + } + // Subject of the ACL defines the entity to which the permissions are granted. Only one subject must be defined per ACL. + AccessControlDeviceSubject device_subject = 1; // @gotags: bson:"deviceSubject,omitempty" + AccessControlRoleSubject role_subject = 2; // @gotags: bson:"roleSubject,omitempty" + AccessControlConnectionSubject connection_subject = 3; // @gotags: bson:"connectionSubject,omitempty" + + // Permissions granted to the subject. + repeated Permission permissions = 4; + // Resources to which the permissions apply. + repeated AccessControlResource resources = 5; +} + +message ACLStatus { + ProvisionStatus status = 1; // @gotags: bson:"status,omitempty" + // Last ACL list provisioned to the device. + repeated AccessControl access_control_list = 2; // @gotags: bson:"accessControlList,omitempty" +} + +message CloudStatus { + reserved 2,4; // string coap_gateway = 2; string id = 4; + reserved "coap_gateway"; + ProvisionStatus status = 1; // @gotags: bson:"status,omitempty" + // Last provider name used to authenticate the device to the cloud. + string provider_name = 3; // @gotags: bson:"providerName,omitempty" + + message Gateway { + // Gateway endpoint in format ://: + string uri = 1; // @gotags: bson:"uri,omitempty" + // UUID of the gateway. + string id = 2; // @gotags: bson:"id,omitempty" + } + // Last provisioned gateways to the device. + repeated Gateway gateways = 5; // @gotags: bson:"gateways,omitempty" + int32 selected_gateway = 6; // @gotags: bson:"selectedGateway,omitempty" +} + +message ProvisioningRecord { + // Registration id, calculated from the manufacturer certificate public key info. + string id = 1; // @gotags: bson:"_id,omitempty" + // ID of the device to which this record belongs to. + string device_id = 2; // @gotags: bson:"deviceId,omitempty" + // Assigned enrollment group. + string enrollment_group_id = 3; // @gotags: bson:"enrollmentGroupId,omitempty" + // Record creation date, in unix nanoseconds timestamp format. + int64 creation_date = 4; // @gotags: bson:"creationDate,omitempty" + // Last device attestation overview. + Attestation attestation = 5; // @gotags: bson:"attestation,omitempty" + // Last credential provision overview. + CredentialStatus credential = 6; // @gotags: bson:"credential,omitempty" + // Last ACL provision overview. + ACLStatus acl = 7; // @gotags: bson:"acl,omitempty" + // Last cloud provision overview. + CloudStatus cloud = 8; // @gotags: bson:"cloud,omitempty" + // Last ownership provision overview. + OwnershipStatus ownership = 9; // @gotags: bson:"ownership,omitempty" + // Last plgd-time provision overview. + ProvisionStatus plgd_time = 10; // @gotags: bson:"plgdTime,omitempty" + // Last local endpoints + repeated string local_endpoints = 11; // @gotags: bson:"localEndpoints,omitempty" + // Owner ID. + string owner = 12; // @gotags: bson:"owner,omitempty" +} + +message DeleteProvisioningRecordsRequest { + // Filter by id. + repeated string id_filter = 1; + // Filter by device id. + repeated string device_id_filter = 2; + // Filter by enrollment group id. + repeated string enrollment_group_id_filter = 3; +} + +message DeleteProvisioningRecordsResponse { + // Number of deleted records. + int64 count = 1; +} diff --git a/device-provisioning-service/pb/service.pb.gw.go b/device-provisioning-service/pb/service.pb.gw.go new file mode 100644 index 000000000..de974a71a --- /dev/null +++ b/device-provisioning-service/pb/service.pb.gw.go @@ -0,0 +1,938 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: github.com/plgd-dev/hub/v2/device-provisioning-service/pb/service.proto + +/* +Package pb is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package pb + +import ( + "context" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var _ codes.Code +var _ io.Reader +var _ status.Status +var _ = runtime.String +var _ = utilities.NewDoubleArray +var _ = metadata.Join + +var ( + filter_DeviceProvisionService_GetProvisioningRecords_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_DeviceProvisionService_GetProvisioningRecords_0(ctx context.Context, marshaler runtime.Marshaler, client DeviceProvisionServiceClient, req *http.Request, pathParams map[string]string) (DeviceProvisionService_GetProvisioningRecordsClient, runtime.ServerMetadata, error) { + var protoReq GetProvisioningRecordsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_DeviceProvisionService_GetProvisioningRecords_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + stream, err := client.GetProvisioningRecords(ctx, &protoReq) + if err != nil { + return nil, metadata, err + } + header, err := stream.Header() + if err != nil { + return nil, metadata, err + } + metadata.HeaderMD = header + return stream, metadata, nil + +} + +var ( + filter_DeviceProvisionService_DeleteProvisioningRecords_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_DeviceProvisionService_DeleteProvisioningRecords_0(ctx context.Context, marshaler runtime.Marshaler, client DeviceProvisionServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DeleteProvisioningRecordsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_DeviceProvisionService_DeleteProvisioningRecords_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.DeleteProvisioningRecords(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_DeviceProvisionService_DeleteProvisioningRecords_0(ctx context.Context, marshaler runtime.Marshaler, server DeviceProvisionServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DeleteProvisioningRecordsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_DeviceProvisionService_DeleteProvisioningRecords_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.DeleteProvisioningRecords(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_DeviceProvisionService_GetEnrollmentGroups_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_DeviceProvisionService_GetEnrollmentGroups_0(ctx context.Context, marshaler runtime.Marshaler, client DeviceProvisionServiceClient, req *http.Request, pathParams map[string]string) (DeviceProvisionService_GetEnrollmentGroupsClient, runtime.ServerMetadata, error) { + var protoReq GetEnrollmentGroupsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_DeviceProvisionService_GetEnrollmentGroups_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + stream, err := client.GetEnrollmentGroups(ctx, &protoReq) + if err != nil { + return nil, metadata, err + } + header, err := stream.Header() + if err != nil { + return nil, metadata, err + } + metadata.HeaderMD = header + return stream, metadata, nil + +} + +func request_DeviceProvisionService_CreateEnrollmentGroup_0(ctx context.Context, marshaler runtime.Marshaler, client DeviceProvisionServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq CreateEnrollmentGroupRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.CreateEnrollmentGroup(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_DeviceProvisionService_CreateEnrollmentGroup_0(ctx context.Context, marshaler runtime.Marshaler, server DeviceProvisionServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq CreateEnrollmentGroupRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.CreateEnrollmentGroup(ctx, &protoReq) + return msg, metadata, err + +} + +func request_DeviceProvisionService_UpdateEnrollmentGroup_0(ctx context.Context, marshaler runtime.Marshaler, client DeviceProvisionServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UpdateEnrollmentGroupRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.EnrollmentGroup); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + + msg, err := client.UpdateEnrollmentGroup(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_DeviceProvisionService_UpdateEnrollmentGroup_0(ctx context.Context, marshaler runtime.Marshaler, server DeviceProvisionServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UpdateEnrollmentGroupRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.EnrollmentGroup); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + + msg, err := server.UpdateEnrollmentGroup(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_DeviceProvisionService_DeleteEnrollmentGroups_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_DeviceProvisionService_DeleteEnrollmentGroups_0(ctx context.Context, marshaler runtime.Marshaler, client DeviceProvisionServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DeleteEnrollmentGroupsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_DeviceProvisionService_DeleteEnrollmentGroups_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.DeleteEnrollmentGroups(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_DeviceProvisionService_DeleteEnrollmentGroups_0(ctx context.Context, marshaler runtime.Marshaler, server DeviceProvisionServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DeleteEnrollmentGroupsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_DeviceProvisionService_DeleteEnrollmentGroups_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.DeleteEnrollmentGroups(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_DeviceProvisionService_GetHubs_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_DeviceProvisionService_GetHubs_0(ctx context.Context, marshaler runtime.Marshaler, client DeviceProvisionServiceClient, req *http.Request, pathParams map[string]string) (DeviceProvisionService_GetHubsClient, runtime.ServerMetadata, error) { + var protoReq GetHubsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_DeviceProvisionService_GetHubs_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + stream, err := client.GetHubs(ctx, &protoReq) + if err != nil { + return nil, metadata, err + } + header, err := stream.Header() + if err != nil { + return nil, metadata, err + } + metadata.HeaderMD = header + return stream, metadata, nil + +} + +func request_DeviceProvisionService_CreateHub_0(ctx context.Context, marshaler runtime.Marshaler, client DeviceProvisionServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq CreateHubRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.CreateHub(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_DeviceProvisionService_CreateHub_0(ctx context.Context, marshaler runtime.Marshaler, server DeviceProvisionServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq CreateHubRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.CreateHub(ctx, &protoReq) + return msg, metadata, err + +} + +func request_DeviceProvisionService_UpdateHub_0(ctx context.Context, marshaler runtime.Marshaler, client DeviceProvisionServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UpdateHubRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Hub); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + + msg, err := client.UpdateHub(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_DeviceProvisionService_UpdateHub_0(ctx context.Context, marshaler runtime.Marshaler, server DeviceProvisionServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UpdateHubRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Hub); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + + msg, err := server.UpdateHub(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_DeviceProvisionService_DeleteHubs_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_DeviceProvisionService_DeleteHubs_0(ctx context.Context, marshaler runtime.Marshaler, client DeviceProvisionServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DeleteHubsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_DeviceProvisionService_DeleteHubs_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.DeleteHubs(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_DeviceProvisionService_DeleteHubs_0(ctx context.Context, marshaler runtime.Marshaler, server DeviceProvisionServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DeleteHubsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_DeviceProvisionService_DeleteHubs_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.DeleteHubs(ctx, &protoReq) + return msg, metadata, err + +} + +// RegisterDeviceProvisionServiceHandlerServer registers the http handlers for service DeviceProvisionService to "mux". +// UnaryRPC :call DeviceProvisionServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterDeviceProvisionServiceHandlerFromEndpoint instead. +func RegisterDeviceProvisionServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server DeviceProvisionServiceServer) error { + + mux.Handle("GET", pattern_DeviceProvisionService_GetProvisioningRecords_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") + _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + }) + + mux.Handle("DELETE", pattern_DeviceProvisionService_DeleteProvisioningRecords_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/DeleteProvisioningRecords", runtime.WithHTTPPathPattern("/api/v1/provisioning-records")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_DeviceProvisionService_DeleteProvisioningRecords_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_DeleteProvisioningRecords_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_DeviceProvisionService_GetEnrollmentGroups_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") + _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + }) + + mux.Handle("POST", pattern_DeviceProvisionService_CreateEnrollmentGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/CreateEnrollmentGroup", runtime.WithHTTPPathPattern("/api/v1/enrollment-groups")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_DeviceProvisionService_CreateEnrollmentGroup_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_CreateEnrollmentGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("PUT", pattern_DeviceProvisionService_UpdateEnrollmentGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/UpdateEnrollmentGroup", runtime.WithHTTPPathPattern("/api/v1/enrollment-groups/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_DeviceProvisionService_UpdateEnrollmentGroup_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_UpdateEnrollmentGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("DELETE", pattern_DeviceProvisionService_DeleteEnrollmentGroups_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/DeleteEnrollmentGroups", runtime.WithHTTPPathPattern("/api/v1/enrollment-groups")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_DeviceProvisionService_DeleteEnrollmentGroups_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_DeleteEnrollmentGroups_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_DeviceProvisionService_GetHubs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") + _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + }) + + mux.Handle("POST", pattern_DeviceProvisionService_CreateHub_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/CreateHub", runtime.WithHTTPPathPattern("/api/v1/hubs")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_DeviceProvisionService_CreateHub_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_CreateHub_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("PUT", pattern_DeviceProvisionService_UpdateHub_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/UpdateHub", runtime.WithHTTPPathPattern("/api/v1/hubs/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_DeviceProvisionService_UpdateHub_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_UpdateHub_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("DELETE", pattern_DeviceProvisionService_DeleteHubs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/DeleteHubs", runtime.WithHTTPPathPattern("/api/v1/hubs")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_DeviceProvisionService_DeleteHubs_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_DeleteHubs_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +// RegisterDeviceProvisionServiceHandlerFromEndpoint is same as RegisterDeviceProvisionServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterDeviceProvisionServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.DialContext(ctx, endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterDeviceProvisionServiceHandler(ctx, mux, conn) +} + +// RegisterDeviceProvisionServiceHandler registers the http handlers for service DeviceProvisionService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterDeviceProvisionServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterDeviceProvisionServiceHandlerClient(ctx, mux, NewDeviceProvisionServiceClient(conn)) +} + +// RegisterDeviceProvisionServiceHandlerClient registers the http handlers for service DeviceProvisionService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "DeviceProvisionServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "DeviceProvisionServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "DeviceProvisionServiceClient" to call the correct interceptors. +func RegisterDeviceProvisionServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client DeviceProvisionServiceClient) error { + + mux.Handle("GET", pattern_DeviceProvisionService_GetProvisioningRecords_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/GetProvisioningRecords", runtime.WithHTTPPathPattern("/api/v1/provisioning-records")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_DeviceProvisionService_GetProvisioningRecords_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_GetProvisioningRecords_0(annotatedContext, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("DELETE", pattern_DeviceProvisionService_DeleteProvisioningRecords_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/DeleteProvisioningRecords", runtime.WithHTTPPathPattern("/api/v1/provisioning-records")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_DeviceProvisionService_DeleteProvisioningRecords_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_DeleteProvisioningRecords_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_DeviceProvisionService_GetEnrollmentGroups_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/GetEnrollmentGroups", runtime.WithHTTPPathPattern("/api/v1/enrollment-groups")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_DeviceProvisionService_GetEnrollmentGroups_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_GetEnrollmentGroups_0(annotatedContext, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_DeviceProvisionService_CreateEnrollmentGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/CreateEnrollmentGroup", runtime.WithHTTPPathPattern("/api/v1/enrollment-groups")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_DeviceProvisionService_CreateEnrollmentGroup_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_CreateEnrollmentGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("PUT", pattern_DeviceProvisionService_UpdateEnrollmentGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/UpdateEnrollmentGroup", runtime.WithHTTPPathPattern("/api/v1/enrollment-groups/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_DeviceProvisionService_UpdateEnrollmentGroup_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_UpdateEnrollmentGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("DELETE", pattern_DeviceProvisionService_DeleteEnrollmentGroups_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/DeleteEnrollmentGroups", runtime.WithHTTPPathPattern("/api/v1/enrollment-groups")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_DeviceProvisionService_DeleteEnrollmentGroups_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_DeleteEnrollmentGroups_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_DeviceProvisionService_GetHubs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/GetHubs", runtime.WithHTTPPathPattern("/api/v1/hubs")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_DeviceProvisionService_GetHubs_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_GetHubs_0(annotatedContext, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_DeviceProvisionService_CreateHub_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/CreateHub", runtime.WithHTTPPathPattern("/api/v1/hubs")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_DeviceProvisionService_CreateHub_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_CreateHub_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("PUT", pattern_DeviceProvisionService_UpdateHub_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/UpdateHub", runtime.WithHTTPPathPattern("/api/v1/hubs/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_DeviceProvisionService_UpdateHub_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_UpdateHub_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("DELETE", pattern_DeviceProvisionService_DeleteHubs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/deviceprovisioningservice.pb.DeviceProvisionService/DeleteHubs", runtime.WithHTTPPathPattern("/api/v1/hubs")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_DeviceProvisionService_DeleteHubs_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_DeviceProvisionService_DeleteHubs_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_DeviceProvisionService_GetProvisioningRecords_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "provisioning-records"}, "")) + + pattern_DeviceProvisionService_DeleteProvisioningRecords_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "provisioning-records"}, "")) + + pattern_DeviceProvisionService_GetEnrollmentGroups_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "enrollment-groups"}, "")) + + pattern_DeviceProvisionService_CreateEnrollmentGroup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "enrollment-groups"}, "")) + + pattern_DeviceProvisionService_UpdateEnrollmentGroup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "enrollment-groups", "id"}, "")) + + pattern_DeviceProvisionService_DeleteEnrollmentGroups_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "enrollment-groups"}, "")) + + pattern_DeviceProvisionService_GetHubs_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "hubs"}, "")) + + pattern_DeviceProvisionService_CreateHub_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "hubs"}, "")) + + pattern_DeviceProvisionService_UpdateHub_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "hubs", "id"}, "")) + + pattern_DeviceProvisionService_DeleteHubs_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "hubs"}, "")) +) + +var ( + forward_DeviceProvisionService_GetProvisioningRecords_0 = runtime.ForwardResponseStream + + forward_DeviceProvisionService_DeleteProvisioningRecords_0 = runtime.ForwardResponseMessage + + forward_DeviceProvisionService_GetEnrollmentGroups_0 = runtime.ForwardResponseStream + + forward_DeviceProvisionService_CreateEnrollmentGroup_0 = runtime.ForwardResponseMessage + + forward_DeviceProvisionService_UpdateEnrollmentGroup_0 = runtime.ForwardResponseMessage + + forward_DeviceProvisionService_DeleteEnrollmentGroups_0 = runtime.ForwardResponseMessage + + forward_DeviceProvisionService_GetHubs_0 = runtime.ForwardResponseStream + + forward_DeviceProvisionService_CreateHub_0 = runtime.ForwardResponseMessage + + forward_DeviceProvisionService_UpdateHub_0 = runtime.ForwardResponseMessage + + forward_DeviceProvisionService_DeleteHubs_0 = runtime.ForwardResponseMessage +) diff --git a/device-provisioning-service/pb/service.proto b/device-provisioning-service/pb/service.proto new file mode 100644 index 000000000..c761901ec --- /dev/null +++ b/device-provisioning-service/pb/service.proto @@ -0,0 +1,129 @@ +syntax = "proto3"; + +package deviceprovisioningservice.pb; + +import "pb/provisioningRecords.proto"; +import "pb/enrollmentGroup.proto"; +import "pb/hub.proto"; + +import "google/api/annotations.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "plgd hub - HTTP Provisioned device service API"; + version: "1.0"; + description: ""; + contact: { + name: "plgd.dev"; + url: "https://github.com/plgd-dev/hub/v2/device-provisioning-service"; + email: "info@plgd.dev"; + }; + license: { + name: "Commercial"; + }; + }; + schemes: [ HTTPS ]; + consumes: [ "application/json", "application/protojson" ]; + produces: [ "application/json", "application/protojson" ]; +}; + +option go_package = "github.com/plgd-dev/hub/v2/device-provisioning-service/pb;pb"; +// https://github.com/googleapis/googleapis/blob/master/google/api/http.proto + +service DeviceProvisionService { + // Get registrations of devices + rpc GetProvisioningRecords (GetProvisioningRecordsRequest) returns (stream ProvisioningRecord) { + option (google.api.http) = { + get: "/api/v1/provisioning-records" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Provisioning records" ] + }; + }; + + rpc DeleteProvisioningRecords (DeleteProvisioningRecordsRequest) returns (DeleteProvisioningRecordsResponse) { + option (google.api.http) = { + delete: "/api/v1/provisioning-records" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Provisioning record" ] + }; + }; + + rpc GetEnrollmentGroups (GetEnrollmentGroupsRequest) returns (stream EnrollmentGroup) { + option (google.api.http) = { + get: "/api/v1/enrollment-groups" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Enrollment Groups" ] + }; + }; + + rpc CreateEnrollmentGroup (CreateEnrollmentGroupRequest) returns (EnrollmentGroup) { + option (google.api.http) = { + post: "/api/v1/enrollment-groups" + body: "*" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Enrollment Groups" ] + }; + }; + + rpc UpdateEnrollmentGroup (UpdateEnrollmentGroupRequest) returns (EnrollmentGroup) { + option (google.api.http) = { + put: "/api/v1/enrollment-groups/{id}" + body: "enrollment_group" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Enrollment Group" ] + }; + }; + + rpc DeleteEnrollmentGroups (DeleteEnrollmentGroupsRequest) returns (DeleteEnrollmentGroupsResponse) { + option (google.api.http) = { + delete: "/api/v1/enrollment-groups" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Enrollment Group" ] + }; + }; + + rpc GetHubs (GetHubsRequest) returns (stream Hub) { + option (google.api.http) = { + get: "/api/v1/hubs" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Hubs" ] + }; + }; + + rpc CreateHub (CreateHubRequest) returns (Hub) { + option (google.api.http) = { + post: "/api/v1/hubs" + body: "*" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Hubs" ] + }; + }; + + rpc UpdateHub (UpdateHubRequest) returns (Hub) { + option (google.api.http) = { + put: "/api/v1/hubs/{id}" + body: "hub" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Hub" ] + }; + }; + + rpc DeleteHubs (DeleteHubsRequest) returns (DeleteHubsResponse) { + option (google.api.http) = { + delete: "/api/v1/hubs" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Hub" ] + }; + }; +} diff --git a/device-provisioning-service/pb/service.swagger.json b/device-provisioning-service/pb/service.swagger.json new file mode 100644 index 000000000..0c606939d --- /dev/null +++ b/device-provisioning-service/pb/service.swagger.json @@ -0,0 +1,1443 @@ +{ + "swagger": "2.0", + "info": { + "title": "plgd hub - HTTP Provisioned device service API", + "version": "1.0", + "contact": { + "name": "plgd.dev", + "url": "https://github.com/plgd-dev/hub/tree/main/device-provisioning-service", + "email": "info@plgd.dev" + }, + "license": { + "name": "Commercial" + } + }, + "tags": [ + { + "name": "DeviceProvisionService" + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json", + "application/protojson" + ], + "produces": [ + "application/json", + "application/protojson" + ], + "paths": { + "/api/v1/enrollment-groups": { + "get": { + "operationId": "DeviceProvisionService_GetEnrollmentGroups", + "responses": { + "200": { + "description": "A successful response.(streaming responses)", + "schema": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/pbEnrollmentGroup" + }, + "error": { + "$ref": "#/definitions/rpcStatus" + } + }, + "title": "Stream result of pbEnrollmentGroup" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "idFilter", + "description": "Filter by id.", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }, + { + "name": "attestationMechanismX509CertificateNames", + "description": "Filter by certificates comman names in x509 attestation mechanism", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }, + { + "name": "hubIdFilter", + "description": "Filter by hubId.", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "Enrollment Groups" + ] + }, + "delete": { + "operationId": "DeviceProvisionService_DeleteEnrollmentGroups", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbDeleteEnrollmentGroupsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "idFilter", + "description": "Enrollment group ID.", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "Enrollment Group" + ] + }, + "post": { + "operationId": "DeviceProvisionService_CreateEnrollmentGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbEnrollmentGroup" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/pbCreateEnrollmentGroupRequest" + } + } + ], + "tags": [ + "Enrollment Groups" + ] + } + }, + "/api/v1/enrollment-groups/{id}": { + "put": { + "operationId": "DeviceProvisionService_UpdateEnrollmentGroup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbEnrollmentGroup" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "id", + "description": "Enrollment group ID.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "enrollmentGroup", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/pbUpdateEnrollmentGroup" + } + } + ], + "tags": [ + "Enrollment Group" + ] + } + }, + "/api/v1/hubs": { + "get": { + "operationId": "DeviceProvisionService_GetHubs", + "responses": { + "200": { + "description": "A successful response.(streaming responses)", + "schema": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/pbHub" + }, + "error": { + "$ref": "#/definitions/rpcStatus" + } + }, + "title": "Stream result of pbHub" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "idFilter", + "description": "Filter by id.", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }, + { + "name": "hubIdFilter", + "description": "Filter by hub_id.", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "Hubs" + ] + }, + "delete": { + "operationId": "DeviceProvisionService_DeleteHubs", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbDeleteHubsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "idFilter", + "description": "Record ID.", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "Hub" + ] + }, + "post": { + "operationId": "DeviceProvisionService_CreateHub", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbHub" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/pbCreateHubRequest" + } + } + ], + "tags": [ + "Hubs" + ] + } + }, + "/api/v1/hubs/{id}": { + "put": { + "operationId": "DeviceProvisionService_UpdateHub", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbHub" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "id", + "description": "Record ID.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "hub", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/pbUpdateHub" + } + } + ], + "tags": [ + "Hub" + ] + } + }, + "/api/v1/provisioning-records": { + "get": { + "summary": "Get registrations of devices", + "operationId": "DeviceProvisionService_GetProvisioningRecords", + "responses": { + "200": { + "description": "A successful response.(streaming responses)", + "schema": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/pbProvisioningRecord" + }, + "error": { + "$ref": "#/definitions/rpcStatus" + } + }, + "title": "Stream result of pbProvisioningRecord" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "idFilter", + "description": "Filter by id.", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }, + { + "name": "deviceIdFilter", + "description": "Filter by device id.", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }, + { + "name": "enrollmentGroupIdFilter", + "description": "Filter by enrollment group id.", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "Provisioning records" + ] + }, + "delete": { + "operationId": "DeviceProvisionService_DeleteProvisioningRecords", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbDeleteProvisioningRecordsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "idFilter", + "description": "Filter by id.", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }, + { + "name": "deviceIdFilter", + "description": "Filter by device id.", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }, + { + "name": "enrollmentGroupIdFilter", + "description": "Filter by enrollment group id.", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "Provisioning record" + ] + } + } + }, + "definitions": { + "AccessControlConnectionSubjectConnectionType": { + "type": "string", + "enum": [ + "ANON_CLEAR", + "AUTH_CRYPT" + ], + "default": "ANON_CLEAR", + "title": "- ANON_CLEAR: anonymous clear-text connection TCP or UDP without encryption\n - AUTH_CRYPT: authenticated encrypted connection using TLS or DTLS" + }, + "AccessControlPermission": { + "type": "string", + "enum": [ + "CREATE", + "READ", + "WRITE", + "DELETE", + "NOTIFY" + ], + "default": "CREATE", + "title": "- CREATE: create access\n - READ: read-only access\n - WRITE: read-write access\n - DELETE: delete access\n - NOTIFY: notify access" + }, + "AccessControlResourceWildcard": { + "type": "string", + "enum": [ + "NONE", + "NONCFG_SEC_ENDPOINT", + "NONCFG_NONSEC_ENDPOINT", + "NONCFG_ALL" + ], + "default": "NONE", + "description": " - NONE: no wildcard\n - NONCFG_SEC_ENDPOINT: Shall match all Discoverable Non-Configuration Resources which expose at least one Secure OCF Endpoint.\n - NONCFG_NONSEC_ENDPOINT: Shall match all Discoverable Non-Configuration Resources which expose at least one Unsecure OCF Endpoint.\n - NONCFG_ALL: Shall match all Non-Configuration Resources." + }, + "CloudStatusGateway": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "@gotags: bson:\"uri,omitempty\"", + "title": "Gateway endpoint in format \u003cscheme\u003e://\u003chost\u003e:\u003cport\u003e" + }, + "id": { + "type": "string", + "description": "UUID of the gateway.\n\n@gotags: bson:\"id,omitempty\"" + } + } + }, + "CredentialCredentialRefreshMethod": { + "type": "string", + "enum": [ + "UNKNOWN", + "PROVISION_SERVICE", + "KEY_AGREEMENT_PROTOCOL_AND_RANDOM_PIN", + "KEY_AGREEMENT_PROTOCOL", + "KEY_DISTRIBUTION_SERVICE", + "PKCS10_REQUEST_TO_CA" + ], + "default": "UNKNOWN" + }, + "CredentialCredentialType": { + "type": "string", + "enum": [ + "EMPTY", + "SYMMETRIC_PAIR_WISE", + "SYMMETRIC_GROUP", + "ASYMMETRIC_SIGNING", + "ASYMMETRIC_SIGNING_WITH_CERTIFICATE", + "PIN_OR_PASSWORD", + "ASYMMETRIC_ENCRYPTION_KEY" + ], + "default": "EMPTY" + }, + "CredentialCredentialUsage": { + "type": "string", + "enum": [ + "NONE", + "TRUST_CA", + "CERT", + "ROLE_CERT", + "MFG_TRUST_CA", + "MFG_CERT" + ], + "default": "NONE" + }, + "pbACLStatus": { + "type": "object", + "properties": { + "status": { + "$ref": "#/definitions/pbProvisionStatus", + "title": "@gotags: bson:\"status,omitempty\"" + }, + "accessControlList": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/pbAccessControl" + }, + "description": "Last ACL list provisioned to the device.\n\n@gotags: bson:\"accessControlList,omitempty\"" + } + } + }, + "pbAccessControl": { + "type": "object", + "properties": { + "deviceSubject": { + "$ref": "#/definitions/pbAccessControlDeviceSubject", + "description": "Subject of the ACL defines the entity to which the permissions are granted. Only one subject must be defined per ACL.\n\n@gotags: bson:\"deviceSubject,omitempty\"" + }, + "roleSubject": { + "$ref": "#/definitions/pbAccessControlRoleSubject", + "title": "@gotags: bson:\"roleSubject,omitempty\"" + }, + "connectionSubject": { + "$ref": "#/definitions/pbAccessControlConnectionSubject", + "title": "@gotags: bson:\"connectionSubject,omitempty\"" + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/definitions/AccessControlPermission" + }, + "description": "Permissions granted to the subject." + }, + "resources": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/pbAccessControlResource" + }, + "description": "Resources to which the permissions apply." + } + } + }, + "pbAccessControlConnectionSubject": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/AccessControlConnectionSubjectConnectionType" + } + } + }, + "pbAccessControlDeviceSubject": { + "type": "object", + "properties": { + "deviceId": { + "type": "string" + } + } + }, + "pbAccessControlResource": { + "type": "object", + "properties": { + "href": { + "type": "string", + "description": "Resource href." + }, + "resourceTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Resource type." + }, + "interfaces": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Resource interface." + }, + "wildcard": { + "$ref": "#/definitions/AccessControlResourceWildcard", + "description": "Resource wildcard." + } + } + }, + "pbAccessControlRoleSubject": { + "type": "object", + "properties": { + "authority": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "pbAttestation": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "int64", + "description": "Last time the device successfully established a TLS connection, in unix nanoseconds timestamp format.\n\n@gotags: bson:\"date,omitempty\"" + }, + "x509": { + "$ref": "#/definitions/pbX509Attestation", + "description": "X509 attestation, set if used by the device.\n\n@gotags: bson:\"x509,omitempty\"" + } + } + }, + "pbAttestationMechanism": { + "type": "object", + "properties": { + "x509": { + "$ref": "#/definitions/pbX509Configuration", + "description": "@gotags: bson:\"x509\"", + "title": "X509 attestation" + } + } + }, + "pbAuthorizationConfig": { + "type": "object", + "properties": { + "ownerClaim": { + "type": "string", + "description": "owner_claim is key where will be stored owner in JWT.\n\n@gotags: bson:\"ownerClaim\"" + }, + "deviceIdClaim": { + "type": "string", + "description": "@gotags: bson:\"deviceIdClaim\"", + "title": "device_id_claim is key where will be stored deviceID in JWT(optional)" + }, + "provider": { + "$ref": "#/definitions/pbAuthorizationProviderConfig", + "title": "@gotags: bson:\"provider\"" + } + }, + "description": "AuthorizationConfig is used to generate the authorization code for the device when providing cloud configuration." + }, + "pbAuthorizationProviderConfig": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "@gotags: bson:\"name\"", + "title": "the name of the provider, which is set in configuration in coap-gateway" + }, + "authority": { + "type": "string", + "description": "@gotags: bson:\"authority\"", + "title": "the url to get oauth endpoints" + }, + "clientId": { + "type": "string", + "description": "@gotags: bson:\"clientId\"", + "title": "client id which is associated to the name" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "@gotags: bson:\"scopes\"", + "title": "scopes will be set in token" + }, + "audience": { + "type": "string", + "description": "@gotags: bson:\"audience\"", + "title": "audience will be set in token" + }, + "clientSecret": { + "type": "string", + "description": "@gotags: bson:\"clientSecret\"", + "title": "client secret. Supported formats: \u003c/path/to/clientSecret\u003e,\u003cdata:;base64,{ClientSecret in BASE64}\u003e" + }, + "http": { + "$ref": "#/definitions/pbHttpConfig", + "description": "@gotags: bson:\"http\"", + "title": "http configuration" + } + } + }, + "pbCloudStatus": { + "type": "object", + "properties": { + "status": { + "$ref": "#/definitions/pbProvisionStatus", + "title": "@gotags: bson:\"status,omitempty\"" + }, + "providerName": { + "type": "string", + "description": "Last provider name used to authenticate the device to the cloud.\n\n@gotags: bson:\"providerName,omitempty\"" + }, + "gateways": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/CloudStatusGateway" + }, + "description": "Last provisioned gateways to the device.\n\n@gotags: bson:\"gateways,omitempty\"" + }, + "selectedGateway": { + "type": "integer", + "format": "int32", + "title": "@gotags: bson:\"selectedGateway,omitempty\"" + } + } + }, + "pbCreateEnrollmentGroupRequest": { + "type": "object", + "properties": { + "attestationMechanism": { + "$ref": "#/definitions/pbAttestationMechanism", + "title": "Attestation mechanism" + }, + "hubIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Hub configuration to configure device." + }, + "preSharedKey": { + "type": "string", + "title": "Pre shared key for devices in enrollment group. It can be used for maintenance operations by d2d client. Supported formats: \u003c/path/to/psk\u003e,\u003cdata:;base64,{PSK in BASE64}\u003e" + }, + "name": { + "type": "string", + "title": "name of enrollment group" + } + } + }, + "pbCreateHubRequest": { + "type": "object", + "properties": { + "hubId": { + "type": "string", + "description": "Hub identifier - it must match with common name of gateway(coap-gateway) hub certificate." + }, + "gateways": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Address of gateways in format scheme://host:port" + }, + "certificateAuthority": { + "$ref": "#/definitions/pbGrpcClientConfig", + "description": "Signs identity ceritificate for the device." + }, + "authorization": { + "$ref": "#/definitions/pbAuthorizationConfig", + "description": "Acquire HUB authorization code for the device." + }, + "name": { + "type": "string", + "description": "Hub name." + } + } + }, + "pbCredential": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "int64", + "description": "Credential ID. If not set, the device will generate one.\n\n@gotags: bson:\"id,omitempty\"" + }, + "type": { + "type": "array", + "items": { + "$ref": "#/definitions/CredentialCredentialType" + }, + "description": "Credential type.\n\n@gotags: bson:\"type,omitempty\"" + }, + "subject": { + "type": "string", + "description": "Credential subject.\n\n@gotags: bson:\"subject,omitempty\"" + }, + "usage": { + "$ref": "#/definitions/CredentialCredentialUsage", + "description": "Credential usage.\n\n@gotags: bson:\"usage,omitempty\"" + }, + "supportedRefreshMethods": { + "type": "array", + "items": { + "$ref": "#/definitions/CredentialCredentialRefreshMethod" + }, + "description": "Supported credential refresh methods.\n\n@gotags: bson:\"supportedRefreshMethods,omitempty\"" + }, + "optionalData": { + "$ref": "#/definitions/pbCredentialOptionalData", + "description": "Optional data.\n\n@gotags: bson:\"optionalData,omitempty\"" + }, + "period": { + "type": "string", + "description": "Period of validity in seconds.\n\n@gotags: bson:\"period,omitempty\"" + }, + "privateData": { + "$ref": "#/definitions/pbCredentialPrivateData", + "description": "Private data.\n\n@gotags: bson:\"privateData,omitempty\"" + }, + "publicData": { + "$ref": "#/definitions/pbCredentialPublicData", + "description": "Public data.\n\n@gotags: bson:\"publicData,omitempty\"" + }, + "roleId": { + "$ref": "#/definitions/pbCredentialRoleID", + "description": "Role ID.\n\n@gotags: bson:\"roleId,omitempty\"" + } + } + }, + "pbCredentialOptionalData": { + "type": "object", + "properties": { + "data": { + "type": "string", + "format": "byte", + "description": "Data to be provisioned." + }, + "encoding": { + "$ref": "#/definitions/pbCredentialOptionalDataEncoding", + "description": "Encoding of the data." + }, + "isRevoked": { + "type": "boolean", + "description": "If set, the credential is revoked." + } + }, + "title": "Credential Type dependent - eg revocation status information" + }, + "pbCredentialOptionalDataEncoding": { + "type": "string", + "enum": [ + "UNKNOWN", + "RAW", + "JWT", + "CWT", + "BASE64", + "PEM", + "DER" + ], + "default": "UNKNOWN" + }, + "pbCredentialPrivateData": { + "type": "object", + "properties": { + "data": { + "type": "string", + "format": "byte", + "description": "Data to be provisioned." + }, + "encoding": { + "$ref": "#/definitions/pbCredentialPrivateDataEncoding", + "description": "Encoding of the data." + }, + "handle": { + "type": "string", + "format": "int64", + "description": "Handle to a key storage Resource." + } + }, + "description": "Private credential information - non-public contents." + }, + "pbCredentialPrivateDataEncoding": { + "type": "string", + "enum": [ + "UNKNOWN", + "RAW", + "JWT", + "CWT", + "BASE64", + "URI", + "HANDLE" + ], + "default": "UNKNOWN" + }, + "pbCredentialPublicData": { + "type": "object", + "properties": { + "data": { + "type": "string", + "format": "byte", + "description": "Data to be provisioned." + }, + "encoding": { + "$ref": "#/definitions/pbCredentialPublicDataEncoding", + "description": "Encoding of the data." + } + }, + "description": "Credential Type dependent - public contents." + }, + "pbCredentialPublicDataEncoding": { + "type": "string", + "enum": [ + "UNKNOWN", + "RAW", + "JWT", + "CWT", + "BASE64", + "URI", + "PEM", + "DER" + ], + "default": "UNKNOWN" + }, + "pbCredentialRoleID": { + "type": "object", + "properties": { + "authority": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "pbCredentialStatus": { + "type": "object", + "properties": { + "status": { + "$ref": "#/definitions/pbProvisionStatus", + "title": "@gotags: bson:\"status,omitempty\"" + }, + "identityCertificatePem": { + "type": "string", + "description": "Last identity certificate issued for the device.\n\n@gotags: bson:\"identityCertificate,omitempty\"" + }, + "preSharedKey": { + "$ref": "#/definitions/pbPreSharedKey", + "description": "Last pre shared key issued for the device.\n\n@gotags: bson:\"preSharedKey,omitempty\"" + }, + "credentials": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/pbCredential" + }, + "title": "@gotags: bson:\"credentials,omitempty\"" + } + } + }, + "pbDeleteEnrollmentGroupsResponse": { + "type": "object", + "properties": { + "count": { + "type": "string", + "format": "int64", + "description": "Number of deleted records." + } + } + }, + "pbDeleteHubsResponse": { + "type": "object", + "properties": { + "count": { + "type": "string", + "format": "int64", + "description": "Number of deleted records." + } + } + }, + "pbDeleteProvisioningRecordsResponse": { + "type": "object", + "properties": { + "count": { + "type": "string", + "format": "int64", + "description": "Number of deleted records." + } + } + }, + "pbEnrollmentGroup": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Enrollment group ID.\n\n@gotags: bson:\"_id\"" + }, + "owner": { + "type": "string", + "description": "HUB owner of device - used for hub authorization.\n\n@gotags: bson:\"owner\"" + }, + "attestationMechanism": { + "$ref": "#/definitions/pbAttestationMechanism", + "description": "@gotags: bson:\"attestationMechanism\"", + "title": "Attestation mechanism" + }, + "hubIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Hub configuration to configure device.\n\n@gotags: bson:\"hubIds\"" + }, + "preSharedKey": { + "type": "string", + "description": "@gotags: bson:\"preSharedKey\"", + "title": "Pre shared key for devices in enrollment group. It can be used for maintenance operations by d2d client. Supported formats: \u003c/path/to/psk\u003e,\u003cdata:;base64,{PSK in BASE64}\u003e" + }, + "name": { + "type": "string", + "description": "@gotags: bson:\"name\"", + "title": "name of enrollment group" + } + } + }, + "pbGrpcClientConfig": { + "type": "object", + "properties": { + "grpc": { + "$ref": "#/definitions/pbGrpcConnectionConfig", + "description": "@gotags: bson:\"grpc\"", + "title": "GRPC protocol" + } + } + }, + "pbGrpcConnectionConfig": { + "type": "object", + "properties": { + "address": { + "type": "string", + "description": "@gotags: bson:\"address\"", + "title": "Address in format {host:port}" + }, + "keepAlive": { + "$ref": "#/definitions/pbGrpcKeepAliveConfig", + "title": "@gotags: bson:\"keepAlive\"" + }, + "tls": { + "$ref": "#/definitions/pbTlsConfig", + "title": "@gotags: bson:\"tls\"" + } + } + }, + "pbGrpcKeepAliveConfig": { + "type": "object", + "properties": { + "time": { + "type": "string", + "format": "int64", + "description": "After a duration in nanoseconds of this time if the client doesn't see any activity it\npings the server to see if the transport is still alive.\nThe zero value is infinity and if it set below 10s, a minimum value of 10s will be used instead.\n\n@gotags: bson:\"time\"" + }, + "timeout": { + "type": "string", + "format": "int64", + "description": "After having pinged for keepalive check, the client waits for a duration\nof Timeout and if no activity is seen even after that the connection is\nclosed.\n\n@gotags: bson:\"timeout\"" + }, + "permitWithoutStream": { + "type": "boolean", + "description": "If true, client sends keepalive pings even with no active RPCs. If false,\nwhen there are no active RPCs, Time and Timeout will be ignored and no\nkeepalive pings will be sent.\n\n@gotags: bson:\"permitWithoutStream" + } + } + }, + "pbHttpConfig": { + "type": "object", + "properties": { + "maxIdleConns": { + "type": "integer", + "format": "int64", + "description": "MaxIdleConns controls the maximum number of idle (keep-alive)\nconnections across all hosts. Zero means no limit.\n\n@gotags: bson:\"maxIdleConns\"" + }, + "maxConnsPerHost": { + "type": "integer", + "format": "int64", + "description": "MaxConnsPerHost optionally limits the total number of\nconnections per host, including connections in the dialing,\nactive, and idle states. On limit violation, dials will block.\n\nZero means no limit.\n\n@gotags: bson:\"maxConnsPerHost\"" + }, + "maxIdleConnsPerHost": { + "type": "integer", + "format": "int64", + "description": "MaxIdleConnsPerHost, if non-zero, controls the maximum idle\n(keep-alive) connections to keep per-host. If zero,\nDefaultMaxIdleConnsPerHost is used.\n\n@gotags: bson:\"maxIdleConnsPerHost\"" + }, + "idleConnTimeout": { + "type": "string", + "format": "int64", + "description": "IdleConnTimeout is the maximum amount of time an idle\n(keep-alive) connection will remain idle before closing\nitself in nanoseconds.\nZero means no limit.\n\n@gotags: bson:\"idleConnTimeout\"" + }, + "timeout": { + "type": "string", + "format": "int64", + "description": "Timeout specifies a time limit for requests made by this\nClient in nanoseconds. The timeout includes connection time, any\nredirects, and reading the response body. The timer remains\nrunning after Get, Head, Post, or Do return and will\ninterrupt reading of the Response.Body.\n\nA Timeout of zero means no timeout.\n\nThe Client cancels requests to the underlying Transport\nas if the Request's Context ended.\n\nFor compatibility, the Client will also use the deprecated\nCancelRequest method on Transport if found. New\nRoundTripper implementations should use the Request's Context\nfor cancellation instead of implementing CancelRequest.\n\n@gotags: bson:\"timeout\"" + }, + "tls": { + "$ref": "#/definitions/pbTlsConfig", + "title": "@gotags: bson:\"tls\"" + } + } + }, + "pbHub": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Record ID.\n\n@gotags: bson:\"_id\"" + }, + "gateways": { + "type": "array", + "items": { + "type": "string" + }, + "description": "@gotags: bson:\"gateways\"", + "title": "Address of gateway in format scheme://host:port" + }, + "certificateAuthority": { + "$ref": "#/definitions/pbGrpcClientConfig", + "description": "Signs identity ceritificate for the device.\n\n@gotags: bson:\"certificateAuthority\"" + }, + "authorization": { + "$ref": "#/definitions/pbAuthorizationConfig", + "description": "Acquire HUB authorization code for the device.\n\n@gotags: bson:\"authorization\"" + }, + "name": { + "type": "string", + "description": "Hub name.\n\n@gotags: bson:\"name\"" + }, + "hubId": { + "type": "string", + "description": "Hub identifier - it must match with common name of gateway(coap-gateway) hub certificate.\n\n@gotags: bson:\"hubId\"" + }, + "owner": { + "type": "string", + "description": "@gotags: bson:\"owner\"", + "title": "Owner of the hub" + } + } + }, + "pbOwnershipStatus": { + "type": "object", + "properties": { + "status": { + "$ref": "#/definitions/pbProvisionStatus", + "title": "@gotags: bson:\"status,omitempty\"" + }, + "owner": { + "type": "string", + "description": "Last provisioned owner to the device.\n\n@gotags: bson:\"owner,omitempty\"" + } + } + }, + "pbPreSharedKey": { + "type": "object", + "properties": { + "subjectId": { + "type": "string", + "description": "ID used to identify the owner by the device.\n\n@gotags: bson:\"subjectId,omitempty\"" + }, + "key": { + "type": "string", + "description": "Associated secret to the owner ID.\n\n@gotags: bson:\"key,omitempty\"" + } + } + }, + "pbProvisionStatus": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "int64", + "description": "Last time the device requested provisioning, in unix nanoseconds timestamp format.\n\n@gotags: bson:\"date,omitempty\"" + }, + "coapCode": { + "type": "integer", + "format": "int32", + "description": "The CoAP code returned to the device.\n\n@gotags: bson:\"coapCode,omitempty\"" + }, + "errorMessage": { + "type": "string", + "description": "Error message if any.\n\n@gotags: bson:\"errorMessage,omitempty\"" + } + } + }, + "pbProvisioningRecord": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Registration id, calculated from the manufacturer certificate public key info.\n\n@gotags: bson:\"_id,omitempty\"" + }, + "deviceId": { + "type": "string", + "description": "ID of the device to which this record belongs to.\n\n@gotags: bson:\"deviceId,omitempty\"" + }, + "enrollmentGroupId": { + "type": "string", + "description": "Assigned enrollment group.\n\n@gotags: bson:\"enrollmentGroupId,omitempty\"" + }, + "creationDate": { + "type": "string", + "format": "int64", + "description": "Record creation date, in unix nanoseconds timestamp format.\n\n@gotags: bson:\"creationDate,omitempty\"" + }, + "attestation": { + "$ref": "#/definitions/pbAttestation", + "description": "Last device attestation overview.\n\n@gotags: bson:\"attestation,omitempty\"" + }, + "credential": { + "$ref": "#/definitions/pbCredentialStatus", + "description": "Last credential provision overview.\n\n@gotags: bson:\"credential,omitempty\"" + }, + "acl": { + "$ref": "#/definitions/pbACLStatus", + "description": "Last ACL provision overview.\n\n@gotags: bson:\"acl,omitempty\"" + }, + "cloud": { + "$ref": "#/definitions/pbCloudStatus", + "description": "Last cloud provision overview.\n\n@gotags: bson:\"cloud,omitempty\"" + }, + "ownership": { + "$ref": "#/definitions/pbOwnershipStatus", + "description": "Last ownership provision overview.\n\n@gotags: bson:\"ownership,omitempty\"" + }, + "plgdTime": { + "$ref": "#/definitions/pbProvisionStatus", + "description": "Last plgd-time provision overview.\n\n@gotags: bson:\"plgdTime,omitempty\"" + }, + "localEndpoints": { + "type": "array", + "items": { + "type": "string" + }, + "description": "@gotags: bson:\"localEndpoints,omitempty\"", + "title": "Last local endpoints" + }, + "owner": { + "type": "string", + "description": "Owner ID.\n\n@gotags: bson:\"owner,omitempty\"" + } + } + }, + "pbTlsConfig": { + "type": "object", + "properties": { + "caPool": { + "type": "array", + "items": { + "type": "string" + }, + "description": "@gotags: bson:\"ca_pool\"", + "title": "the root certificates. Supported formats: \u003c/path/to/cert.pem\u003e,\u003cdata:;base64,{PEM in BASE64}\u003e" + }, + "key": { + "type": "string", + "description": "@gotags: bson:\"key\"", + "title": "private key. Supported formats: \u003c/path/to/cert.pem\u003e,\u003cdata:;base64,{PEM in BASE64}\u003e" + }, + "cert": { + "type": "string", + "description": "@gotags: bson:\"cert\"", + "title": "certificate. Supported formats: \u003c/path/to/cert.pem\u003e,\u003cdata:;base64,{PEM in BASE64}\u003e" + }, + "useSystemCaPool": { + "type": "boolean", + "description": "@gotags: bson:\"useSystemCaPool\"", + "title": "use system certification pool" + } + } + }, + "pbUpdateEnrollmentGroup": { + "type": "object", + "properties": { + "attestationMechanism": { + "$ref": "#/definitions/pbAttestationMechanism", + "title": "Attestation mechanism" + }, + "hubIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Hub configuration to configure device." + }, + "preSharedKey": { + "type": "string", + "title": "Pre shared key for devices in enrollment group. It can be used for maintenance operations by d2d client. Supported formats: \u003c/path/to/psk\u003e,\u003cdata:;base64,{PSK in BASE64}\u003e" + }, + "name": { + "type": "string", + "description": "@gotags: bson:\"name\"", + "title": "name of enrollment group" + } + } + }, + "pbUpdateHub": { + "type": "object", + "properties": { + "gateways": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Address of coap-gateway in format scheme://host:port" + }, + "certificateAuthority": { + "$ref": "#/definitions/pbGrpcClientConfig", + "description": "Signs identity ceritificate for the device." + }, + "authorization": { + "$ref": "#/definitions/pbAuthorizationConfig", + "description": "Acquire HUB authorization code for the device." + }, + "name": { + "type": "string", + "description": "Hub name." + }, + "hubId": { + "type": "string", + "title": "Hub ID" + } + } + }, + "pbX509Attestation": { + "type": "object", + "properties": { + "certificatePem": { + "type": "string", + "description": "Last used x509 manufacturer certificate.\n\n@gotags: bson:\"certificate,omitempty\"" + }, + "commonName": { + "type": "string", + "title": "@gotags: bson:\"commonName,omitempty\"" + } + } + }, + "pbX509Configuration": { + "type": "object", + "properties": { + "certificateChain": { + "type": "string", + "description": "@gotags: bson:\"certificateChain\"", + "title": "chain certficates authorities: ..\u003c-intermediateCA1\u003c-intermediateCA\u003c-RootCA which is used to match enrollment group. Supported formats: \u003c/path/to/cert.pem\u003e,\u003cdata:;base64,{PEM in BASE64}\u003e" + }, + "leadCertificateName": { + "type": "string", + "description": "@gotags: bson:\"leadCertificateName\"", + "title": "the certificate name must be one from certificate_chain, it is used to match enrollment group. If empty, the first certificate from certificate_chain is used" + }, + "expiredCertificateEnabled": { + "type": "boolean", + "description": "@gotags: bson:\"expiredCertificateEnabled\"", + "title": "dont validate time during certificate verification" + } + } + }, + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/protobufAny" + } + } + } + } + } +} \ No newline at end of file diff --git a/device-provisioning-service/pb/service_grpc.pb.go b/device-provisioning-service/pb/service_grpc.pb.go new file mode 100644 index 000000000..744feb0df --- /dev/null +++ b/device-provisioning-service/pb/service_grpc.pb.go @@ -0,0 +1,476 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.27.3 +// source: device-provisioning-service/pb/service.proto + +package pb + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + DeviceProvisionService_GetProvisioningRecords_FullMethodName = "/deviceprovisioningservice.pb.DeviceProvisionService/GetProvisioningRecords" + DeviceProvisionService_DeleteProvisioningRecords_FullMethodName = "/deviceprovisioningservice.pb.DeviceProvisionService/DeleteProvisioningRecords" + DeviceProvisionService_GetEnrollmentGroups_FullMethodName = "/deviceprovisioningservice.pb.DeviceProvisionService/GetEnrollmentGroups" + DeviceProvisionService_CreateEnrollmentGroup_FullMethodName = "/deviceprovisioningservice.pb.DeviceProvisionService/CreateEnrollmentGroup" + DeviceProvisionService_UpdateEnrollmentGroup_FullMethodName = "/deviceprovisioningservice.pb.DeviceProvisionService/UpdateEnrollmentGroup" + DeviceProvisionService_DeleteEnrollmentGroups_FullMethodName = "/deviceprovisioningservice.pb.DeviceProvisionService/DeleteEnrollmentGroups" + DeviceProvisionService_GetHubs_FullMethodName = "/deviceprovisioningservice.pb.DeviceProvisionService/GetHubs" + DeviceProvisionService_CreateHub_FullMethodName = "/deviceprovisioningservice.pb.DeviceProvisionService/CreateHub" + DeviceProvisionService_UpdateHub_FullMethodName = "/deviceprovisioningservice.pb.DeviceProvisionService/UpdateHub" + DeviceProvisionService_DeleteHubs_FullMethodName = "/deviceprovisioningservice.pb.DeviceProvisionService/DeleteHubs" +) + +// DeviceProvisionServiceClient is the client API for DeviceProvisionService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type DeviceProvisionServiceClient interface { + // Get registrations of devices + GetProvisioningRecords(ctx context.Context, in *GetProvisioningRecordsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ProvisioningRecord], error) + DeleteProvisioningRecords(ctx context.Context, in *DeleteProvisioningRecordsRequest, opts ...grpc.CallOption) (*DeleteProvisioningRecordsResponse, error) + GetEnrollmentGroups(ctx context.Context, in *GetEnrollmentGroupsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[EnrollmentGroup], error) + CreateEnrollmentGroup(ctx context.Context, in *CreateEnrollmentGroupRequest, opts ...grpc.CallOption) (*EnrollmentGroup, error) + UpdateEnrollmentGroup(ctx context.Context, in *UpdateEnrollmentGroupRequest, opts ...grpc.CallOption) (*EnrollmentGroup, error) + DeleteEnrollmentGroups(ctx context.Context, in *DeleteEnrollmentGroupsRequest, opts ...grpc.CallOption) (*DeleteEnrollmentGroupsResponse, error) + GetHubs(ctx context.Context, in *GetHubsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Hub], error) + CreateHub(ctx context.Context, in *CreateHubRequest, opts ...grpc.CallOption) (*Hub, error) + UpdateHub(ctx context.Context, in *UpdateHubRequest, opts ...grpc.CallOption) (*Hub, error) + DeleteHubs(ctx context.Context, in *DeleteHubsRequest, opts ...grpc.CallOption) (*DeleteHubsResponse, error) +} + +type deviceProvisionServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewDeviceProvisionServiceClient(cc grpc.ClientConnInterface) DeviceProvisionServiceClient { + return &deviceProvisionServiceClient{cc} +} + +func (c *deviceProvisionServiceClient) GetProvisioningRecords(ctx context.Context, in *GetProvisioningRecordsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ProvisioningRecord], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &DeviceProvisionService_ServiceDesc.Streams[0], DeviceProvisionService_GetProvisioningRecords_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[GetProvisioningRecordsRequest, ProvisioningRecord]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type DeviceProvisionService_GetProvisioningRecordsClient = grpc.ServerStreamingClient[ProvisioningRecord] + +func (c *deviceProvisionServiceClient) DeleteProvisioningRecords(ctx context.Context, in *DeleteProvisioningRecordsRequest, opts ...grpc.CallOption) (*DeleteProvisioningRecordsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteProvisioningRecordsResponse) + err := c.cc.Invoke(ctx, DeviceProvisionService_DeleteProvisioningRecords_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *deviceProvisionServiceClient) GetEnrollmentGroups(ctx context.Context, in *GetEnrollmentGroupsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[EnrollmentGroup], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &DeviceProvisionService_ServiceDesc.Streams[1], DeviceProvisionService_GetEnrollmentGroups_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[GetEnrollmentGroupsRequest, EnrollmentGroup]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type DeviceProvisionService_GetEnrollmentGroupsClient = grpc.ServerStreamingClient[EnrollmentGroup] + +func (c *deviceProvisionServiceClient) CreateEnrollmentGroup(ctx context.Context, in *CreateEnrollmentGroupRequest, opts ...grpc.CallOption) (*EnrollmentGroup, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EnrollmentGroup) + err := c.cc.Invoke(ctx, DeviceProvisionService_CreateEnrollmentGroup_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *deviceProvisionServiceClient) UpdateEnrollmentGroup(ctx context.Context, in *UpdateEnrollmentGroupRequest, opts ...grpc.CallOption) (*EnrollmentGroup, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EnrollmentGroup) + err := c.cc.Invoke(ctx, DeviceProvisionService_UpdateEnrollmentGroup_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *deviceProvisionServiceClient) DeleteEnrollmentGroups(ctx context.Context, in *DeleteEnrollmentGroupsRequest, opts ...grpc.CallOption) (*DeleteEnrollmentGroupsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteEnrollmentGroupsResponse) + err := c.cc.Invoke(ctx, DeviceProvisionService_DeleteEnrollmentGroups_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *deviceProvisionServiceClient) GetHubs(ctx context.Context, in *GetHubsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Hub], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &DeviceProvisionService_ServiceDesc.Streams[2], DeviceProvisionService_GetHubs_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[GetHubsRequest, Hub]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type DeviceProvisionService_GetHubsClient = grpc.ServerStreamingClient[Hub] + +func (c *deviceProvisionServiceClient) CreateHub(ctx context.Context, in *CreateHubRequest, opts ...grpc.CallOption) (*Hub, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Hub) + err := c.cc.Invoke(ctx, DeviceProvisionService_CreateHub_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *deviceProvisionServiceClient) UpdateHub(ctx context.Context, in *UpdateHubRequest, opts ...grpc.CallOption) (*Hub, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Hub) + err := c.cc.Invoke(ctx, DeviceProvisionService_UpdateHub_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *deviceProvisionServiceClient) DeleteHubs(ctx context.Context, in *DeleteHubsRequest, opts ...grpc.CallOption) (*DeleteHubsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteHubsResponse) + err := c.cc.Invoke(ctx, DeviceProvisionService_DeleteHubs_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DeviceProvisionServiceServer is the server API for DeviceProvisionService service. +// All implementations must embed UnimplementedDeviceProvisionServiceServer +// for forward compatibility. +type DeviceProvisionServiceServer interface { + // Get registrations of devices + GetProvisioningRecords(*GetProvisioningRecordsRequest, grpc.ServerStreamingServer[ProvisioningRecord]) error + DeleteProvisioningRecords(context.Context, *DeleteProvisioningRecordsRequest) (*DeleteProvisioningRecordsResponse, error) + GetEnrollmentGroups(*GetEnrollmentGroupsRequest, grpc.ServerStreamingServer[EnrollmentGroup]) error + CreateEnrollmentGroup(context.Context, *CreateEnrollmentGroupRequest) (*EnrollmentGroup, error) + UpdateEnrollmentGroup(context.Context, *UpdateEnrollmentGroupRequest) (*EnrollmentGroup, error) + DeleteEnrollmentGroups(context.Context, *DeleteEnrollmentGroupsRequest) (*DeleteEnrollmentGroupsResponse, error) + GetHubs(*GetHubsRequest, grpc.ServerStreamingServer[Hub]) error + CreateHub(context.Context, *CreateHubRequest) (*Hub, error) + UpdateHub(context.Context, *UpdateHubRequest) (*Hub, error) + DeleteHubs(context.Context, *DeleteHubsRequest) (*DeleteHubsResponse, error) + mustEmbedUnimplementedDeviceProvisionServiceServer() +} + +// UnimplementedDeviceProvisionServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedDeviceProvisionServiceServer struct{} + +func (UnimplementedDeviceProvisionServiceServer) GetProvisioningRecords(*GetProvisioningRecordsRequest, grpc.ServerStreamingServer[ProvisioningRecord]) error { + return status.Errorf(codes.Unimplemented, "method GetProvisioningRecords not implemented") +} +func (UnimplementedDeviceProvisionServiceServer) DeleteProvisioningRecords(context.Context, *DeleteProvisioningRecordsRequest) (*DeleteProvisioningRecordsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteProvisioningRecords not implemented") +} +func (UnimplementedDeviceProvisionServiceServer) GetEnrollmentGroups(*GetEnrollmentGroupsRequest, grpc.ServerStreamingServer[EnrollmentGroup]) error { + return status.Errorf(codes.Unimplemented, "method GetEnrollmentGroups not implemented") +} +func (UnimplementedDeviceProvisionServiceServer) CreateEnrollmentGroup(context.Context, *CreateEnrollmentGroupRequest) (*EnrollmentGroup, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateEnrollmentGroup not implemented") +} +func (UnimplementedDeviceProvisionServiceServer) UpdateEnrollmentGroup(context.Context, *UpdateEnrollmentGroupRequest) (*EnrollmentGroup, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateEnrollmentGroup not implemented") +} +func (UnimplementedDeviceProvisionServiceServer) DeleteEnrollmentGroups(context.Context, *DeleteEnrollmentGroupsRequest) (*DeleteEnrollmentGroupsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteEnrollmentGroups not implemented") +} +func (UnimplementedDeviceProvisionServiceServer) GetHubs(*GetHubsRequest, grpc.ServerStreamingServer[Hub]) error { + return status.Errorf(codes.Unimplemented, "method GetHubs not implemented") +} +func (UnimplementedDeviceProvisionServiceServer) CreateHub(context.Context, *CreateHubRequest) (*Hub, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateHub not implemented") +} +func (UnimplementedDeviceProvisionServiceServer) UpdateHub(context.Context, *UpdateHubRequest) (*Hub, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateHub not implemented") +} +func (UnimplementedDeviceProvisionServiceServer) DeleteHubs(context.Context, *DeleteHubsRequest) (*DeleteHubsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteHubs not implemented") +} +func (UnimplementedDeviceProvisionServiceServer) mustEmbedUnimplementedDeviceProvisionServiceServer() { +} +func (UnimplementedDeviceProvisionServiceServer) testEmbeddedByValue() {} + +// UnsafeDeviceProvisionServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to DeviceProvisionServiceServer will +// result in compilation errors. +type UnsafeDeviceProvisionServiceServer interface { + mustEmbedUnimplementedDeviceProvisionServiceServer() +} + +func RegisterDeviceProvisionServiceServer(s grpc.ServiceRegistrar, srv DeviceProvisionServiceServer) { + // If the following call pancis, it indicates UnimplementedDeviceProvisionServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&DeviceProvisionService_ServiceDesc, srv) +} + +func _DeviceProvisionService_GetProvisioningRecords_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(GetProvisioningRecordsRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(DeviceProvisionServiceServer).GetProvisioningRecords(m, &grpc.GenericServerStream[GetProvisioningRecordsRequest, ProvisioningRecord]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type DeviceProvisionService_GetProvisioningRecordsServer = grpc.ServerStreamingServer[ProvisioningRecord] + +func _DeviceProvisionService_DeleteProvisioningRecords_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteProvisioningRecordsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DeviceProvisionServiceServer).DeleteProvisioningRecords(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DeviceProvisionService_DeleteProvisioningRecords_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DeviceProvisionServiceServer).DeleteProvisioningRecords(ctx, req.(*DeleteProvisioningRecordsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DeviceProvisionService_GetEnrollmentGroups_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(GetEnrollmentGroupsRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(DeviceProvisionServiceServer).GetEnrollmentGroups(m, &grpc.GenericServerStream[GetEnrollmentGroupsRequest, EnrollmentGroup]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type DeviceProvisionService_GetEnrollmentGroupsServer = grpc.ServerStreamingServer[EnrollmentGroup] + +func _DeviceProvisionService_CreateEnrollmentGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateEnrollmentGroupRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DeviceProvisionServiceServer).CreateEnrollmentGroup(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DeviceProvisionService_CreateEnrollmentGroup_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DeviceProvisionServiceServer).CreateEnrollmentGroup(ctx, req.(*CreateEnrollmentGroupRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DeviceProvisionService_UpdateEnrollmentGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateEnrollmentGroupRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DeviceProvisionServiceServer).UpdateEnrollmentGroup(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DeviceProvisionService_UpdateEnrollmentGroup_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DeviceProvisionServiceServer).UpdateEnrollmentGroup(ctx, req.(*UpdateEnrollmentGroupRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DeviceProvisionService_DeleteEnrollmentGroups_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteEnrollmentGroupsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DeviceProvisionServiceServer).DeleteEnrollmentGroups(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DeviceProvisionService_DeleteEnrollmentGroups_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DeviceProvisionServiceServer).DeleteEnrollmentGroups(ctx, req.(*DeleteEnrollmentGroupsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DeviceProvisionService_GetHubs_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(GetHubsRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(DeviceProvisionServiceServer).GetHubs(m, &grpc.GenericServerStream[GetHubsRequest, Hub]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type DeviceProvisionService_GetHubsServer = grpc.ServerStreamingServer[Hub] + +func _DeviceProvisionService_CreateHub_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateHubRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DeviceProvisionServiceServer).CreateHub(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DeviceProvisionService_CreateHub_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DeviceProvisionServiceServer).CreateHub(ctx, req.(*CreateHubRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DeviceProvisionService_UpdateHub_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateHubRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DeviceProvisionServiceServer).UpdateHub(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DeviceProvisionService_UpdateHub_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DeviceProvisionServiceServer).UpdateHub(ctx, req.(*UpdateHubRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DeviceProvisionService_DeleteHubs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteHubsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DeviceProvisionServiceServer).DeleteHubs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DeviceProvisionService_DeleteHubs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DeviceProvisionServiceServer).DeleteHubs(ctx, req.(*DeleteHubsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// DeviceProvisionService_ServiceDesc is the grpc.ServiceDesc for DeviceProvisionService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var DeviceProvisionService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "deviceprovisioningservice.pb.DeviceProvisionService", + HandlerType: (*DeviceProvisionServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "DeleteProvisioningRecords", + Handler: _DeviceProvisionService_DeleteProvisioningRecords_Handler, + }, + { + MethodName: "CreateEnrollmentGroup", + Handler: _DeviceProvisionService_CreateEnrollmentGroup_Handler, + }, + { + MethodName: "UpdateEnrollmentGroup", + Handler: _DeviceProvisionService_UpdateEnrollmentGroup_Handler, + }, + { + MethodName: "DeleteEnrollmentGroups", + Handler: _DeviceProvisionService_DeleteEnrollmentGroups_Handler, + }, + { + MethodName: "CreateHub", + Handler: _DeviceProvisionService_CreateHub_Handler, + }, + { + MethodName: "UpdateHub", + Handler: _DeviceProvisionService_UpdateHub_Handler, + }, + { + MethodName: "DeleteHubs", + Handler: _DeviceProvisionService_DeleteHubs_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "GetProvisioningRecords", + Handler: _DeviceProvisionService_GetProvisioningRecords_Handler, + ServerStreams: true, + }, + { + StreamName: "GetEnrollmentGroups", + Handler: _DeviceProvisionService_GetEnrollmentGroups_Handler, + ServerStreams: true, + }, + { + StreamName: "GetHubs", + Handler: _DeviceProvisionService_GetHubs_Handler, + ServerStreams: true, + }, + }, + Metadata: "device-provisioning-service/pb/service.proto", +} diff --git a/device-provisioning-service/security/oauth/clientcredentials/cache.go b/device-provisioning-service/security/oauth/clientcredentials/cache.go new file mode 100644 index 000000000..76b4ebce4 --- /dev/null +++ b/device-provisioning-service/security/oauth/clientcredentials/cache.go @@ -0,0 +1,183 @@ +package clientcredentials + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + "github.com/plgd-dev/hub/v2/pkg/net/http/client" + "github.com/plgd-dev/hub/v2/pkg/security/jwt" + "github.com/plgd-dev/hub/v2/pkg/security/openid" + "github.com/plgd-dev/hub/v2/pkg/sync/task/future" + "go.opentelemetry.io/otel/trace" + "golang.org/x/oauth2" +) + +type Cache struct { + ctx context.Context + tokens map[string]*future.Future + mutex sync.Mutex + httpClient *client.Client + config Config + cancel context.CancelFunc + wg *sync.WaitGroup +} + +func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider, cleanUpInterval time.Duration) (*Cache, error) { + err := config.Validate() + if err != nil { + return nil, fmt.Errorf("invalid OAuth client credential config: %w", err) + } + httpClient, err := client.New(config.HTTP, fileWatcher, logger, tracerProvider) + if err != nil { + return nil, err + } + oidcfg, err := openid.GetConfiguration(ctx, httpClient.HTTP(), config.Authority) + if err != nil { + return nil, err + } + config.TokenURL = oidcfg.TokenURL + + ctx, cancel := context.WithCancel(ctx) + c := &Cache{ + config: config, + httpClient: httpClient, + tokens: make(map[string]*future.Future), + cancel: cancel, + wg: new(sync.WaitGroup), + } + c.wg.Add(1) + go func() { + defer c.wg.Done() + t := time.NewTicker(cleanUpInterval) + defer t.Stop() + for { + select { + case <-ctx.Done(): + c.httpClient.Close() + return + case now := <-t.C: + c.cleanUpExpiredTokens(now) + } + } + }() + + return c, nil +} + +func (c *Cache) cleanUpExpiredTokens(now time.Time) { + c.mutex.Lock() + defer c.mutex.Unlock() + for owner, f := range c.tokens { + if !f.Ready() { + continue + } + v, err := f.Get(c.ctx) + if err != nil { + delete(c.tokens, owner) + continue + } + t, ok := v.(*oauth2.Token) + if !ok || (!t.Expiry.IsZero() && now.After(t.Expiry)) { + delete(c.tokens, owner) + } + } +} + +type ClaimNotFoundError struct { + Claim string + Expected interface{} + Got interface{} +} + +func NewClaimNotFoundError(claim string, expected, got interface{}) ClaimNotFoundError { + return ClaimNotFoundError{ + Claim: claim, + Expected: expected, + Got: got, + } +} + +func (e ClaimNotFoundError) Error() string { + return fmt.Sprintf("invalid claim %v: expected %v, got %v", e.Claim, e.Expected, e.Got) +} + +func checkRequiredClaims(accessToken string, requiredClaims map[string]interface{}) error { + claims, err := jwt.ParseToken(accessToken) + if err != nil { + return fmt.Errorf("cannot parse access token: %w", err) + } + for k, v := range requiredClaims { + if claims[k] != v { + return NewClaimNotFoundError(k, v, claims[k]) + } + } + return nil +} + +func (c *Cache) GetTokenFromOAuth(ctx context.Context, urlValues map[string]string, requiredClaims map[string]interface{}) (*oauth2.Token, error) { + ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient.HTTP()) + csCfg := c.config.ToDefaultClientCredentials() + for key, val := range urlValues { + csCfg.EndpointParams.Add(key, val) + } + token, err := csCfg.Token(ctx) + if err == nil { + err = checkRequiredClaims(token.AccessToken, requiredClaims) + } + return token, err +} + +func (c *Cache) getFutureToken(key string, old *future.Future) (*future.Future, future.SetFunc) { + c.mutex.Lock() + defer c.mutex.Unlock() + f, ok := c.tokens[key] + if !ok || f == old { + fu, set := future.New() + c.tokens[key] = fu + return fu, set + } + return f, nil +} + +func (c *Cache) GetToken(ctx context.Context, key string, urlValues map[string]string, requiredClaims map[string]interface{}) (*oauth2.Token, error) { + deadline, ok := ctx.Deadline() + if !ok { + return nil, errors.New("deadline is not set in ctx") + } + var oldF *future.Future + var setF future.SetFunc + for { + f, set := c.getFutureToken(key, oldF) + if set != nil { + setF = set + break + } + v, err := f.Get(ctx) + if err == nil { + t, ok := v.(*oauth2.Token) + if !ok { + return nil, fmt.Errorf("invalid object type(%T) in a future", v) + } + if checkRequiredClaims(t.AccessToken, requiredClaims) == nil && (deadline.Before(t.Expiry) || t.Expiry.IsZero()) { + return t, nil + } + } + oldF = f + } + token, err := c.GetTokenFromOAuth(ctx, urlValues, requiredClaims) + setF(token, err) + if err != nil { + return nil, err + } + return token, nil +} + +func (c *Cache) Close() { + c.cancel() + c.wg.Wait() +} diff --git a/device-provisioning-service/security/oauth/clientcredentials/cache_test.go b/device-provisioning-service/security/oauth/clientcredentials/cache_test.go new file mode 100644 index 000000000..e4e8ce9ac --- /dev/null +++ b/device-provisioning-service/security/oauth/clientcredentials/cache_test.go @@ -0,0 +1,82 @@ +package clientcredentials_test + +import ( + "context" + "testing" + "time" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/security/oauth/clientcredentials" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" +) + +func TestNew(t *testing.T) { + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesOAuth) + defer hubShutdown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*8) + defer cancel() + + logger := log.NewLogger(log.Config{}) + + fileWatcher, err := fsnotify.NewWatcher(logger) + require.NoError(t, err) + + defer func() { + err = fileWatcher.Close() + require.NoError(t, err) + }() + + cfg := test.MakeAuthorizationConfig() + p, err := cfg.ToProto() + require.NoError(t, err) + require.NoError(t, p.Validate()) + got, err := clientcredentials.New(ctx, cfg.Provider.Config, fileWatcher, logger, noop.NewTracerProvider(), time.Millisecond*10) + require.NoError(t, err) + defer got.Close() + + tokenA, err := got.GetToken(ctx, "A", map[string]string{ + "key": "value", + }, nil) + require.NoError(t, err) + tokenB, err := got.GetToken(ctx, "B", map[string]string{ + "key": "value", + }, nil) + require.NoError(t, err) + require.NotEqual(t, tokenA, tokenB) + dupTokenA, err := got.GetToken(ctx, "A", map[string]string{ + "key": "value", + }, nil) + require.NoError(t, err) + require.Equal(t, tokenA, dupTokenA) + time.Sleep(time.Second) + tokenAwithoutCache, err := got.GetTokenFromOAuth(ctx, map[string]string{ + "key": "value", + }, map[string]interface{}{ + "sub": "1", + }) + require.NoError(t, err) + require.NotEqual(t, tokenA, tokenAwithoutCache) + _, err = got.GetToken(ctx, "C", map[string]string{ + "key": "value", + }, map[string]interface{}{ + "sub": "1", + }) + require.NoError(t, err) + _, err = got.GetToken(ctx, "C", map[string]string{ + "key": "value", + }, map[string]interface{}{ + "notExist": 123, + }) + require.Error(t, err) + _, err = got.GetToken(ctx, "C", map[string]string{ + "key": "value", + }, map[string]interface{}{ + "sub": 123, + }) + require.Error(t, err) +} diff --git a/device-provisioning-service/security/oauth/clientcredentials/config.go b/device-provisioning-service/security/oauth/clientcredentials/config.go new file mode 100644 index 000000000..bb414868a --- /dev/null +++ b/device-provisioning-service/security/oauth/clientcredentials/config.go @@ -0,0 +1,60 @@ +package clientcredentials + +import ( + "fmt" + "net/url" + + "github.com/plgd-dev/hub/v2/pkg/config/property/urischeme" + "github.com/plgd-dev/hub/v2/pkg/net/http/client" + "golang.org/x/oauth2/clientcredentials" +) + +type Config struct { + Authority string `yaml:"authority" json:"authority"` + ClientID string `yaml:"clientID" json:"clientId"` + ClientSecretFile urischeme.URIScheme `yaml:"clientSecretFile" json:"clientSecretFile"` + Scopes []string `yaml:"scopes" json:"scopes"` + TokenURL string `yaml:"-" json:"tokenUrl"` + Audience string `yaml:"audience" json:"audience"` + ClientSecret string `yaml:"-" json:"clientSecret"` + HTTP client.Config `yaml:"http" json:"http"` +} + +func (c *Config) Validate() error { + if c.Authority == "" { + return fmt.Errorf("authority('%v')", c.Authority) + } + if c.ClientID == "" { + return fmt.Errorf("clientID('%v')", c.ClientID) + } + if c.ClientSecretFile == "" { + return fmt.Errorf("clientSecretFile('%v')", c.ClientSecretFile) + } + if err := c.HTTP.Validate(); err != nil { + return fmt.Errorf("http.%w", err) + } + clientSecret, err := c.ClientSecretFile.Read() + if err != nil { + return fmt.Errorf("clientSecretFile('%v')-%w", c.ClientSecretFile, err) + } + c.ClientSecret = string(clientSecret) + return nil +} + +func (c Config) ToClientCredentials(tokenURL, clientSecret string) clientcredentials.Config { + v := make(url.Values) + if c.Audience != "" { + v.Set("audience", c.Audience) + } + return clientcredentials.Config{ + ClientID: c.ClientID, + ClientSecret: clientSecret, + Scopes: c.Scopes, + TokenURL: tokenURL, + EndpointParams: v, + } +} + +func (c Config) ToDefaultClientCredentials() clientcredentials.Config { + return c.ToClientCredentials(c.TokenURL, c.ClientSecret) +} diff --git a/device-provisioning-service/service/acls.go b/device-provisioning-service/service/acls.go new file mode 100644 index 000000000..bb9601583 --- /dev/null +++ b/device-provisioning-service/service/acls.go @@ -0,0 +1,177 @@ +package service + +import ( + "context" + "time" + + "github.com/plgd-dev/device/v2/schema/acl" + "github.com/plgd-dev/device/v2/schema/cloud" + "github.com/plgd-dev/device/v2/schema/configuration" + "github.com/plgd-dev/device/v2/schema/device" + "github.com/plgd-dev/device/v2/schema/doxm" + "github.com/plgd-dev/device/v2/schema/maintenance" + "github.com/plgd-dev/device/v2/schema/platform" + "github.com/plgd-dev/device/v2/schema/plgdtime" + "github.com/plgd-dev/device/v2/schema/resources" + "github.com/plgd-dev/device/v2/schema/sdi" + "github.com/plgd-dev/device/v2/schema/softwareupdate" + "github.com/plgd-dev/device/v2/schema/sp" + coapCodes "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/plgd-dev/go-coap/v3/mux" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/identity-store/events" + "github.com/plgd-dev/hub/v2/pkg/strings" +) + +func toCoapCode(msg *pool.Message) int32 { + if msg == nil { + return 0 + } + return int32(msg.Code()) +} + +func toErrorStr(err error) string { + if err != nil { + return err.Error() + } + return "" +} + +func (RequestHandle) ProcessACLs(_ context.Context, req *mux.Message, session *Session, linkedHubs []*LinkedHub, _ *EnrollmentGroup) (*pool.Message, error) { + switch req.Code() { + case coapCodes.GET: + msg, acls, err := provisionACLs(req, session, linkedHubs) + session.updateProvisioningRecord(&store.ProvisioningRecord{ + Acl: &pb.ACLStatus{ + Status: &pb.ProvisionStatus{ + Date: time.Now().UnixNano(), + CoapCode: toCoapCode(msg), + ErrorMessage: toErrorStr(err), + }, + AccessControlList: acls, + }, + }) + return msg, err + default: + return nil, statusErrorf(coapCodes.Forbidden, "unsupported command(%v)", req.Code()) + } +} + +func provisionACLs(req *mux.Message, session *Session, linkedHubs []*LinkedHub) (*pool.Message, []*pb.AccessControl, error) { + var resp acl.UpdateRequest + allowedHubResources := []acl.Resource{ + // allow to update device's name from hub + { + Interfaces: []string{"*"}, + Href: configuration.ResourceURI, + }, + // allow to update device from hub + { + Interfaces: []string{"*"}, + Href: softwareupdate.ResourceURI, + }, + // allow to update maintenance from hub + { + Interfaces: []string{"*"}, + Href: maintenance.ResourceURI, + }, + // allow to update time from hub + { + Interfaces: []string{"*"}, + Href: plgdtime.ResourceURI, + }, + } + // allow to access all resources ordinal resources from hub + allowedHubResources = append(allowedHubResources, acl.AllResources...) + + allowedOwnerResources := []acl.Resource{ + // allow access to cloud configuration resource + { + Interfaces: []string{"*"}, + Href: cloud.ResourceURI, + }, + // allow access security profile resource + { + Interfaces: []string{"*"}, + Href: sp.ResourceURI, + }, + { + Interfaces: []string{"*"}, + Href: plgdtime.ResourceURI, + }, + } + allowedOwnerResources = append(allowedOwnerResources, allowedHubResources...) + + resp.AccessControlList = []acl.AccessControl{ + // owner acls - allow user access via client application + { + Permission: acl.AllPermissions, + Subject: acl.Subject{ + Subject_Device: &acl.Subject_Device{ + DeviceID: events.OwnerToUUID(session.enrollmentGroup.Owner), + }, + }, + Resources: allowedOwnerResources, + Tag: DPSTag, + }, + // custom ACLs + { + Permission: acl.Permission_READ, + Subject: acl.Subject{ + Subject_Connection: &acl.Subject_Connection{ + Type: acl.ConnectionType_ANON_CLEAR, + }, + }, + Resources: []acl.Resource{ + { + Href: device.ResourceURI, + Interfaces: []string{"*"}, + }, + { + Href: platform.ResourceURI, + Interfaces: []string{"*"}, + }, + { + Href: resources.ResourceURI, + Interfaces: []string{"*"}, + }, + { + Href: sdi.ResourceURI, + Interfaces: []string{"*"}, + }, + { + Href: doxm.ResourceURI, + Interfaces: []string{"*"}, + }, + }, + Tag: DPSTag, + }, + } + hubIDs := make([]string, 0, len(linkedHubs)) + for _, linkedHub := range linkedHubs { + hubIDs = append(hubIDs, linkedHub.cfg.GetHubId()) + } + hubIDs = strings.UniqueStable(hubIDs) + for _, id := range hubIDs { + // hub acls + resp.AccessControlList = append(resp.AccessControlList, acl.AccessControl{ + Permission: acl.AllPermissions, + Subject: acl.Subject{ + Subject_Device: &acl.Subject_Device{ + DeviceID: id, + }, + }, + Resources: allowedHubResources, + Tag: DPSTag, + }) + } + + msgType, data, err := encodeResponse(resp, req.Options()) + if err != nil { + return nil, nil, statusErrorf(coapCodes.BadRequest, "cannot encode ACLs response: %w", err) + } + + return session.createResponse(coapCodes.Content, req.Token(), msgType, data), pb.DeviceAccessControlListToPb(resp.AccessControlList), nil +} diff --git a/device-provisioning-service/service/acls_test.go b/device-provisioning-service/service/acls_test.go new file mode 100644 index 000000000..0b1811075 --- /dev/null +++ b/device-provisioning-service/service/acls_test.go @@ -0,0 +1,72 @@ +package service_test + +import ( + "context" + "testing" + "time" + + "github.com/plgd-dev/device/v2/schema/acl" + "github.com/plgd-dev/go-coap/v3/dtls" + "github.com/plgd-dev/go-coap/v3/options" + "github.com/plgd-dev/go-coap/v3/tcp" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + "github.com/plgd-dev/hub/v2/device-provisioning-service/uri" + pkgCoapService "github.com/plgd-dev/hub/v2/pkg/net/coap/service" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" +) + +func TestAclsTCP(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesCertificateAuthority|hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId) + defer hubShutdown() + dpsCfg := test.MakeConfig(t) + shutDown := test.New(t, dpsCfg) + defer shutDown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) + defer cancel() + + c, err := tcp.Dial(dpsCfg.APIs.COAP.Addr, options.WithTLS(setupTLSConfig(t)), options.WithContext(ctx)) + require.NoError(t, err) + defer func() { + errC := c.Close() + require.NoError(t, errC) + }() + + resp, err := c.Get(ctx, uri.ACLs) + require.NoError(t, err) + + var acls acl.UpdateRequest + fromCbor(t, resp.Body(), &acls) + + require.Len(t, acls.AccessControlList, 3) +} + +func TestAclsUDP(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesCertificateAuthority|hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId) + defer hubShutdown() + dpsCfg := test.MakeConfig(t) + shutDown := test.New(t, dpsCfg) + defer shutDown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) + defer cancel() + + tlsCfg := setupTLSConfig(t) + c, err := dtls.Dial(dpsCfg.APIs.COAP.Addr, pkgCoapService.TLSConfigToDTLSConfig(tlsCfg), options.WithContext(ctx)) + require.NoError(t, err) + defer func() { + errC := c.Close() + require.NoError(t, errC) + }() + + resp, err := c.Get(ctx, uri.ACLs) + require.NoError(t, err) + + var acls acl.UpdateRequest + fromCbor(t, resp.Body(), &acls) + + require.Len(t, acls.AccessControlList, 3) +} diff --git a/device-provisioning-service/service/auth.go b/device-provisioning-service/service/auth.go new file mode 100644 index 000000000..7acd129ae --- /dev/null +++ b/device-provisioning-service/service/auth.go @@ -0,0 +1,107 @@ +package service + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "time" + + "github.com/plgd-dev/go-coap/v3/pkg/cache" + pkgX509 "github.com/plgd-dev/hub/v2/pkg/security/x509" +) + +type AuthHandler interface { + // tls.Config overrides + VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error + VerifyConnection(tls.ConnectionState) error + + GetChainsCache() *cache.Cache[uint64, [][]*x509.Certificate] +} + +func verifyChain(certificates, certificatesAuthority []*x509.Certificate, currentTime time.Time) ([][]*x509.Certificate, error) { + chains, err := pkgX509.Verify(certificates, certificatesAuthority, false, x509.VerifyOptions{ + CurrentTime: currentTime, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + }) + if err != nil { + return nil, err + } + certificate := certificates[0] + // verify EKU manually + ekuHasClient := false + for _, eku := range certificate.ExtKeyUsage { + if eku == x509.ExtKeyUsageClientAuth { + ekuHasClient = true + } + } + if !ekuHasClient { + return nil, errors.New("not contains ExtKeyUsageClientAuth") + } + return chains, nil +} + +type DefaultAuthHandler struct { + config Config + chainsCache *cache.Cache[uint64, [][]*x509.Certificate] + enrollmentGroupsCache *EnrollmentGroupsCache +} + +func MakeDefaultAuthHandler(config Config, enrollmentGroupsCache *EnrollmentGroupsCache) DefaultAuthHandler { + chainsCache := cache.NewCache[uint64, [][]*x509.Certificate]() + return DefaultAuthHandler{ + config: config, + chainsCache: chainsCache, + enrollmentGroupsCache: enrollmentGroupsCache, + } +} + +func (d DefaultAuthHandler) GetChainsCache() *cache.Cache[uint64, [][]*x509.Certificate] { + return d.chainsCache +} + +func (d DefaultAuthHandler) VerifyPeerCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error { + certs := make([]*x509.Certificate, 0, len(rawCerts)) + issuerNames := make([]string, 0, len(rawCerts)) + for _, rawCert := range rawCerts { + cert, err := x509.ParseCertificate(rawCert) + if err != nil { + return pkgX509.NewError(nil, err) + } + certs = append(certs, cert) + issuerNames = append(issuerNames, cert.Issuer.CommonName) + } + if len(certs) == 0 { + return pkgX509.NewError(nil, errors.New("empty certificates")) + } + ctx, cancel := context.WithTimeout(context.Background(), d.config.APIs.COAP.InactivityMonitor.Timeout) + defer cancel() + success := false + err := d.enrollmentGroupsCache.GetEnrollmentGroupsByIssuerNames(ctx, issuerNames, func(g *EnrollmentGroup) bool { + currentTime := time.Now() + if g.GetAttestationMechanism().GetX509().GetExpiredCertificateEnabled() { + currentTime = certs[0].NotBefore.Add(certs[0].NotAfter.Sub(certs[0].NotBefore) / 2) + } + chains, err := verifyChain(certs, g.AttestationMechanismX509CertificateChain, currentTime) + if err == nil { + el, loaded := d.chainsCache.LoadOrStore(toCRC64(certs[0].Raw), cache.NewElement(chains, time.Now().Add(d.config.APIs.COAP.InactivityMonitor.Timeout), nil)) + if loaded { + el.ValidUntil.Store(time.Now().Add(d.config.APIs.COAP.InactivityMonitor.Timeout / 2)) + } + success = true + return false + } + return true + }) + if err != nil { + return pkgX509.NewError([][]*x509.Certificate{certs}, err) + } + if success { + return nil + } + return pkgX509.NewError([][]*x509.Certificate{certs}, errors.New("untrusted certificate")) +} + +func (d DefaultAuthHandler) VerifyConnection(tls.ConnectionState) error { + return nil +} diff --git a/device-provisioning-service/service/auth_test.go b/device-provisioning-service/service/auth_test.go new file mode 100644 index 000000000..86db93c78 --- /dev/null +++ b/device-provisioning-service/service/auth_test.go @@ -0,0 +1,65 @@ +package service_test + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "testing" + "time" + + "github.com/plgd-dev/go-coap/v3/options" + "github.com/plgd-dev/go-coap/v3/tcp" + caService "github.com/plgd-dev/hub/v2/certificate-authority/test" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/plgd-dev/kit/v2/security/generateCertificate" + "github.com/stretchr/testify/require" +) + +func TestWithUntrustedCertificate(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId) + defer hubShutdown() + caCfg := caService.MakeConfig(t) + caCfg.Signer.ExpiresIn = time.Hour * 10 + caShutdown := caService.New(t, caCfg) + defer caShutdown() + + dpsCfg := test.MakeConfig(t) + shutDown := test.New(t, dpsCfg) + defer shutDown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*8) + defer cancel() + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + cfg := generateCertificate.Configuration{ + ValidFrom: time.Now().Add(-time.Hour).Format(time.RFC3339), + ValidFor: 2 * time.Hour, + ExtensionKeyUsages: []string{"client", "server"}, + } + cfg.Subject.CommonName = "test" + data, err := generateCertificate.GenerateRootCA(cfg, priv) + require.NoError(t, err) + b, err := x509.MarshalECPrivateKey(priv) + require.NoError(t, err) + key := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}) + crt, err := tls.X509KeyPair(data, key) + require.NoError(t, err) + + // create connection via mfg certificate + c, err := tcp.Dial(dpsCfg.APIs.COAP.Addr, options.WithTLS(&tls.Config{ + Certificates: []tls.Certificate{ + crt, + }, + InsecureSkipVerify: true, + }), options.WithContext(ctx)) + require.NoError(t, err) + err = c.Ping(ctx) + require.Error(t, err) +} diff --git a/device-provisioning-service/service/clients.go b/device-provisioning-service/service/clients.go new file mode 100644 index 000000000..1868aa221 --- /dev/null +++ b/device-provisioning-service/service/clients.go @@ -0,0 +1,59 @@ +package service + +import ( + "context" + "fmt" + + pbCA "github.com/plgd-dev/hub/v2/certificate-authority/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store/mongodb" + "github.com/plgd-dev/hub/v2/pkg/fn" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + "github.com/plgd-dev/hub/v2/pkg/net/grpc/client" + cmClient "github.com/plgd-dev/hub/v2/pkg/security/certManager/client" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc" +) + +func newGrpcClient(config client.Config, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider, service string) (*grpc.ClientConn, func(), error) { + idConn, err := client.New(config, fileWatcher, logger, tracerProvider) + if err != nil { + return nil, nil, fmt.Errorf("cannot create connection to %v: %w", service, err) + } + closeIDConn := func() { + if err := idConn.Close(); err != nil { + logger.Errorf("error occurs during close connection to %v: %w", service, err) + } + } + return idConn.GRPC(), closeIDConn, nil +} + +func newCertificateAuthorityClient(config client.Config, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (pbCA.CertificateAuthorityClient, func(), error) { + client, closeClient, err := newGrpcClient(config, fileWatcher, logger, tracerProvider, "certificate-authority") + if err != nil { + return nil, nil, err + } + return pbCA.NewCertificateAuthorityClient(client), closeClient, err +} + +func NewStore(ctx context.Context, config mongodb.Config, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*mongodb.Store, func(), error) { + var fl fn.FuncList + certManager, err := cmClient.New(config.Mongo.TLS, fileWatcher, logger) + if err != nil { + return nil, nil, fmt.Errorf("cannot create cert manager: %w", err) + } + fl.AddFunc(certManager.Close) + + db, err := mongodb.NewStore(ctx, config, certManager.GetTLSConfig(), logger, tracerProvider) + if err != nil { + fl.Execute() + return nil, nil, fmt.Errorf("cannot create mongodb store: %w", err) + } + fl.AddFunc(func() { + if err := db.Close(ctx); err != nil { + log.Errorf("failed to close mongodb store: %w", err) + } + }) + + return db, fl.ToFunction(), nil +} diff --git a/device-provisioning-service/service/cloudConfiguration.go b/device-provisioning-service/service/cloudConfiguration.go new file mode 100644 index 000000000..cfd486305 --- /dev/null +++ b/device-provisioning-service/service/cloudConfiguration.go @@ -0,0 +1,191 @@ +package service + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/go-multierror" + "github.com/plgd-dev/device/v2/schema/cloud" + coapCodes "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/plgd-dev/go-coap/v3/mux" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/kit/v2/codec/cbor" +) + +func (RequestHandle) ProcessCloudConfiguration(ctx context.Context, req *mux.Message, session *Session, linkedHubs []*LinkedHub, group *EnrollmentGroup) (*pool.Message, error) { + switch req.Code() { + case coapCodes.POST: + msg, deviceID, cloudCfg, err := postProvisionCloudConfiguration(ctx, req, session, linkedHubs, group) + coapGateways := make([]*pb.CloudStatus_Gateway, 0, len(cloudCfg.Endpoints)) + selectedGateway := -1 + for idx, c := range cloudCfg.Endpoints { + coapGateways = append(coapGateways, &pb.CloudStatus_Gateway{ + Uri: c.URI, + Id: c.ID, + }) + if c.ID == cloudCfg.CloudID && c.URI == cloudCfg.URL { + selectedGateway = idx + } + } + session.updateProvisioningRecord(&store.ProvisioningRecord{ + DeviceId: deviceID, + Cloud: &pb.CloudStatus{ + Status: &pb.ProvisionStatus{ + Date: time.Now().UnixNano(), + CoapCode: toCoapCode(msg), + ErrorMessage: toErrorStr(err), + }, + Gateways: coapGateways, + ProviderName: cloudCfg.AuthorizationProvider, + SelectedGateway: int32(selectedGateway), + }, + }) + return msg, err + default: + return nil, statusErrorf(coapCodes.Forbidden, "unsupported command(%v)", req.Code()) + } +} + +type ProvisionCloudConfigurationRequest struct { + DeviceID string `json:"di"` + SelectedGateway cloud.Endpoint `json:"selectedGateway"` +} + +func postProvisionCloudConfiguration(ctx context.Context, req *mux.Message, session *Session, linkedHubs []*LinkedHub, group *EnrollmentGroup) (resp *pool.Message, deviceID string, cloudCfg cloud.ConfigurationUpdateRequest, err error) { + if req.Body() == nil { + return nil, "", cloudCfg, statusErrorf(coapCodes.BadRequest, "unable to parse cloud configuration request from empty body") + } + var provisionCloudConfigurationRequest ProvisionCloudConfigurationRequest + err = cbor.ReadFrom(req.Body(), &provisionCloudConfigurationRequest) + if err != nil { + return nil, provisionCloudConfigurationRequest.DeviceID, cloudCfg, statusErrorf(coapCodes.BadRequest, "unable to parse cloud configuration request from body: %w", err) + } + resp, cloudCfg, err = provisionCloudConfiguration(ctx, req, session, linkedHubs, group, provisionCloudConfigurationRequest) + return resp, provisionCloudConfigurationRequest.DeviceID, cloudCfg, err +} + +func findSelectedLinkedHub(selectedHub cloud.Endpoint, linkedHubs []*LinkedHub) *LinkedHub { + if selectedHub.URI == "" || selectedHub.ID == "" { + return nil + } + for _, l := range linkedHubs { + if l.cfg.GetHubId() == selectedHub.ID { + for _, c := range l.cfg.GetGateways() { + uri, _ := pb.ValidateCoapGatewayURI(c) + if uri == selectedHub.URI { + return l + } + } + } + } + return nil +} + +func findDefaultLinkedHub(linkedHubs []*LinkedHub) *LinkedHub { + if len(linkedHubs) == 0 { + return nil + } + return linkedHubs[0] +} + +func findLinkedHub(selectedHub cloud.Endpoint, linkedHubs []*LinkedHub) (*LinkedHub, cloud.Endpoint, error) { + linkedHub := findSelectedLinkedHub(selectedHub, linkedHubs) + if linkedHub != nil { + return linkedHub, selectedHub, nil + } + linkedHub = findDefaultLinkedHub(linkedHubs) + if linkedHub != nil { + var uri string + var errs *multierror.Error + for _, c := range linkedHub.cfg.GetGateways() { + var err error + uri, err = pb.ValidateCoapGatewayURI(c) + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("invalid coap gateway uri %v: %w", c, err)) + } else { + break + } + } + if uri != "" { + return linkedHub, cloud.Endpoint{ + URI: uri, + ID: linkedHub.cfg.GetId(), + }, nil + } + return nil, cloud.Endpoint{}, statusErrorf(coapCodes.BadRequest, "cannot find valid coap gateway: %w", errs.ErrorOrNil()) + } + return nil, cloud.Endpoint{}, statusErrorf(coapCodes.BadRequest, "cannot find linked hub") +} + +type cloudEndpoints []cloud.Endpoint + +func (e cloudEndpoints) Deduplicate() cloudEndpoints { + if len(e) == 0 { + return e + } + deduplicated := make(cloudEndpoints, 0, len(e)) + m := make(map[uint64]struct{}, len(e)) + for _, v := range e { + if _, ok := m[toCRC64([]byte(v.ID+v.URI))]; ok { + continue + } + m[toCRC64([]byte(v.ID+v.URI))] = struct{}{} + deduplicated = append(deduplicated, v) + } + return deduplicated +} + +func provisionCloudConfiguration(ctx context.Context, req *mux.Message, session *Session, linkedHubs []*LinkedHub, group *EnrollmentGroup, provisionCloudConfigurationRequest ProvisionCloudConfigurationRequest) (*pool.Message, cloud.ConfigurationUpdateRequest, error) { + linkedHub, selectedGateway, err := findLinkedHub(provisionCloudConfigurationRequest.SelectedGateway, linkedHubs) + if err != nil { + return nil, cloud.ConfigurationUpdateRequest{}, err + } + requiredClaims := map[string]interface{}{ + linkedHub.cfg.GetAuthorization().GetOwnerClaim(): group.Owner, + } + urlValues := map[string]string{ + linkedHub.cfg.GetAuthorization().GetOwnerClaim(): group.Owner, + } + if linkedHub.cfg.GetAuthorization().GetDeviceIdClaim() != "" { + urlValues[linkedHub.cfg.GetAuthorization().GetDeviceIdClaim()] = provisionCloudConfigurationRequest.DeviceID + requiredClaims[linkedHub.cfg.GetAuthorization().GetDeviceIdClaim()] = provisionCloudConfigurationRequest.DeviceID + } + token, err := linkedHub.GetTokenFromOAuth(ctx, urlValues, requiredClaims) + if err != nil { + err = processErrClaimNotFound(err, session.getLogger(), linkedHub, group) + return nil, cloud.ConfigurationUpdateRequest{}, statusErrorf(coapCodes.BadRequest, "cannot get token for cloud configuration response: %w", err) + } + + endpoints := make(cloudEndpoints, 0, len(linkedHubs)) + for _, l := range linkedHubs { + for _, c := range l.cfg.GetGateways() { + uri, errC := pb.ValidateCoapGatewayURI(c) + if errC != nil { + continue + } + endpoints = append(endpoints, cloud.Endpoint{ + URI: uri, + ID: l.cfg.GetId(), + }) + } + } + endpoints = endpoints.Deduplicate() + + resp := cloud.ConfigurationUpdateRequest{ + AuthorizationProvider: linkedHub.cfg.GetAuthorization().GetProvider().GetName(), + URL: selectedGateway.URI, + CloudID: selectedGateway.ID, + AuthorizationCode: token.AccessToken, + Endpoints: endpoints, + } + + msgType, data, err := encodeResponse(resp, req.Options()) + if err != nil { + return nil, cloud.ConfigurationUpdateRequest{}, statusErrorf(coapCodes.BadRequest, "cannot encode cloud configuration response: %w", err) + } + + return session.createResponse(coapCodes.Changed, req.Token(), msgType, data), resp, nil +} diff --git a/device-provisioning-service/service/cloudConfiguration_test.go b/device-provisioning-service/service/cloudConfiguration_test.go new file mode 100644 index 000000000..13eaaca89 --- /dev/null +++ b/device-provisioning-service/service/cloudConfiguration_test.go @@ -0,0 +1,155 @@ +package service_test + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + "time" + + "github.com/google/uuid" + "github.com/plgd-dev/device/v2/schema/cloud" + "github.com/plgd-dev/device/v2/schema/credential" + "github.com/plgd-dev/device/v2/schema/csr" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/go-coap/v3/options" + "github.com/plgd-dev/go-coap/v3/tcp" + coapTcpClient "github.com/plgd-dev/go-coap/v3/tcp/client" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + "github.com/plgd-dev/hub/v2/device-provisioning-service/uri" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/plgd-dev/kit/v2/security/generateCertificate" + "github.com/stretchr/testify/require" +) + +func GetCredentials(ctx context.Context, t *testing.T, c *coapTcpClient.Conn, deviceID string) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + csrReq, err := generateCertificate.GenerateIdentityCSR(generateCertificate.Configuration{}, deviceID, priv) + require.NoError(t, err) + + req := service.CredentialsRequest{ + CSR: service.CSR{ + Encoding: csr.CertificateEncoding_PEM, + Data: string(csrReq), + }, + } + + resp, err := c.Post(ctx, uri.Credentials, message.AppOcfCbor, bytes.NewReader(toCbor(t, req))) + require.NoError(t, err) + + var creds credential.CredentialUpdateRequest + fromCbor(t, resp.Body(), &creds) + + require.Len(t, creds.Credentials, 2) +} + +func TestCloudPOST(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesCertificateAuthority|hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId) + defer hubShutdown() + dpsCfg := test.MakeConfig(t) + shutDown := test.New(t, dpsCfg) + defer shutDown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) + defer cancel() + + c, err := tcp.Dial(dpsCfg.APIs.COAP.Addr, options.WithTLS(setupTLSConfig(t)), options.WithContext(ctx)) + require.NoError(t, err) + defer func() { + errC := c.Close() + require.NoError(t, errC) + }() + + resp, err := c.Post(ctx, uri.CloudConfiguration, message.AppOcfCbor, bytes.NewReader(toCbor(t, service.ProvisionCloudConfigurationRequest{DeviceID: uuid.NewString()}))) + require.NoError(t, err) + + var cfg cloud.ConfigurationUpdateRequest + fromCbor(t, resp.Body(), &cfg) + + require.NotEmpty(t, cfg.AuthorizationCode) + require.NotEmpty(t, cfg.AuthorizationProvider) + require.NotEmpty(t, cfg.CloudID) + require.NotEmpty(t, cfg.URL) +} + +func TestInvalidateEnrollmentGroupHubCache(t *testing.T) { + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesCertificateAuthority|hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId) + defer hubShutdown() + + store, closeStore := test.NewMongoStore(t) + defer closeStore() + + dpsCfg := test.MakeConfig(t) + dpsCfg.Clients.Storage.CacheExpiration = time.Minute + shutDown := test.New(t, dpsCfg) + defer shutDown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*8) + defer cancel() + + c, err := tcp.Dial(dpsCfg.APIs.COAP.Addr, options.WithTLS(setupTLSConfig(t)), options.WithContext(ctx)) + require.NoError(t, err) + defer func() { + errC := c.Close() + require.NoError(t, errC) + }() + + resp, err := c.Post(ctx, uri.CloudConfiguration, message.AppOcfCbor, bytes.NewReader(toCbor(t, service.ProvisionCloudConfigurationRequest{DeviceID: uuid.NewString()}))) + require.NoError(t, err) + + var cfg cloud.ConfigurationUpdateRequest + fromCbor(t, resp.Body(), &cfg) + + require.NotEmpty(t, cfg.AuthorizationCode) + require.NotEmpty(t, cfg.AuthorizationProvider) + require.NotEmpty(t, cfg.CloudID) + require.NotEmpty(t, cfg.URL) + + eg, hubs, err := dpsCfg.EnrollmentGroups[0].ToProto() + require.NoError(t, err) + + // test invalidate hub cache + hub := hubs[0] + hub.Gateways = []string{"invalidateCacheHub"} + err = store.UpdateHub(ctx, hub.GetOwner(), hub) + require.NoError(t, err) + + // wait for sync caches + time.Sleep(time.Millisecond * 500) + + resp, err = c.Post(ctx, uri.CloudConfiguration, message.AppOcfCbor, bytes.NewReader(toCbor(t, service.ProvisionCloudConfigurationRequest{DeviceID: uuid.NewString()}))) + require.NoError(t, err) + var cfg1 cloud.ConfigurationUpdateRequest + fromCbor(t, resp.Body(), &cfg1) + require.Equal(t, hub.GetGateways()[0], cfg1.URL) + + // test invalidate enrollment group cache + hub.Id = "00000000-0000-0000-0000-000000012345" + hub.HubId = hub.GetId() + hub.Gateways[0] = "invalidateEnrollmentGroup" + err = store.CreateHub(ctx, hub.GetOwner(), hub) + require.NoError(t, err) + eg.HubIds = []string{hub.GetId()} + err = store.UpdateEnrollmentGroup(ctx, eg.GetOwner(), eg) + require.NoError(t, err) + + // wait for sync caches + time.Sleep(time.Millisecond * 500) + + c2, err := tcp.Dial(dpsCfg.APIs.COAP.Addr, options.WithTLS(setupTLSConfig(t)), options.WithContext(ctx)) + require.NoError(t, err) + defer func() { + errC := c2.Close() + require.NoError(t, errC) + }() + resp, err = c2.Post(ctx, uri.CloudConfiguration, message.AppOcfCbor, bytes.NewReader(toCbor(t, service.ProvisionCloudConfigurationRequest{DeviceID: uuid.NewString()}))) + require.NoError(t, err) + var cfg2 cloud.ConfigurationUpdateRequest + fromCbor(t, resp.Body(), &cfg2) + require.Equal(t, hub.GetGateways()[0], cfg2.URL) +} diff --git a/device-provisioning-service/service/config.go b/device-provisioning-service/service/config.go new file mode 100644 index 000000000..3bba13daa --- /dev/null +++ b/device-provisioning-service/service/config.go @@ -0,0 +1,416 @@ +package service + +import ( + "fmt" + "hash/crc64" + "time" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/security/oauth/clientcredentials" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service/http" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store/mongodb" + "github.com/plgd-dev/hub/v2/pkg/config" + "github.com/plgd-dev/hub/v2/pkg/config/property/urischeme" + "github.com/plgd-dev/hub/v2/pkg/log" + pkgCoapService "github.com/plgd-dev/hub/v2/pkg/net/coap/service" + "github.com/plgd-dev/hub/v2/pkg/net/grpc/client" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpClient "github.com/plgd-dev/hub/v2/pkg/net/http/client" + pkgCertManagerClient "github.com/plgd-dev/hub/v2/pkg/security/certManager/client" + "github.com/plgd-dev/hub/v2/pkg/strings" + "github.com/plgd-dev/hub/v2/pkg/sync/task/queue" +) + +// Config represents application configuration +type Config struct { + Log LogConfig `yaml:"log" json:"log"` + APIs APIsConfig `yaml:"apis" json:"apis"` + TaskQueue queue.Config `yaml:"taskQueue" json:"taskQueue"` + Clients ClientsConfig `yaml:"clients" json:"clients"` + EnrollmentGroups EnrollmentGroups `yaml:"enrollmentGroups" json:"enrollmentGroups"` +} + +func (c *Config) Validate() error { + if err := c.APIs.Validate(); err != nil { + return fmt.Errorf("apis.%w", err) + } + if err := c.Log.Validate(); err != nil { + return fmt.Errorf("log.%w", err) + } + if err := c.TaskQueue.Validate(); err != nil { + return fmt.Errorf("taskQueue.%w", err) + } + if err := c.Clients.Validate(); err != nil { + return fmt.Errorf("clients.%w", err) + } + if len(c.EnrollmentGroups) == 0 { + // EnrollmentGroups are optional because they can be added later through the HTTP API + return nil + } + for i := range c.EnrollmentGroups { + if err := c.EnrollmentGroups[i].Validate(); err != nil { + return fmt.Errorf("enrollmentGroups[%v].%w", i, err) + } + } + return nil +} + +// LogConfig represents application configuration +type LogConfig = log.Config + +type GrpcClientConfig struct { + Connection client.Config `yaml:"grpc" json:"grpc"` +} + +func (c *GrpcClientConfig) Validate() error { + if err := c.Connection.Validate(); err != nil { + return fmt.Errorf("grpc.%w", err) + } + return nil +} + +func TLSConfigToProto(cfg pkgCertManagerClient.Config) (*pb.TlsConfig, error) { + if err := cfg.Validate(); err != nil { + return nil, err + } + caPool, err := cfg.CAPoolArray() + if err != nil { + return nil, err + } + caPoolStr := make([]string, 0, len(caPool)) + for _, ca := range caPool { + caPoolStr = append(caPoolStr, string(ca)) + } + + return &pb.TlsConfig{ + CaPool: caPoolStr, + Cert: string(cfg.CertFile), + Key: string(cfg.KeyFile), + UseSystemCaPool: cfg.UseSystemCAPool, + }, nil +} + +func (c *GrpcClientConfig) ToProto() (*pb.GrpcClientConfig, error) { + tls, err := TLSConfigToProto(c.Connection.TLS) + if err != nil { + return nil, err + } + + return &pb.GrpcClientConfig{ + Grpc: &pb.GrpcConnectionConfig{ + Address: c.Connection.Addr, + KeepAlive: &pb.GrpcKeepAliveConfig{ + PermitWithoutStream: c.Connection.KeepAlive.PermitWithoutStream, + Time: c.Connection.KeepAlive.Time.Nanoseconds(), + Timeout: c.Connection.KeepAlive.Timeout.Nanoseconds(), + }, + Tls: tls, + }, + }, nil +} + +type HTTPConfig struct { + http.Config `yaml:",inline"` + Enabled bool `yaml:"enabled" json:"enabled"` +} + +func (c *HTTPConfig) Validate() error { + return c.Config.Validate() +} + +type APIsConfig struct { + COAP COAPConfig `yaml:"coap" json:"coap"` + HTTP HTTPConfig `yaml:"http" json:"http"` +} + +func (c *APIsConfig) Validate() error { + if err := c.COAP.Validate(); err != nil { + return fmt.Errorf("coap.%w", err) + } + if c.HTTP.Enabled { + if err := c.HTTP.Validate(); err != nil { + return fmt.Errorf("http.%w", err) + } + } + return nil +} + +type COAPConfig struct { + pkgCoapService.Config `yaml:",inline" json:",inline"` +} + +func (c *COAPConfig) Validate() error { + c.TLS.Embedded.CAPoolIsOptional = true + c.TLS.Embedded.ClientCertificateRequired = true + enabled := true + c.TLS.Enabled = &enabled + return c.Config.Validate() +} + +// String returns string representation of Config +func (c Config) String() string { + return config.ToString(c) +} + +type EnrollmentGroups []EnrollmentGroupConfig + +func (g EnrollmentGroups) FindByID(id string) (EnrollmentGroupConfig, bool) { + for _, eg := range g { + if eg.ID == id { + return eg, true + } + } + return EnrollmentGroupConfig{}, false +} + +func toCRC64(d []byte) uint64 { + c := crc64.New(crc64.MakeTable(crc64.ISO)) + c.Write(d) + return c.Sum64() +} + +type X509 struct { + CertificateChain urischeme.URIScheme `yaml:"certificateChain" json:"certificateChain"` + LeadCertificateName string `yaml:"leadCertificateName" json:"leadCertificateName"` + ExpiredCertificateEnabled bool `yaml:"expiredCertificateEnabled" json:"expiredCertificateEnabled"` +} + +func (c *X509) ToProto() *pb.X509Configuration { + return &pb.X509Configuration{ + CertificateChain: string(c.CertificateChain), + ExpiredCertificateEnabled: c.ExpiredCertificateEnabled, + LeadCertificateName: c.LeadCertificateName, + } +} + +type AttestationMechanism struct { + X509 X509 `yaml:"x509" json:"x509"` +} + +func (c *AttestationMechanism) ToProto() *pb.AttestationMechanism { + return &pb.AttestationMechanism{ + X509: c.X509.ToProto(), + } +} + +type EnrollmentGroupConfig struct { + ID string `yaml:"id" json:"id"` + Owner string `yaml:"owner" json:"owner"` + AttestationMechanism AttestationMechanism `yaml:"attestationMechanism" json:"attestationMechanism"` + Hub HubConfig `yaml:"hub" json:"hub"` + Hubs []HubConfig `yaml:"hubs" json:"hubs"` + PreSharedKeyFile urischeme.URIScheme `yaml:"preSharedKeyFile" json:"preSharedKeyFile"` + Name string `yaml:"name" json:"name"` +} + +func (e *EnrollmentGroupConfig) ToProto() (*pb.EnrollmentGroup, []*pb.Hub, error) { + attestationMechanism := e.AttestationMechanism.ToProto() + hubIDs := make([]string, 0, len(e.Hubs)) + hubs := make([]*pb.Hub, 0, len(e.Hubs)) + if e.Hub.ID != "" || e.Hub.HubID != "" { + hub, err := e.Hub.ToProto(e.Owner) + if err == nil { + err = hub.Validate(e.Owner) + } + if err != nil { + return nil, nil, fmt.Errorf("hub.%w", err) + } + hubIDs = append(hubIDs, hub.GetHubId()) + hubs = append(hubs, hub) + } + for idx, hub := range e.Hubs { + hub, err := hub.ToProto(e.Owner) + if err == nil { + err = hub.Validate(e.Owner) + } + if err != nil { + return nil, nil, fmt.Errorf("hubs[%v].%w", idx, err) + } + hubIDs = append(hubIDs, hub.GetHubId()) + hubs = append(hubs, hub) + } + eg := &pb.EnrollmentGroup{ + Id: e.ID, + Owner: e.Owner, + AttestationMechanism: attestationMechanism, + HubIds: hubIDs, + PreSharedKey: string(e.PreSharedKeyFile), + Name: e.Name, + } + if err := eg.Validate(e.Owner); err != nil { + return nil, nil, err + } + return eg, hubs, nil +} + +func (e *EnrollmentGroupConfig) String() string { + return e.ID +} + +func (e *EnrollmentGroupConfig) Validate() error { + _, _, err := e.ToProto() + return err +} + +type AuthorizationProviderConfig struct { + Name string `yaml:"name" json:"name"` + clientcredentials.Config `yaml:",inline"` +} + +func HTTPConfigToProto(cfg pkgHttpClient.Config) (*pb.HttpConfig, error) { + tls, err := TLSConfigToProto(cfg.TLS) + if err != nil { + return nil, err + } + + return &pb.HttpConfig{ + MaxIdleConns: uint32(cfg.MaxIdleConns), + MaxConnsPerHost: uint32(cfg.MaxConnsPerHost), + MaxIdleConnsPerHost: uint32(cfg.MaxIdleConnsPerHost), + IdleConnTimeout: cfg.IdleConnTimeout.Nanoseconds(), + Timeout: cfg.Timeout.Nanoseconds(), + Tls: tls, + }, nil +} + +func (c *AuthorizationProviderConfig) ToProto() (*pb.AuthorizationProviderConfig, error) { + http, err := HTTPConfigToProto(c.HTTP) + if err != nil { + return nil, err + } + + return &pb.AuthorizationProviderConfig{ + Name: c.Name, + Authority: c.Authority, + ClientId: c.ClientID, + ClientSecret: string(c.ClientSecretFile), + Scopes: c.Scopes, + Audience: c.Audience, + Http: http, + }, nil +} + +type AuthorizationConfig struct { + OwnerClaim string `yaml:"ownerClaim" json:"ownerClaim"` + DeviceIDClaim string `yaml:"deviceIDClaim" json:"deviceIdClaim"` + Provider AuthorizationProviderConfig `yaml:"provider" json:"provider"` +} + +func (c *AuthorizationConfig) ToProto() (*pb.AuthorizationConfig, error) { + provider, err := c.Provider.ToProto() + if err != nil { + return nil, fmt.Errorf("provider.%w", err) + } + return &pb.AuthorizationConfig{ + Provider: provider, + OwnerClaim: c.OwnerClaim, + DeviceIdClaim: c.DeviceIDClaim, + }, nil +} + +type HubConfig struct { + ID string `yaml:"id" json:"id"` + HubID string `yaml:"hubID" json:"hubId"` + CoapGateway string `yaml:"coapGateway" json:"coapGateway"` + Gateways []string `yaml:"gateways" json:"gateways"` + CertificateAuthority GrpcClientConfig `yaml:"certificateAuthority" json:"certificateAuthority"` + Authorization AuthorizationConfig `yaml:"authorization" json:"authorization"` + Name string `yaml:"name" json:"name"` +} + +func (c *HubConfig) Validate(owner string) error { + p, err := c.ToProto(owner) + if err != nil { + return err + } + return p.Validate(owner) +} + +func (c *HubConfig) ToProto(owner string) (*pb.Hub, error) { + authorization, err := c.Authorization.ToProto() + if err != nil { + return nil, fmt.Errorf("authorization.%w", err) + } + certificateAuthority, err := c.CertificateAuthority.ToProto() + if err != nil { + return nil, fmt.Errorf("authorization.%w", err) + } + coapGWs := make([]string, 0, len(c.Gateways)+1) + if c.CoapGateway != "" { + coapGWs = append(coapGWs, c.CoapGateway) + } + coapGWs = append(coapGWs, c.Gateways...) + if c.ID == "" { + c.ID = c.HubID + } + if c.HubID == "" { + c.HubID = c.ID + } + + return &pb.Hub{ + Id: c.ID, + HubId: c.HubID, + Gateways: strings.UniqueStable(coapGWs), + CertificateAuthority: certificateAuthority, + Authorization: authorization, + Name: c.Name, + Owner: owner, + }, nil +} + +type CoapConfig struct { + Address string `yaml:"address" json:"address"` +} + +func (c *CoapConfig) Validate() error { + if c.Address == "" { + return fmt.Errorf("address('%v')", c.Address) + } + return nil +} + +type CoapGatewayConfig struct { + COAP CoapConfig `yaml:"coap" json:"coap"` + ProviderName string `yaml:"providerName" json:"providerName"` +} + +func (c *CoapGatewayConfig) Validate() error { + if err := c.COAP.Validate(); err != nil { + return fmt.Errorf("coap.%w", err) + } + if c.ProviderName == "" { + return fmt.Errorf("providerName('%v')", c.ProviderName) + } + return nil +} + +type ClientsConfig struct { + Storage StorageConfig `yaml:"storage" json:"storage"` + OpenTelemetryCollector pkgHttp.OpenTelemetryCollectorConfig `yaml:"openTelemetryCollector" json:"openTelemetryCollector"` +} + +func (c *ClientsConfig) Validate() error { + if err := c.Storage.Validate(); err != nil { + return fmt.Errorf("storage.%w", err) + } + if err := c.OpenTelemetryCollector.Validate(); err != nil { + return fmt.Errorf("openTelemetryCollector.%w", err) + } + return nil +} + +type StorageConfig struct { + // expiration time of cached DB records + CacheExpiration time.Duration `yaml:"cacheExpiration" json:"cacheExpiration"` + MongoDB mongodb.Config `yaml:"mongoDB" json:"mongoDb"` //nolint:tagliatelle +} + +func (c *StorageConfig) Validate() error { + if c.CacheExpiration < time.Second { + return fmt.Errorf("cacheExpiration('%v') - less than %v", c.CacheExpiration, time.Second) + } + if err := c.MongoDB.Validate(); err != nil { + return fmt.Errorf("mongoDB.%w", err) + } + return nil +} diff --git a/device-provisioning-service/service/credentials.go b/device-provisioning-service/service/credentials.go new file mode 100644 index 000000000..c470137aa --- /dev/null +++ b/device-provisioning-service/service/credentials.go @@ -0,0 +1,231 @@ +package service + +import ( + "context" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "time" + + "github.com/plgd-dev/device/v2/pkg/net/coap" + "github.com/plgd-dev/device/v2/schema/cloud" + "github.com/plgd-dev/device/v2/schema/credential" + "github.com/plgd-dev/device/v2/schema/csr" + "github.com/plgd-dev/go-coap/v3/message" + coapCodes "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/plgd-dev/go-coap/v3/mux" + "github.com/plgd-dev/hub/v2/certificate-authority/pb" + "github.com/plgd-dev/hub/v2/coap-gateway/coapconv" + dpsPb "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/security/oauth/clientcredentials" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/identity-store/events" + "github.com/plgd-dev/hub/v2/pkg/log" + "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/kit/v2/codec/cbor" + "github.com/plgd-dev/kit/v2/security" +) + +type CSR struct { + Encoding csr.CertificateEncoding `json:"encoding"` + Data string `json:"data"` +} + +type CredentialsRequest struct { + CSR CSR `json:"csr"` + SelectedGateway cloud.Endpoint `json:"selectedGateway"` +} + +func (RequestHandle) ProcessCredentials(ctx context.Context, req *mux.Message, session *Session, linkedHubs []*LinkedHub, group *EnrollmentGroup) (*pool.Message, error) { + switch req.Code() { + case coapCodes.POST: + msg, deviceID, identityCertPem, psk, credentials, err := provisionCredentials(ctx, req, session, linkedHubs, group) + session.updateProvisioningRecord(&store.ProvisioningRecord{ + DeviceId: deviceID, + Credential: &dpsPb.CredentialStatus{ + Status: &dpsPb.ProvisionStatus{ + Date: time.Now().UnixNano(), + CoapCode: toCoapCode(msg), + ErrorMessage: toErrorStr(err), + }, + IdentityCertificatePem: identityCertPem, + PreSharedKey: psk, + Credentials: credentials, + }, + }) + return msg, err + default: + return nil, statusErrorf(coapCodes.Forbidden, "unsupported command(%v)", req.Code()) + } +} + +func (s *Session) signCertificate(ctx context.Context, linkedHub *LinkedHub, csrReq *CredentialsRequest) ([]*x509.Certificate, error) { + if len(csrReq.CSR.Data) == 0 { + // csr property was not set so we will send only CAs + return nil, nil + } + if csrReq.CSR.Encoding != csr.CertificateEncoding_PEM { + return nil, fmt.Errorf("unsupported encoding (%v)", csrReq.CSR.Encoding) + } + resp, err := linkedHub.SignIdentityCertificate(ctx, &pb.SignCertificateRequest{CertificateSigningRequest: []byte(csrReq.CSR.Data)}) + if err != nil { + return nil, fmt.Errorf("cannot sign identity certificate: %w", err) + } + identityChanCert := resp.GetCertificate() + + certsFromChain, err := security.ParseX509FromPEM(identityChanCert) + if err != nil { + return nil, fmt.Errorf("cannot parse chain of X509 certs: %w", err) + } + return certsFromChain, nil +} + +func encodeResponse(resp interface{}, options message.Options) (message.MediaType, []byte, error) { + accept := coapconv.GetAccept(options) + encode, err := coapconv.GetEncoder(accept) + if err != nil { + return 0, nil, err + } + out, err := encode(resp) + if err != nil { + return 0, nil, err + } + return accept, out, nil +} + +func certsToPem(certs []*x509.Certificate) string { + certificateChain := make([]byte, 0, 4096) + for _, cert := range certs { + certificateChain = append(certificateChain, pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", Bytes: cert.Raw, + })...) + } + return string(certificateChain) +} + +func certToPem(c *x509.Certificate) string { + return certsToPem([]*x509.Certificate{c}) +} + +func processErrClaimNotFound(err error, logger log.Logger, linkedHub *LinkedHub, group *EnrollmentGroup) error { + var claimNotFound clientcredentials.ClaimNotFoundError + if errors.As(err, &claimNotFound) && claimNotFound.Claim == linkedHub.cfg.GetAuthorization().GetOwnerClaim() { + err = fmt.Errorf("configured OAuth client for the hub %v used in enrollment group %v returned owner id claim %v which doesn't match the expected value %v", + linkedHub.cfg.GetId(), group.GetId(), claimNotFound.Claim, group.GetOwner()) + logger.Warn(err) + } + return err +} + +func parseProvisionCredentialsReq(req *mux.Message) (*CredentialsRequest, error) { + if req.Body() == nil { + // body is empty we will send only CAs from configuration. + return nil, errors.New("empty body") + } + ct, err := req.Options().ContentFormat() + if err != nil { + return nil, fmt.Errorf("cannot get content type: %w", err) + } + + var csrReq CredentialsRequest + switch ct { + case message.AppCBOR, message.AppOcfCbor: + err = cbor.ReadFrom(req.Body(), &csrReq) + if err != nil { + return nil, fmt.Errorf("cannot decode body: %w", err) + } + default: + return nil, fmt.Errorf("unsupported content type(%v)", ct) + } + return &csrReq, nil +} + +func provisionCredentials(ctx context.Context, req *mux.Message, session *Session, linkedHubs []*LinkedHub, group *EnrollmentGroup) (resp *pool.Message, deviceID string, identityCertPem string, psk *dpsPb.PreSharedKey, credentials []*dpsPb.Credential, err error) { + credReq, err := parseProvisionCredentialsReq(req) + if err != nil { + return nil, "", "", nil, nil, statusErrorf(coapCodes.BadRequest, "cannot parse request: %w", err) + } + + linkedHub, _, err := findLinkedHub(credReq.SelectedGateway, linkedHubs) + if err != nil { + return nil, "", "", nil, nil, statusErrorf(coapCodes.BadRequest, "cannot find linked hub: %w", err) + } + + token, err := linkedHub.GetToken(ctx, group.Owner, map[string]string{ + linkedHub.cfg.GetAuthorization().GetOwnerClaim(): group.Owner, + }, map[string]interface{}{ + linkedHub.cfg.GetAuthorization().GetOwnerClaim(): group.Owner, + }) + if err != nil { + err = processErrClaimNotFound(err, session.getLogger(), linkedHub, group) + return nil, "", "", nil, nil, statusErrorf(coapCodes.InternalServerError, "cannot get token for enrollment group %v: %w", group.GetId(), err) + } + ctx = grpc.CtxWithToken(ctx, token.AccessToken) + ownerID := events.OwnerToUUID(group.Owner) + chain, err := session.signCertificate(ctx, linkedHub, credReq) + if err != nil { + return nil, "", "", nil, nil, statusErrorf(coapCodes.BadRequest, "%w", err) + } + if len(chain) == 0 { + return nil, "", "", nil, nil, statusErrorf(coapCodes.InternalServerError, "unexpected empty chain") + } + var credUpdResp credential.CredentialUpdateRequest + key, ok, err := group.ResolvePreSharedKey() + if err == nil && ok { + psk = &dpsPb.PreSharedKey{ + SubjectId: ownerID, + Key: key[:16], + } + credUpdResp.Credentials = append(credUpdResp.Credentials, credential.Credential{ + Subject: ownerID, + Type: credential.CredentialType_SYMMETRIC_PAIR_WISE, + PrivateData: &credential.CredentialPrivateData{ + DataInternal: psk.GetKey(), + Encoding: credential.CredentialPrivateDataEncoding_RAW, + }, + Tag: DPSTag, + }) + } + if len(chain) > 0 { + deviceID, err = coap.GetDeviceIDFromIdentityCertificate(chain[0]) + if err != nil { + return nil, "", "", nil, nil, statusErrorf(coapCodes.BadRequest, "%w", err) + } + session.SetDeviceID(deviceID) + + identityCert := make([]byte, 0, 1024) + for i := 0; i < len(chain)-1; i++ { + identityCert = append(identityCert, certToPem(chain[i])...) + } + identityCertPem = string(identityCert) + + credUpdResp.Credentials = append(credUpdResp.Credentials, credential.Credential{ + Subject: deviceID, + Type: credential.CredentialType_ASYMMETRIC_SIGNING_WITH_CERTIFICATE, + Usage: credential.CredentialUsage_CERT, + PublicData: &credential.CredentialPublicData{ + DataInternal: identityCertPem, + Encoding: credential.CredentialPublicDataEncoding_PEM, + }, + Tag: DPSTag, + }) + credUpdResp.Credentials = append(credUpdResp.Credentials, credential.Credential{ + Subject: ownerID, + Type: credential.CredentialType_ASYMMETRIC_SIGNING_WITH_CERTIFICATE, + Usage: credential.CredentialUsage_TRUST_CA, + PublicData: &credential.CredentialPublicData{ + DataInternal: string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: chain[len(chain)-1].Raw})), + Encoding: credential.CredentialPublicDataEncoding_PEM, + }, + Tag: DPSTag, + }) + } + + msgType, data, err := encodeResponse(credUpdResp, req.Options()) + if err != nil { + return nil, "", "", nil, nil, statusErrorf(coapCodes.BadRequest, "cannot encode credentials: %w", err) + } + return session.createResponse(coapCodes.Changed, req.Token(), msgType, data), deviceID, identityCertPem, psk, dpsPb.CredentialsToPb(credUpdResp.Credentials), nil +} diff --git a/device-provisioning-service/service/credentials_test.go b/device-provisioning-service/service/credentials_test.go new file mode 100644 index 000000000..b231f9296 --- /dev/null +++ b/device-provisioning-service/service/credentials_test.go @@ -0,0 +1,213 @@ +package service_test + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "io" + "os" + "testing" + "time" + + "github.com/google/uuid" + "github.com/plgd-dev/device/v2/schema/credential" + "github.com/plgd-dev/device/v2/schema/csr" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/go-coap/v3/options" + "github.com/plgd-dev/go-coap/v3/tcp" + caService "github.com/plgd-dev/hub/v2/certificate-authority/test" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + "github.com/plgd-dev/hub/v2/device-provisioning-service/uri" + "github.com/plgd-dev/hub/v2/pkg/config/property/urischeme" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/plgd-dev/kit/v2/codec/cbor" + "github.com/plgd-dev/kit/v2/security" + "github.com/plgd-dev/kit/v2/security/generateCertificate" + "github.com/stretchr/testify/require" +) + +func setupTLSConfig(t *testing.T) *tls.Config { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + signerCert, err := security.LoadX509(os.Getenv("TEST_DPS_INTERMEDIATE_CA_CERT")) + require.NoError(t, err) + signerKey, err := security.LoadX509PrivateKey(os.Getenv("TEST_DPS_INTERMEDIATE_CA_KEY")) + require.NoError(t, err) + + certData, err := generateCertificate.GenerateCert(generateCertificate.Configuration{ + ValidFrom: time.Now().Add(-time.Hour).Format(time.RFC3339), + ValidFor: 2 * time.Hour, + ExtensionKeyUsages: []string{"client", "server"}, + }, priv, signerCert, signerKey) + require.NoError(t, err) + b, err := x509.MarshalECPrivateKey(priv) + require.NoError(t, err) + key := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}) + crt, err := tls.X509KeyPair(certData, key) + require.NoError(t, err) + caPool := x509.NewCertPool() + for _, c := range signerCert { + caPool.AddCert(c) + } + return &tls.Config{ + Certificates: []tls.Certificate{ + crt, + }, + RootCAs: caPool, + } +} + +func toCbor(t *testing.T, v interface{}) []byte { + data, err := cbor.Encode(v) + require.NoError(t, err) + return data +} + +func fromCbor(t *testing.T, w io.Reader, v interface{}) { + err := cbor.ReadFrom(w, v) + require.NoError(t, err) +} + +func privateKeyToPem(t *testing.T, p *ecdsa.PrivateKey) []byte { + b, err := x509.MarshalECPrivateKey(p) + require.NoError(t, err) + return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}) +} + +func TestCredentials(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId) + defer hubShutdown() + caCfg := caService.MakeConfig(t) + caCfg.Signer.ExpiresIn = time.Hour * 10 + caShutdown := caService.New(t, caCfg) + defer caShutdown() + + dpsCfg := test.MakeConfig(t) + shutDown := test.New(t, dpsCfg) + defer shutDown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*8) + defer cancel() + + // create connection via mfg certificate + c, err := tcp.Dial(dpsCfg.APIs.COAP.Addr, options.WithTLS(setupTLSConfig(t)), options.WithContext(ctx)) + require.NoError(t, err) + defer func() { + errC := c.Close() + require.NoError(t, errC) + }() + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + deviceID, err := uuid.NewRandom() + require.NoError(t, err) + + csrReq, err := generateCertificate.GenerateIdentityCSR(generateCertificate.Configuration{}, deviceID.String(), priv) + require.NoError(t, err) + + req := service.CredentialsRequest{ + CSR: service.CSR{ + Encoding: csr.CertificateEncoding_PEM, + Data: string(csrReq), + }, + } + + resp, err := c.Post(ctx, uri.Credentials, message.AppOcfCbor, bytes.NewReader(toCbor(t, req))) + require.NoError(t, err) + + var creds credential.CredentialUpdateRequest + fromCbor(t, resp.Body(), &creds) + + require.Len(t, creds.Credentials, 2) + + // create connection via identity certificate + key := privateKeyToPem(t, priv) + crt, err := tls.X509KeyPair(creds.Credentials[0].PublicData.Data(), key) + require.NoError(t, err) + caPool := x509.NewCertPool() + signerCert, err := security.LoadX509(os.Getenv("TEST_ROOT_CA_CERT")) + require.NoError(t, err) + for _, c := range signerCert { + caPool.AddCert(c) + } + tlsCfg := &tls.Config{ + Certificates: []tls.Certificate{ + crt, + }, + RootCAs: caPool, + } + c1, err := tcp.Dial(dpsCfg.APIs.COAP.Addr, options.WithTLS(tlsCfg), options.WithContext(ctx)) + require.NoError(t, err) + + _, err = c1.Post(ctx, uri.Credentials, message.AppOcfCbor, bytes.NewReader(toCbor(t, req))) + require.Error(t, err) + + _ = c1.Close() +} + +const testPSK = "0123456789abcdef" + +func TestCredentialsWithPSK(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId) + defer hubShutdown() + caCfg := caService.MakeConfig(t) + caCfg.Signer.ExpiresIn = time.Hour * 10 + caShutdown := caService.New(t, caCfg) + defer caShutdown() + + psk := testPSK + pskFile := writeToTempFile(t, "psk.key", []byte(psk)) + defer func() { + err := os.Remove(pskFile) + require.NoError(t, err) + }() + + dpsCfg := test.MakeConfig(t) + dpsCfg.EnrollmentGroups[0].PreSharedKeyFile = urischeme.URIScheme(pskFile) + shutDown := test.New(t, dpsCfg) + defer shutDown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*8) + defer cancel() + + // create connection via mfg certificate + c, err := tcp.Dial(dpsCfg.APIs.COAP.Addr, options.WithTLS(setupTLSConfig(t)), options.WithContext(ctx)) + require.NoError(t, err) + defer func() { + errC := c.Close() + require.NoError(t, errC) + }() + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + deviceID, err := uuid.NewRandom() + require.NoError(t, err) + + csrReq, err := generateCertificate.GenerateIdentityCSR(generateCertificate.Configuration{}, deviceID.String(), priv) + require.NoError(t, err) + + req := service.CredentialsRequest{ + CSR: service.CSR{ + Encoding: csr.CertificateEncoding_PEM, + Data: string(csrReq), + }, + } + + resp, err := c.Post(ctx, uri.Credentials, message.AppOcfCbor, bytes.NewReader(toCbor(t, req))) + require.NoError(t, err) + + var creds credential.CredentialUpdateRequest + fromCbor(t, resp.Body(), &creds) + + require.Len(t, creds.Credentials, 3) +} diff --git a/device-provisioning-service/service/enrollmentGroupsCache.go b/device-provisioning-service/service/enrollmentGroupsCache.go new file mode 100644 index 000000000..ba54b9124 --- /dev/null +++ b/device-provisioning-service/service/enrollmentGroupsCache.go @@ -0,0 +1,243 @@ +package service + +import ( + "context" + "crypto/x509" + "errors" + "sync" + "time" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store/mongodb" + "github.com/plgd-dev/hub/v2/pkg/log" + "go.uber.org/atomic" +) + +type EnrollmentGroup struct { + *pb.EnrollmentGroup + AttestationMechanismX509CertificateChain []*x509.Certificate + expireAt atomic.Time +} + +type enrollmentGroupsElement = sync.Map // map[EnrollmentGroup.Id]*EnrollmentGroup + +type EnrollmentGroupsCache struct { + ctx context.Context + cancel context.CancelFunc + cache sync.Map // map[issuerName]*enrollmentGroupsElement + cacheByID sync.Map // map[id]*EnrollmentGroup + store *mongodb.Store + expiration time.Duration + wg sync.WaitGroup +} + +func NewEnrollmentGroupsCache(ctx context.Context, expiration time.Duration, store *mongodb.Store, logger log.Logger) *EnrollmentGroupsCache { + ctx, cancel := context.WithCancel(ctx) + eg := EnrollmentGroupsCache{ + store: store, + expiration: expiration, + ctx: ctx, + cancel: cancel, + } + eg.wg.Add(1) + go func() { + defer eg.wg.Done() + err := eg.watch() + if err != nil { + logger.Warnf("cannot watch for DB changes in enrollment groups: %v", err) + } + }() + return &eg +} + +func (c *EnrollmentGroupsCache) storeEnrollmentGroup(issuerName string, g *EnrollmentGroup) bool { + v, _ := c.cache.LoadOrStore(issuerName, new(enrollmentGroupsElement)) + e, ok := v.(*enrollmentGroupsElement) + if !ok { + return false + } + val, ok := e.LoadOrStore(g.GetId(), g) + c.cacheByID.Store(g.GetId(), val) + return !ok +} + +func (c *EnrollmentGroupsCache) removeByID(id string) { + // try to remove from cacheByID + val, ok := c.cacheByID.LoadAndDelete(id) + if !ok { + return + } + // try to remove from cache + g, ok := val.(*EnrollmentGroup) + if !ok { + return + } + certName := g.GetAttestationMechanism().GetX509().GetLeadCertificateName() + element, ok := c.cache.Load(certName) + if !ok { + return + } + e, ok := element.(*enrollmentGroupsElement) + if !ok { + c.cache.Delete(certName) + return + } + e.Delete(id) +} + +func (c *EnrollmentGroupsCache) watch() error { + iter, err := c.store.WatchEnrollmentGroups(c.ctx) + if err != nil { + return err + } + for { + _, id, ok := iter.Next(c.ctx) + if !ok { + break + } + c.removeByID(id) + } + err = iter.Err() + errClose := iter.Close() + if err == nil { + err = errClose + } + if errors.Is(err, context.Canceled) { + return nil + } + return err +} + +func (c *EnrollmentGroupsCache) Close() { + c.cancel() + c.wg.Wait() +} + +func (c *EnrollmentGroupsCache) getEnrollmentGroupsFromCache(issuerNames []string, onEnrollmentGroup func(g *EnrollmentGroup) (wantNext bool)) bool { + for _, issuerName := range issuerNames { + v, ok := c.cache.Load(issuerName) + if !ok { + continue + } + e, ok := v.(*enrollmentGroupsElement) + if !ok { + continue + } + wantNext := true + e.Range(func(_, value any) bool { + if g, ok := value.(*EnrollmentGroup); ok { + g.expireAt.Store(time.Now().Add(c.expiration)) + wantNext = onEnrollmentGroup(g) + return wantNext + } + return true + }) + if !wantNext { + return true + } + } + return false +} + +func (c *EnrollmentGroupsCache) iterateOverGroups(ctx context.Context, iter store.EnrollmentGroupIter, onEnrollmentGroup func(g *EnrollmentGroup) bool) error { + for { + var g pb.EnrollmentGroup + if !iter.Next(ctx, &g) { + break + } + var attestationMechanismX509CertificateChain []*x509.Certificate + var err error + if g.GetAttestationMechanism().GetX509().GetCertificateChain() != "" { + attestationMechanismX509CertificateChain, err = g.GetAttestationMechanism().GetX509().ResolveCertificateChain() + if err != nil { + log.Errorf("cannot parse AttestationMechanism.X509.CertificateChain for enrollment group %v", g.GetId()) + continue + } + } + eg := &EnrollmentGroup{ + EnrollmentGroup: &g, + AttestationMechanismX509CertificateChain: attestationMechanismX509CertificateChain, + } + eg.expireAt.Store(time.Now().Add(c.expiration)) + if !c.storeEnrollmentGroup(g.GetAttestationMechanism().GetX509().GetLeadCertificateName(), eg) { + continue + } + if !onEnrollmentGroup(eg) { + break + } + } + return iter.Err() +} + +func (c *EnrollmentGroupsCache) GetEnrollmentGroupsByIssuerNames(ctx context.Context, issuerNames []string, onEnrollmentGroup func(g *EnrollmentGroup) bool) error { + if c.getEnrollmentGroupsFromCache(issuerNames, onEnrollmentGroup) { + return nil + } + + return c.store.LoadEnrollmentGroups(ctx, "", &pb.GetEnrollmentGroupsRequest{ + AttestationMechanismX509CertificateNames: issuerNames, + }, func(ctx context.Context, iter store.EnrollmentGroupIter) (err error) { + return c.iterateOverGroups(ctx, iter, onEnrollmentGroup) + }) +} + +func compareEnrollmentGroupLeafCertificate(cert *x509.Certificate, g *EnrollmentGroup) bool { + if len(g.AttestationMechanismX509CertificateChain) == 0 { + return false + } + + return cert.CheckSignatureFrom(g.AttestationMechanismX509CertificateChain[0]) == nil +} + +func (c *EnrollmentGroupsCache) GetEnrollmentGroup(ctx context.Context, chains [][]*x509.Certificate) (*EnrollmentGroup, bool, error) { + var eg *EnrollmentGroup + for _, chain := range chains { + for _, cert := range chain { + err := c.GetEnrollmentGroupsByIssuerNames(ctx, []string{cert.Issuer.CommonName}, func(g *EnrollmentGroup) bool { + if compareEnrollmentGroupLeafCertificate(cert, g) { + eg = g + return false + } + return true + }) + if err != nil { + return nil, false, err + } + if eg != nil { + return eg, true, nil + } + } + } + + return nil, false, nil +} + +func (c *EnrollmentGroupsCache) CheckExpirations(t time.Time) { + c.cache.Range(func(issuerName, element any) bool { + e, ok := element.(*enrollmentGroupsElement) + if !ok { + c.cache.Delete(issuerName) + return true + } + wantToDelete := true + e.Range(func(id, enrollmentGroup any) bool { + g, ok := enrollmentGroup.(*EnrollmentGroup) + if !ok { + e.Delete(id) + return true + } + if g.expireAt.Load().After(t) { + e.Delete(id) + c.cacheByID.Delete(id) + } else { + wantToDelete = false + } + return true + }) + if wantToDelete { + c.cache.Delete(issuerName) + } + return true + }) +} diff --git a/device-provisioning-service/service/grpc/createEnrollmentGroup.go b/device-provisioning-service/service/grpc/createEnrollmentGroup.go new file mode 100644 index 000000000..b0a318000 --- /dev/null +++ b/device-provisioning-service/service/grpc/createEnrollmentGroup.go @@ -0,0 +1,31 @@ +package grpc + +import ( + "context" + + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (d *DeviceProvisionServiceServer) CreateEnrollmentGroup(ctx context.Context, req *pb.CreateEnrollmentGroupRequest) (*pb.EnrollmentGroup, error) { + owner, err := grpc.OwnerFromTokenMD(ctx, d.ownerClaim) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, "cannot get owner: %v", err) + } + g := &pb.EnrollmentGroup{ + Id: uuid.NewString(), + Owner: owner, + AttestationMechanism: req.GetAttestationMechanism(), + HubIds: req.GetHubIds(), + PreSharedKey: req.GetPreSharedKey(), + Name: req.GetName(), + } + err = d.store.CreateEnrollmentGroup(ctx, owner, g) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "enrollment group('%v'): %v", g.GetId(), err) + } + return g, nil +} diff --git a/device-provisioning-service/service/grpc/createEnrollmentGroup_test.go b/device-provisioning-service/service/grpc/createEnrollmentGroup_test.go new file mode 100644 index 000000000..1fdb15f9b --- /dev/null +++ b/device-provisioning-service/service/grpc/createEnrollmentGroup_test.go @@ -0,0 +1,111 @@ +package grpc_test + +import ( + "context" + "encoding/base64" + "testing" + + "github.com/fullstorydev/grpchan/inprocgrpc" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service/grpc" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + "github.com/plgd-dev/hub/v2/pkg/config/property/urischeme" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestDeviceProvisionServiceServerCreateEnrollmentGroup(t *testing.T) { + eg := test.NewEnrollmentGroup(t, uuid.NewString(), test.DPSOwner) + + certificateChain, err := urischeme.URIScheme(eg.GetAttestationMechanism().GetX509().GetCertificateChain()).Read() + require.NoError(t, err) + uriSchemeDataCertificateChain := "data:;base64," + base64.StdEncoding.EncodeToString(certificateChain) + + preSharedKey, err := urischeme.URIScheme(eg.GetPreSharedKey()).Read() + require.NoError(t, err) + uriSchemeDataPreSharedKey := "data:;base64," + base64.StdEncoding.EncodeToString(preSharedKey) + + type args struct { + req *pb.CreateEnrollmentGroupRequest + } + tests := []struct { + name string + args args + want *pb.EnrollmentGroup + wantErr bool + }{ + { + name: "valid-certificateChain-file", + args: args{ + req: &pb.CreateEnrollmentGroupRequest{ + HubIds: eg.GetHubIds(), + AttestationMechanism: eg.GetAttestationMechanism(), + PreSharedKey: eg.GetPreSharedKey(), + Name: eg.GetName(), + }, + }, + want: &pb.EnrollmentGroup{ + HubIds: eg.GetHubIds(), + Owner: eg.GetOwner(), + AttestationMechanism: eg.GetAttestationMechanism(), + PreSharedKey: eg.GetPreSharedKey(), + Name: eg.GetName(), + }, + }, + { + name: "valid-certificateChain-data", + args: args{ + req: &pb.CreateEnrollmentGroupRequest{ + HubIds: eg.GetHubIds(), + AttestationMechanism: &pb.AttestationMechanism{ + X509: &pb.X509Configuration{ + CertificateChain: uriSchemeDataCertificateChain, + }, + }, + PreSharedKey: uriSchemeDataPreSharedKey, + Name: eg.GetName(), + }, + }, + want: &pb.EnrollmentGroup{ + HubIds: eg.GetHubIds(), + AttestationMechanism: &pb.AttestationMechanism{ + X509: &pb.X509Configuration{ + CertificateChain: uriSchemeDataCertificateChain, + LeadCertificateName: eg.GetAttestationMechanism().GetX509().GetLeadCertificateName(), + }, + }, + PreSharedKey: uriSchemeDataPreSharedKey, + Name: eg.GetName(), + Owner: eg.GetOwner(), + }, + }, + } + ch := new(inprocgrpc.Channel) + + store, closeStore := test.NewMongoStore(t) + defer closeStore() + pb.RegisterDeviceProvisionServiceServer(ch, grpc.NewDeviceProvisionServiceServer(store, test.MakeAuthorizationConfig().OwnerClaim)) + grpcClient := pb.NewDeviceProvisionServiceClient(ch) + + ctx := kitNetGrpc.CtxWithToken(context.Background(), config.CreateJwtToken(t, jwt.MapClaims{ + "sub": test.DPSOwner, + })) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := grpcClient.CreateEnrollmentGroup(ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotEmpty(t, got.GetId()) + got.Id = "" + hubTest.CheckProtobufs(t, tt.want, got, hubTest.RequireToCheckFunc(require.Equal)) + }) + } +} diff --git a/device-provisioning-service/service/grpc/createHub.go b/device-provisioning-service/service/grpc/createHub.go new file mode 100644 index 000000000..35b8f0580 --- /dev/null +++ b/device-provisioning-service/service/grpc/createHub.go @@ -0,0 +1,33 @@ +package grpc + +import ( + "context" + + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (d *DeviceProvisionServiceServer) CreateHub(ctx context.Context, req *pb.CreateHubRequest) (*pb.Hub, error) { + owner, err := grpc.OwnerFromTokenMD(ctx, d.ownerClaim) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, "cannot get owner: %v", err) + } + + h := &pb.Hub{ + Id: uuid.NewString(), + HubId: req.GetHubId(), + Gateways: req.GetGateways(), + CertificateAuthority: req.GetCertificateAuthority(), + Authorization: req.GetAuthorization(), + Name: req.GetName(), + Owner: owner, + } + err = d.store.CreateHub(ctx, owner, h) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "hub('%v'): %v", req.GetHubId(), err) + } + return h, nil +} diff --git a/device-provisioning-service/service/grpc/createHub_test.go b/device-provisioning-service/service/grpc/createHub_test.go new file mode 100644 index 000000000..3b9e23a90 --- /dev/null +++ b/device-provisioning-service/service/grpc/createHub_test.go @@ -0,0 +1,76 @@ +package grpc_test + +import ( + "context" + "testing" + + "github.com/fullstorydev/grpchan/inprocgrpc" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service/grpc" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestDeviceProvisionServiceServerCreateHub(t *testing.T) { + h := test.NewHub(uuid.NewString(), test.DPSOwner) + type args struct { + req *pb.CreateHubRequest + } + tests := []struct { + name string + args args + want *pb.Hub + wantErr bool + }{ + { + name: "invalidID", + args: args{ + req: &pb.CreateHubRequest{}, + }, + wantErr: true, + }, + { + name: "valid", + args: args{ + req: &pb.CreateHubRequest{ + HubId: h.GetHubId(), + Gateways: h.GetGateways(), + CertificateAuthority: h.GetCertificateAuthority(), + Authorization: h.GetAuthorization(), + Name: h.GetName(), + }, + }, + want: h, + }, + } + ch := new(inprocgrpc.Channel) + + store, closeStore := test.NewMongoStore(t) + defer closeStore() + pb.RegisterDeviceProvisionServiceServer(ch, grpc.NewDeviceProvisionServiceServer(store, test.MakeAuthorizationConfig().OwnerClaim)) + grpcClient := pb.NewDeviceProvisionServiceClient(ch) + + ctx := kitNetGrpc.CtxWithToken(context.Background(), config.CreateJwtToken(t, jwt.MapClaims{ + "sub": test.DPSOwner, + })) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := grpcClient.CreateHub(ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotEmpty(t, got.GetId()) + require.NotEqual(t, tt.want.GetId(), got.GetId()) + got.Id = tt.want.GetId() + hubTest.CheckProtobufs(t, tt.want, got, hubTest.RequireToCheckFunc(require.Equal)) + }) + } +} diff --git a/device-provisioning-service/service/grpc/deleteEnrollmentGroup_test.go b/device-provisioning-service/service/grpc/deleteEnrollmentGroup_test.go new file mode 100644 index 000000000..0ac7eacde --- /dev/null +++ b/device-provisioning-service/service/grpc/deleteEnrollmentGroup_test.go @@ -0,0 +1,62 @@ +package grpc_test + +import ( + "context" + "testing" + + "github.com/fullstorydev/grpchan/inprocgrpc" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service/grpc" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestDeviceProvisionServiceServerDeleteEnrollmentGroups(t *testing.T) { + store, closeStore := test.NewMongoStore(t) + defer closeStore() + eg := test.NewEnrollmentGroup(t, uuid.NewString(), test.DPSOwner) + err := store.CreateEnrollmentGroup(context.Background(), test.DPSOwner, eg) + require.NoError(t, err) + + type args struct { + req *pb.DeleteEnrollmentGroupsRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "valid", + args: args{ + req: &pb.DeleteEnrollmentGroupsRequest{ + IdFilter: []string{eg.GetId()}, + }, + }, + }, + } + + ch := new(inprocgrpc.Channel) + pb.RegisterDeviceProvisionServiceServer(ch, grpc.NewDeviceProvisionServiceServer(store, test.MakeAuthorizationConfig().OwnerClaim)) + grpcClient := pb.NewDeviceProvisionServiceClient(ch) + + ctx := kitNetGrpc.CtxWithToken(context.Background(), config.CreateJwtToken(t, jwt.MapClaims{ + "sub": test.DPSOwner, + })) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := grpcClient.DeleteEnrollmentGroups(ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + }) + } +} diff --git a/device-provisioning-service/service/grpc/deleteEnrollmentGroups.go b/device-provisioning-service/service/grpc/deleteEnrollmentGroups.go new file mode 100644 index 000000000..dec00566d --- /dev/null +++ b/device-provisioning-service/service/grpc/deleteEnrollmentGroups.go @@ -0,0 +1,24 @@ +package grpc + +import ( + "context" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (d *DeviceProvisionServiceServer) DeleteEnrollmentGroups(ctx context.Context, req *pb.DeleteEnrollmentGroupsRequest) (*pb.DeleteEnrollmentGroupsResponse, error) { + owner, err := grpc.OwnerFromTokenMD(ctx, d.ownerClaim) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, "cannot get owner: %v", err) + } + count, err := d.store.DeleteEnrollmentGroups(ctx, owner, &pb.GetEnrollmentGroupsRequest{IdFilter: req.GetIdFilter()}) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "enrollment group('%v'): %v", req.GetIdFilter(), err) + } + return &pb.DeleteEnrollmentGroupsResponse{ + Count: count, + }, nil +} diff --git a/device-provisioning-service/service/grpc/deleteHubs.go b/device-provisioning-service/service/grpc/deleteHubs.go new file mode 100644 index 000000000..332c5c0f6 --- /dev/null +++ b/device-provisioning-service/service/grpc/deleteHubs.go @@ -0,0 +1,24 @@ +package grpc + +import ( + "context" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (d *DeviceProvisionServiceServer) DeleteHubs(ctx context.Context, req *pb.DeleteHubsRequest) (*pb.DeleteHubsResponse, error) { + owner, err := grpc.OwnerFromTokenMD(ctx, d.ownerClaim) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, "cannot get owner: %v", err) + } + count, err := d.store.DeleteHubs(ctx, owner, &pb.GetHubsRequest{IdFilter: req.GetIdFilter()}) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "hub('%v'): %v", req.GetIdFilter(), err) + } + return &pb.DeleteHubsResponse{ + Count: count, + }, nil +} diff --git a/device-provisioning-service/service/grpc/deleteHubs_test.go b/device-provisioning-service/service/grpc/deleteHubs_test.go new file mode 100644 index 000000000..1cbc30aca --- /dev/null +++ b/device-provisioning-service/service/grpc/deleteHubs_test.go @@ -0,0 +1,61 @@ +package grpc_test + +import ( + "context" + "testing" + + "github.com/fullstorydev/grpchan/inprocgrpc" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service/grpc" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestDeviceProvisionServiceServerDeleteHubs(t *testing.T) { + h := test.NewHub(uuid.NewString(), test.DPSOwner) + ch := new(inprocgrpc.Channel) + store, closeStore := test.NewMongoStore(t) + defer closeStore() + pb.RegisterDeviceProvisionServiceServer(ch, grpc.NewDeviceProvisionServiceServer(store, test.MakeAuthorizationConfig().OwnerClaim)) + grpcClient := pb.NewDeviceProvisionServiceClient(ch) + + err := store.CreateHub(context.Background(), h.GetOwner(), h) + require.NoError(t, err) + type args struct { + req *pb.DeleteHubsRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "valid", + args: args{ + req: &pb.DeleteHubsRequest{ + IdFilter: []string{h.GetId()}, + }, + }, + }, + } + + ctx := kitNetGrpc.CtxWithToken(context.Background(), config.CreateJwtToken(t, jwt.MapClaims{ + "sub": test.DPSOwner, + })) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := grpcClient.DeleteHubs(ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + }) + } +} diff --git a/device-provisioning-service/service/grpc/deleteProvisioningRecords.go b/device-provisioning-service/service/grpc/deleteProvisioningRecords.go new file mode 100644 index 000000000..7dc6f7c8e --- /dev/null +++ b/device-provisioning-service/service/grpc/deleteProvisioningRecords.go @@ -0,0 +1,28 @@ +package grpc + +import ( + "context" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (d *DeviceProvisionServiceServer) DeleteProvisioningRecords(ctx context.Context, req *pb.DeleteProvisioningRecordsRequest) (*pb.DeleteProvisioningRecordsResponse, error) { + owner, err := grpc.OwnerFromTokenMD(ctx, d.ownerClaim) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, "cannot get owner: %v", err) + } + count, err := d.store.DeleteProvisioningRecords(ctx, owner, &pb.GetProvisioningRecordsRequest{ + IdFilter: req.GetIdFilter(), + DeviceIdFilter: req.GetDeviceIdFilter(), + EnrollmentGroupIdFilter: req.GetEnrollmentGroupIdFilter(), + }) + if err != nil { + return nil, err + } + return &pb.DeleteProvisioningRecordsResponse{ + Count: count, + }, nil +} diff --git a/device-provisioning-service/service/grpc/deleteProvisioningRecords_test.go b/device-provisioning-service/service/grpc/deleteProvisioningRecords_test.go new file mode 100644 index 000000000..2989fabcc --- /dev/null +++ b/device-provisioning-service/service/grpc/deleteProvisioningRecords_test.go @@ -0,0 +1,81 @@ +package grpc_test + +import ( + "context" + "testing" + + "github.com/fullstorydev/grpchan/inprocgrpc" + "github.com/golang-jwt/jwt/v5" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service/grpc" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +const ( + provisionRecordID = "mfgID" +) + +func TestDeviceProvisionServiceServerDeleteProvisioningRecords(t *testing.T) { + type args struct { + req *pb.DeleteProvisioningRecordsRequest + } + tests := []struct { + name string + args args + want *pb.DeleteProvisioningRecordsResponse + }{ + { + name: "invalidID", + args: args{ + req: &pb.DeleteProvisioningRecordsRequest{ + IdFilter: []string{"invalidID"}, + }, + }, + want: &pb.DeleteProvisioningRecordsResponse{ + Count: 0, + }, + }, + { + name: "valid", + args: args{ + req: &pb.DeleteProvisioningRecordsRequest{ + IdFilter: []string{provisionRecordID}, + }, + }, + want: &pb.DeleteProvisioningRecordsResponse{ + Count: 1, + }, + }, + } + + store, closeStore := test.NewMongoStore(t) + defer closeStore() + + err := store.UpdateProvisioningRecord(context.Background(), test.DPSOwner, &pb.ProvisioningRecord{ + Id: provisionRecordID, + Owner: test.DPSOwner, + }) + require.NoError(t, err) + err = store.FlushBulkWriter() + require.NoError(t, err) + + ch := new(inprocgrpc.Channel) + pb.RegisterDeviceProvisionServiceServer(ch, grpc.NewDeviceProvisionServiceServer(store, test.MakeAuthorizationConfig().OwnerClaim)) + grpcClient := pb.NewDeviceProvisionServiceClient(ch) + + ctx := kitNetGrpc.CtxWithToken(context.Background(), config.CreateJwtToken(t, jwt.MapClaims{ + "sub": test.DPSOwner, + })) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := grpcClient.DeleteProvisioningRecords(ctx, tt.args.req) + require.NoError(t, err) + hubTest.CheckProtobufs(t, tt.want, resp, hubTest.RequireToCheckFunc(require.Equal)) + }) + } +} diff --git a/device-provisioning-service/service/grpc/getEnrollmentGroups.go b/device-provisioning-service/service/grpc/getEnrollmentGroups.go new file mode 100644 index 000000000..e2e037c68 --- /dev/null +++ b/device-provisioning-service/service/grpc/getEnrollmentGroups.go @@ -0,0 +1,29 @@ +package grpc + +import ( + "context" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (d *DeviceProvisionServiceServer) GetEnrollmentGroups(req *pb.GetEnrollmentGroupsRequest, srv pb.DeviceProvisionService_GetEnrollmentGroupsServer) error { + owner, err := grpc.OwnerFromTokenMD(srv.Context(), d.ownerClaim) + if err != nil { + return status.Errorf(codes.Unauthenticated, "cannot get owner: %v", err) + } + return d.store.LoadEnrollmentGroups(srv.Context(), owner, req, func(ctx context.Context, iter store.EnrollmentGroupIter) (err error) { + for { + var g pb.EnrollmentGroup + if ok := iter.Next(ctx, &g); !ok { + return iter.Err() + } + if err = srv.Send(&g); err != nil { + return err + } + } + }) +} diff --git a/device-provisioning-service/service/grpc/getEnrollmentGroups_test.go b/device-provisioning-service/service/grpc/getEnrollmentGroups_test.go new file mode 100644 index 000000000..601ae9a85 --- /dev/null +++ b/device-provisioning-service/service/grpc/getEnrollmentGroups_test.go @@ -0,0 +1,91 @@ +package grpc_test + +import ( + "context" + "errors" + "io" + "testing" + + "github.com/fullstorydev/grpchan/inprocgrpc" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service/grpc" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestDeviceProvisionServiceServerGetEnrollmentGroups(t *testing.T) { + eg := test.NewEnrollmentGroup(t, uuid.NewString(), test.DPSOwner) + type args struct { + req *pb.GetEnrollmentGroupsRequest + } + tests := []struct { + name string + args args + want pb.EnrollmentGroups + wantErr bool + }{ + { + name: "invalidID", + args: args{ + req: &pb.GetEnrollmentGroupsRequest{ + IdFilter: []string{"invalidID"}, + }, + }, + wantErr: true, + }, + { + name: "valid", + args: args{ + req: &pb.GetEnrollmentGroupsRequest{ + IdFilter: []string{eg.GetId()}, + }, + }, + want: []*pb.EnrollmentGroup{eg}, + }, + } + + store, closeStore := test.NewMongoStore(t) + defer closeStore() + + err := store.CreateEnrollmentGroup(context.Background(), eg.GetOwner(), eg) + require.NoError(t, err) + + ch := new(inprocgrpc.Channel) + pb.RegisterDeviceProvisionServiceServer(ch, grpc.NewDeviceProvisionServiceServer(store, test.MakeAuthorizationConfig().OwnerClaim)) + grpcClient := pb.NewDeviceProvisionServiceClient(ch) + + ctx := kitNetGrpc.CtxWithToken(context.Background(), config.CreateJwtToken(t, jwt.MapClaims{ + "sub": test.DPSOwner, + })) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := grpcClient.GetEnrollmentGroups(ctx, tt.args.req) + require.NoError(t, err) + var got pb.EnrollmentGroups + for { + r, err := client.Recv() + if errors.Is(err, io.EOF) { + break + } + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + got = append(got, r) + } + require.Equal(t, len(tt.want), len(got)) + tt.want.Sort() + got.Sort() + for i := range got { + hubTest.CheckProtobufs(t, tt.want[i], got[i], hubTest.RequireToCheckFunc(require.Equal)) + } + }) + } +} diff --git a/device-provisioning-service/service/grpc/getHubs.go b/device-provisioning-service/service/grpc/getHubs.go new file mode 100644 index 000000000..b39724cc8 --- /dev/null +++ b/device-provisioning-service/service/grpc/getHubs.go @@ -0,0 +1,29 @@ +package grpc + +import ( + "context" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (d *DeviceProvisionServiceServer) GetHubs(req *pb.GetHubsRequest, srv pb.DeviceProvisionService_GetHubsServer) error { + owner, err := grpc.OwnerFromTokenMD(srv.Context(), d.ownerClaim) + if err != nil { + return status.Errorf(codes.Unauthenticated, "cannot get owner: %v", err) + } + return d.store.LoadHubs(srv.Context(), owner, req, func(ctx context.Context, iter store.HubIter) (err error) { + for { + var g pb.Hub + if ok := iter.Next(ctx, &g); !ok { + return iter.Err() + } + if err = srv.Send(&g); err != nil { + return err + } + } + }) +} diff --git a/device-provisioning-service/service/grpc/getHubs_test.go b/device-provisioning-service/service/grpc/getHubs_test.go new file mode 100644 index 000000000..bcb5b8016 --- /dev/null +++ b/device-provisioning-service/service/grpc/getHubs_test.go @@ -0,0 +1,91 @@ +package grpc_test + +import ( + "context" + "errors" + "io" + "testing" + + "github.com/fullstorydev/grpchan/inprocgrpc" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service/grpc" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestDeviceProvisionServiceServerGetHubs(t *testing.T) { + h := test.NewHub(uuid.NewString(), test.DPSOwner) + type args struct { + req *pb.GetHubsRequest + } + tests := []struct { + name string + args args + want pb.Hubs + wantErr bool + }{ + { + name: "invalidID", + args: args{ + req: &pb.GetHubsRequest{ + IdFilter: []string{"invalidID"}, + }, + }, + wantErr: true, + }, + { + name: "valid", + args: args{ + req: &pb.GetHubsRequest{ + IdFilter: []string{h.GetId()}, + }, + }, + want: []*pb.Hub{h}, + }, + } + + store, closeStore := test.NewMongoStore(t) + defer closeStore() + + err := store.CreateHub(context.Background(), h.GetOwner(), h) + require.NoError(t, err) + + ch := new(inprocgrpc.Channel) + pb.RegisterDeviceProvisionServiceServer(ch, grpc.NewDeviceProvisionServiceServer(store, test.MakeAuthorizationConfig().OwnerClaim)) + grpcClient := pb.NewDeviceProvisionServiceClient(ch) + + ctx := kitNetGrpc.CtxWithToken(context.Background(), config.CreateJwtToken(t, jwt.MapClaims{ + "sub": test.DPSOwner, + })) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := grpcClient.GetHubs(ctx, tt.args.req) + require.NoError(t, err) + var got pb.Hubs + for { + r, err := client.Recv() + if errors.Is(err, io.EOF) { + break + } + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + got = append(got, r) + } + require.Equal(t, len(tt.want), len(got)) + tt.want.Sort() + got.Sort() + for i := range got { + hubTest.CheckProtobufs(t, tt.want[i], got[i], hubTest.RequireToCheckFunc(require.Equal)) + } + }) + } +} diff --git a/device-provisioning-service/service/grpc/getProvisioningRecords.go b/device-provisioning-service/service/grpc/getProvisioningRecords.go new file mode 100644 index 000000000..458317f53 --- /dev/null +++ b/device-provisioning-service/service/grpc/getProvisioningRecords.go @@ -0,0 +1,29 @@ +package grpc + +import ( + "context" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (d *DeviceProvisionServiceServer) GetProvisioningRecords(req *pb.GetProvisioningRecordsRequest, srv pb.DeviceProvisionService_GetProvisioningRecordsServer) error { + owner, err := grpc.OwnerFromTokenMD(srv.Context(), d.ownerClaim) + if err != nil { + return status.Errorf(codes.Unauthenticated, "cannot get owner: %v", err) + } + return d.store.LoadProvisioningRecords(srv.Context(), owner, req, func(ctx context.Context, iter store.ProvisioningRecordIter) (err error) { + for { + var sub pb.ProvisioningRecord + if ok := iter.Next(ctx, &sub); !ok { + return iter.Err() + } + if err = srv.Send(&sub); err != nil { + return err + } + } + }) +} diff --git a/device-provisioning-service/service/grpc/getProvisioningRecords_test.go b/device-provisioning-service/service/grpc/getProvisioningRecords_test.go new file mode 100644 index 000000000..7a98df9a1 --- /dev/null +++ b/device-provisioning-service/service/grpc/getProvisioningRecords_test.go @@ -0,0 +1,108 @@ +package grpc_test + +import ( + "context" + "errors" + "io" + "testing" + + "github.com/fullstorydev/grpchan/inprocgrpc" + "github.com/golang-jwt/jwt/v5" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service/grpc" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestDeviceProvisionServiceServerGetProvisioningRecords(t *testing.T) { + type args struct { + req *pb.GetProvisioningRecordsRequest + } + tests := []struct { + name string + args args + want pb.ProvisioningRecords + wantErr bool + }{ + { + name: "invalidID", + args: args{ + req: &pb.GetProvisioningRecordsRequest{ + IdFilter: []string{"invalidID"}, + }, + }, + wantErr: true, + }, + { + name: "valid", + args: args{ + req: &pb.GetProvisioningRecordsRequest{ + IdFilter: []string{provisionRecordID}, + }, + }, + want: []*pb.ProvisioningRecord{{Id: provisionRecordID, Owner: test.DPSOwner}}, + }, + { + name: "all", + args: args{ + req: &pb.GetProvisioningRecordsRequest{}, + }, + want: []*pb.ProvisioningRecord{{Id: provisionRecordID, Owner: test.DPSOwner}}, + }, + } + + store, closeStore := test.NewMongoStore(t) + defer closeStore() + + err := store.UpdateProvisioningRecord(context.Background(), test.DPSOwner, &pb.ProvisioningRecord{ + Id: provisionRecordID, + Owner: test.DPSOwner, + }) + require.NoError(t, err) + err = store.UpdateProvisioningRecord(context.Background(), "anotherOwner", &pb.ProvisioningRecord{ + Id: "anotherID", + Owner: "anotherOwner", + }) + require.NoError(t, err) + err = store.FlushBulkWriter() + require.NoError(t, err) + + ch := new(inprocgrpc.Channel) + pb.RegisterDeviceProvisionServiceServer(ch, grpc.NewDeviceProvisionServiceServer(store, test.MakeAuthorizationConfig().OwnerClaim)) + grpcClient := pb.NewDeviceProvisionServiceClient(ch) + + ctx := kitNetGrpc.CtxWithToken(context.Background(), config.CreateJwtToken(t, jwt.MapClaims{ + "sub": test.DPSOwner, + })) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := grpcClient.GetProvisioningRecords(ctx, tt.args.req) + require.NoError(t, err) + var got pb.ProvisioningRecords + for { + r, err := client.Recv() + if errors.Is(err, io.EOF) { + break + } + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Greater(t, r.GetCreationDate(), int64(0)) + r.CreationDate = 0 + got = append(got, r) + } + require.Equal(t, len(tt.want), len(got)) + tt.want.Sort() + got.Sort() + for i := range got { + hubTest.CheckProtobufs(t, tt.want[i], got[i], hubTest.RequireToCheckFunc(require.Equal)) + } + }) + } +} diff --git a/device-provisioning-service/service/grpc/server.go b/device-provisioning-service/service/grpc/server.go new file mode 100644 index 000000000..c155110fe --- /dev/null +++ b/device-provisioning-service/service/grpc/server.go @@ -0,0 +1,20 @@ +package grpc + +import ( + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" +) + +type DeviceProvisionServiceServer struct { + store store.Store + ownerClaim string + + pb.UnimplementedDeviceProvisionServiceServer +} + +func NewDeviceProvisionServiceServer(store store.Store, ownerClaim string) *DeviceProvisionServiceServer { + return &DeviceProvisionServiceServer{ + store: store, + ownerClaim: ownerClaim, + } +} diff --git a/device-provisioning-service/service/grpc/updateEnrollmentGroup.go b/device-provisioning-service/service/grpc/updateEnrollmentGroup.go new file mode 100644 index 000000000..d6a5a3f9b --- /dev/null +++ b/device-provisioning-service/service/grpc/updateEnrollmentGroup.go @@ -0,0 +1,67 @@ +package grpc + +import ( + "context" + "errors" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "go.mongodb.org/mongo-driver/mongo" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const ( + errEnrollmentGroupFmt = "enrollment group('%v'): %v" + errEnrollmentGroupNotFoundFmt = "enrollment group('%v'): not found" +) + +func (d *DeviceProvisionServiceServer) loadEnrollmentGroup(ctx context.Context, owner, id string) (*pb.EnrollmentGroup, error) { + var res pb.EnrollmentGroup + var ok bool + err := d.store.LoadEnrollmentGroups(ctx, owner, &pb.GetEnrollmentGroupsRequest{IdFilter: []string{id}}, func(ctx context.Context, iter store.EnrollmentGroupIter) (err error) { + ok = iter.Next(ctx, &res) + return iter.Err() + }) + if err != nil { + if errors.Is(err, mongo.ErrNilDocument) { + return nil, status.Errorf(codes.NotFound, errEnrollmentGroupNotFoundFmt, id) + } + return nil, status.Errorf(codes.InvalidArgument, errEnrollmentGroupFmt, id, err) + } + if !ok { + return nil, status.Errorf(codes.NotFound, errEnrollmentGroupNotFoundFmt, id) + } + return &res, nil +} + +func (d *DeviceProvisionServiceServer) UpdateEnrollmentGroup(ctx context.Context, req *pb.UpdateEnrollmentGroupRequest) (*pb.EnrollmentGroup, error) { + x509Attestation := req.GetEnrollmentGroup().GetAttestationMechanism().GetX509() + if x509Attestation != nil { + err := x509Attestation.Validate() + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, errEnrollmentGroupFmt, req.GetId(), err) + } + } + owner, err := grpc.OwnerFromTokenMD(ctx, d.ownerClaim) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, "cannot get owner: %v", err) + } + + err = d.store.UpdateEnrollmentGroup(ctx, owner, &pb.EnrollmentGroup{ + Id: req.GetId(), + Owner: owner, + AttestationMechanism: req.GetEnrollmentGroup().GetAttestationMechanism(), + HubIds: req.GetEnrollmentGroup().GetHubIds(), + PreSharedKey: req.GetEnrollmentGroup().GetPreSharedKey(), + Name: req.GetEnrollmentGroup().GetName(), + }) + if err != nil { + if errors.Is(err, mongo.ErrNilDocument) { + return nil, status.Errorf(codes.NotFound, errEnrollmentGroupNotFoundFmt, req.GetId()) + } + return nil, status.Errorf(codes.InvalidArgument, errEnrollmentGroupFmt, req.GetId(), err) + } + return d.loadEnrollmentGroup(ctx, owner, req.GetId()) +} diff --git a/device-provisioning-service/service/grpc/updateEnrollmentGroup_test.go b/device-provisioning-service/service/grpc/updateEnrollmentGroup_test.go new file mode 100644 index 000000000..215017a44 --- /dev/null +++ b/device-provisioning-service/service/grpc/updateEnrollmentGroup_test.go @@ -0,0 +1,83 @@ +package grpc_test + +import ( + "context" + "testing" + + "github.com/fullstorydev/grpchan/inprocgrpc" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service/grpc" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestDeviceProvisionServiceServerUpdateEnrollmentGroup(t *testing.T) { + store, closeStore := test.NewMongoStore(t) + defer closeStore() + eg := test.NewEnrollmentGroup(t, uuid.NewString(), test.DPSOwner) + err := store.CreateEnrollmentGroup(context.Background(), eg.GetOwner(), eg) + require.NoError(t, err) + + egUpd := eg + egUpd.PreSharedKey = "" + egUpd.Name = "newName" + type args struct { + req *pb.UpdateEnrollmentGroupRequest + } + tests := []struct { + name string + args args + wantErr bool + want *pb.EnrollmentGroup + }{ + { + name: "valid", + args: args{ + req: &pb.UpdateEnrollmentGroupRequest{ + Id: eg.GetId(), + EnrollmentGroup: &pb.UpdateEnrollmentGroup{ + AttestationMechanism: egUpd.GetAttestationMechanism(), + HubIds: eg.GetHubIds(), + PreSharedKey: egUpd.GetPreSharedKey(), + Name: egUpd.GetName(), + }, + }, + }, + want: egUpd, + }, + { + name: "invalid", + args: args{ + req: &pb.UpdateEnrollmentGroupRequest{ + Id: "invalid", + }, + }, + wantErr: true, + }, + } + + ch := new(inprocgrpc.Channel) + pb.RegisterDeviceProvisionServiceServer(ch, grpc.NewDeviceProvisionServiceServer(store, test.MakeAuthorizationConfig().OwnerClaim)) + grpcClient := pb.NewDeviceProvisionServiceClient(ch) + + ctx := kitNetGrpc.CtxWithToken(context.Background(), config.CreateJwtToken(t, jwt.MapClaims{ + "sub": test.DPSOwner, + })) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := grpcClient.UpdateEnrollmentGroup(ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + hubTest.CheckProtobufs(t, tt.want, got, hubTest.RequireToCheckFunc(require.Equal)) + }) + } +} diff --git a/device-provisioning-service/service/grpc/updateHub.go b/device-provisioning-service/service/grpc/updateHub.go new file mode 100644 index 000000000..fc4f1d481 --- /dev/null +++ b/device-provisioning-service/service/grpc/updateHub.go @@ -0,0 +1,58 @@ +package grpc + +import ( + "context" + "errors" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "go.mongodb.org/mongo-driver/mongo" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const errHubNotFoundFmt = "hub('%v'): not found" + +func (d *DeviceProvisionServiceServer) loadHub(ctx context.Context, owner string, id string) (*pb.Hub, error) { + var res pb.Hub + var ok bool + err := d.store.LoadHubs(ctx, owner, &pb.GetHubsRequest{IdFilter: []string{id}}, func(ctx context.Context, iter store.HubIter) (err error) { + ok = iter.Next(ctx, &res) + return iter.Err() + }) + if err != nil { + if errors.Is(err, mongo.ErrNilDocument) { + return nil, status.Errorf(codes.NotFound, errHubNotFoundFmt, id) + } + return nil, status.Errorf(codes.InvalidArgument, "hub('%v'): %v", id, err) + } + if !ok { + return nil, status.Errorf(codes.NotFound, errHubNotFoundFmt, id) + } + return &res, nil +} + +func (d *DeviceProvisionServiceServer) UpdateHub(ctx context.Context, req *pb.UpdateHubRequest) (*pb.Hub, error) { + owner, err := grpc.OwnerFromTokenMD(ctx, d.ownerClaim) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, "cannot get owner: %v", err) + } + + err = d.store.UpdateHub(ctx, owner, &pb.Hub{ + Id: req.GetId(), + HubId: req.GetHub().GetHubId(), + Gateways: req.GetHub().GetGateways(), + CertificateAuthority: req.GetHub().GetCertificateAuthority(), + Authorization: req.GetHub().GetAuthorization(), + Name: req.GetHub().GetName(), + Owner: owner, + }) + if err != nil { + if errors.Is(err, mongo.ErrNilDocument) { + return nil, status.Errorf(codes.NotFound, errHubNotFoundFmt, req.GetId()) + } + return nil, status.Errorf(codes.InvalidArgument, "hub('%v'): %v", req.GetId(), err) + } + return d.loadHub(ctx, owner, req.GetId()) +} diff --git a/device-provisioning-service/service/grpc/updateHub_test.go b/device-provisioning-service/service/grpc/updateHub_test.go new file mode 100644 index 000000000..f49187167 --- /dev/null +++ b/device-provisioning-service/service/grpc/updateHub_test.go @@ -0,0 +1,82 @@ +package grpc_test + +import ( + "context" + "testing" + + "github.com/fullstorydev/grpchan/inprocgrpc" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service/grpc" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestDeviceProvisionServiceServerUpdateHub(t *testing.T) { + store, closeStore := test.NewMongoStore(t) + defer closeStore() + h := test.NewHub(uuid.NewString(), test.DPSOwner) + err := store.CreateHub(context.Background(), h.GetOwner(), h) + require.NoError(t, err) + hUpd := h + hUpd.Gateways = []string{"coaps://123"} + hUpd.Name = "new name" + + type args struct { + req *pb.UpdateHubRequest + } + tests := []struct { + name string + args args + wantErr bool + want *pb.Hub + }{ + { + name: "valid", + args: args{ + req: &pb.UpdateHubRequest{ + Id: hUpd.GetId(), + Hub: &pb.UpdateHub{ + Gateways: hUpd.GetGateways(), + CertificateAuthority: hUpd.GetCertificateAuthority(), + Authorization: hUpd.GetAuthorization(), + Name: hUpd.GetName(), + }, + }, + }, + want: hUpd, + }, + { + name: "invalid", + args: args{ + req: &pb.UpdateHubRequest{ + Id: "invalid", + }, + }, + wantErr: true, + }, + } + ch := new(inprocgrpc.Channel) + pb.RegisterDeviceProvisionServiceServer(ch, grpc.NewDeviceProvisionServiceServer(store, test.MakeAuthorizationConfig().OwnerClaim)) + grpcClient := pb.NewDeviceProvisionServiceClient(ch) + + ctx := kitNetGrpc.CtxWithToken(context.Background(), config.CreateJwtToken(t, jwt.MapClaims{ + "sub": test.DPSOwner, + })) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := grpcClient.UpdateHub(ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + hubTest.CheckProtobufs(t, tt.want, got, hubTest.RequireToCheckFunc(require.Equal)) + }) + } +} diff --git a/device-provisioning-service/service/http/config.go b/device-provisioning-service/service/http/config.go new file mode 100644 index 000000000..bdcedd2e3 --- /dev/null +++ b/device-provisioning-service/service/http/config.go @@ -0,0 +1,34 @@ +package http + +import ( + "fmt" + + "github.com/plgd-dev/hub/v2/pkg/net/http/server" + "github.com/plgd-dev/hub/v2/pkg/net/listener" + "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" +) + +type AuthorizationConfig struct { + OwnerClaim string `yaml:"ownerClaim" json:"ownerClaim"` + validator.Config `yaml:",inline" json:",inline"` +} + +func (c *AuthorizationConfig) Validate() error { + if c.OwnerClaim == "" { + return fmt.Errorf("ownerClaim('%v')", c.OwnerClaim) + } + return c.Config.Validate() +} + +type Config struct { + Connection listener.Config `yaml:",inline" json:",inline"` + Authorization AuthorizationConfig `yaml:"authorization" json:"authorization"` + Server server.Config `yaml:",inline" json:",inline"` +} + +func (c *Config) Validate() error { + if err := c.Authorization.Validate(); err != nil { + return fmt.Errorf("authorization.%w", err) + } + return c.Connection.Validate() +} diff --git a/device-provisioning-service/service/http/getRegistrations_test.go b/device-provisioning-service/service/http/getRegistrations_test.go new file mode 100644 index 000000000..505d09536 --- /dev/null +++ b/device-provisioning-service/service/http/getRegistrations_test.go @@ -0,0 +1,126 @@ +package http_test + +import ( + "context" + "errors" + "io" + "net/http" + "testing" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + httpService "github.com/plgd-dev/hub/v2/device-provisioning-service/service/http" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" + hubTest "github.com/plgd-dev/hub/v2/test" + httpTest "github.com/plgd-dev/hub/v2/test/http" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDeviceProvisionServiceServerGetProvisioningRecords(t *testing.T) { + samples := pb.ProvisioningRecords{ + { + Id: "mfgID1", + EnrollmentGroupId: "eg1", + DeviceId: "d1", + Owner: test.DPSOwner, + }, { + Id: "mfgID2", + EnrollmentGroupId: "eg2", + DeviceId: "d2", + Owner: test.DPSOwner, + }, + } + type args struct { + accept string + idFilter []string + enrollmentGroupIDFilter []string + deviceIDFilter []string + } + tests := []struct { + name string + args args + want pb.ProvisioningRecords + wantErr bool + }{ + { + name: "invalidID", + args: args{ + idFilter: []string{"invalidID"}, + }, + wantErr: true, + }, + { + name: "filter by id", + args: args{ + idFilter: []string{samples[0].GetId()}, + }, + want: []*pb.ProvisioningRecord{samples[0]}, + }, + { + name: "filter by enrollmentGroupId", + args: args{ + enrollmentGroupIDFilter: []string{samples[1].GetEnrollmentGroupId()}, + }, + want: []*pb.ProvisioningRecord{samples[1]}, + }, + { + name: "filter by deviceId", + args: args{ + deviceIDFilter: []string{samples[1].GetDeviceId()}, + }, + want: []*pb.ProvisioningRecord{samples[1]}, + }, + } + + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesOAuth) + defer hubShutdown() + + store, closeStore := test.NewMongoStore(t) + defer closeStore() + + _, closeHTTP := test.NewHTTPService(context.Background(), t, store) + defer closeHTTP() + + for _, sample := range samples { + err := store.UpdateProvisioningRecord(context.Background(), test.DPSOwner, sample) + require.NoError(t, err) + } + err := store.FlushBulkWriter() + require.NoError(t, err) + + token := oauthTest.GetDefaultAccessToken(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + request := httpgwTest.NewRequest(http.MethodGet, httpService.ProvisioningRecords, nil). + Host(test.DPSHTTPHost).AuthToken(token).Accept(tt.args.accept).AddQuery(httpService.IDFilterQueryKey, tt.args.idFilter...). + AddQuery(httpService.EnrollmentGroupIDFilterQueryKey, tt.args.enrollmentGroupIDFilter...).AddQuery(httpService.DeviceIDFilterQueryKey, tt.args.deviceIDFilter...).Build() + resp := httpgwTest.HTTPDo(t, request) + defer func() { + _ = resp.Body.Close() + }() + + var got pb.ProvisioningRecords + for { + var dev pb.ProvisioningRecord + err := httpTest.Unmarshal(resp.StatusCode, resp.Body, &dev) + if errors.Is(err, io.EOF) { + break + } + require.NoError(t, err) + assert.NotEmpty(t, dev.GetCreationDate()) + dev.CreationDate = 0 + got = append(got, &dev) + } + require.Equal(t, len(tt.want), len(got)) + tt.want.Sort() + got.Sort() + for i := range got { + hubTest.CheckProtobufs(t, tt.want[i], got[i], hubTest.RequireToCheckFunc(require.Equal)) + } + }) + } +} diff --git a/device-provisioning-service/service/http/service.go b/device-provisioning-service/service/http/service.go new file mode 100644 index 000000000..fd05b7327 --- /dev/null +++ b/device-provisioning-service/service/http/service.go @@ -0,0 +1,87 @@ +package http + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/fullstorydev/grpchan/inprocgrpc" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service/grpc" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/http-gateway/serverMux" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + kitNetHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + "github.com/plgd-dev/hub/v2/pkg/net/listener" + "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" + "go.opentelemetry.io/otel/trace" +) + +type Service struct { + httpServer *http.Server + listener *listener.Server +} + +// New creates new HTTP service +func New(ctx context.Context, serviceName string, config Config, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider, store store.Store) (*Service, error) { + validator, err := validator.New(ctx, config.Authorization.Config, fileWatcher, logger, tracerProvider) + if err != nil { + return nil, fmt.Errorf("cannot create validator: %w", err) + } + listener, err := listener.New(config.Connection, fileWatcher, logger) + if err != nil { + validator.Close() + return nil, fmt.Errorf("cannot create grpc server: %w", err) + } + listener.AddCloseFunc(validator.Close) + + ch := new(inprocgrpc.Channel) + pb.RegisterDeviceProvisionServiceServer(ch, grpc.NewDeviceProvisionServiceServer(store, config.Authorization.OwnerClaim)) + grpcClient := pb.NewDeviceProvisionServiceClient(ch) + + auth := kitNetHttp.NewInterceptorWithValidator(validator, kitNetHttp.NewDefaultAuthorizationRules(APIV1)) + mux := serverMux.New() + r := serverMux.NewRouter(queryCaseInsensitive, auth) + + // register grpc-proxy handler + if err := pb.RegisterDeviceProvisionServiceHandlerClient(context.Background(), mux, grpcClient); err != nil { + return nil, fmt.Errorf("failed to register grpc-gateway handler: %w", err) + } + r.PathPrefix("/").Handler(mux) + httpServer := &http.Server{ + Handler: kitNetHttp.OpenTelemetryNewHandler(r, serviceName, tracerProvider), + ReadTimeout: config.Server.ReadTimeout, + ReadHeaderTimeout: config.Server.ReadHeaderTimeout, + WriteTimeout: config.Server.WriteTimeout, + IdleTimeout: config.Server.IdleTimeout, + } + + return &Service{ + httpServer: httpServer, + listener: listener, + }, nil +} + +// Serve starts the service's HTTP server and blocks +func (s *Service) Serve() error { + return s.httpServer.Serve(s.listener) +} + +// Shutdown ends serving +func (s *Service) Close() error { + return s.httpServer.Shutdown(context.Background()) +} + +const ( + IDFilterQueryKey = "idFilter" + EnrollmentGroupIDFilterQueryKey = "enrollmentGroupIdFilter" + DeviceIDFilterQueryKey = "deviceIdFilter" +) + +var queryCaseInsensitive = map[string]string{ + strings.ToLower(IDFilterQueryKey): IDFilterQueryKey, + strings.ToLower(EnrollmentGroupIDFilterQueryKey): EnrollmentGroupIDFilterQueryKey, + strings.ToLower(DeviceIDFilterQueryKey): DeviceIDFilterQueryKey, +} diff --git a/device-provisioning-service/service/http/uri.go b/device-provisioning-service/service/http/uri.go new file mode 100644 index 000000000..62591e28c --- /dev/null +++ b/device-provisioning-service/service/http/uri.go @@ -0,0 +1,9 @@ +package http + +// HTTP Service URIs. +const ( + API = "/api" + APIV1 = API + "/v1" + + ProvisioningRecords = APIV1 + "/provisioning-records" +) diff --git a/device-provisioning-service/service/linkedHub.go b/device-provisioning-service/service/linkedHub.go new file mode 100644 index 000000000..c6a8c2081 --- /dev/null +++ b/device-provisioning-service/service/linkedHub.go @@ -0,0 +1,108 @@ +package service + +import ( + "context" + "errors" + "time" + + pbCA "github.com/plgd-dev/hub/v2/certificate-authority/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/security/oauth/clientcredentials" + "github.com/plgd-dev/hub/v2/pkg/fn" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + "go.opentelemetry.io/otel/trace" + "go.uber.org/atomic" + "golang.org/x/oauth2" + "google.golang.org/grpc" +) + +type LinkedHub struct { + cfg *pb.Hub + expiration time.Duration + certificateAuthority pbCA.CertificateAuthorityClient + tokenCache *clientcredentials.Cache + closer fn.FuncList + invalid atomic.Bool + tokenExpireAt atomic.Time + expireAt atomic.Time +} + +func NewLinkedHub(ctx context.Context, expiration time.Duration, cfg *pb.Hub, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*LinkedHub, error) { + if cfg.GetId() == "" { + return nil, errors.New("hub ID is empty") + } + var closer fn.FuncList + certificateAuthority, certificateAuthorityClose, err := newCertificateAuthorityClient(cfg.GetCertificateAuthority().GetGrpc().ToConfig(), fileWatcher, logger, tracerProvider) + if err != nil { + closer.Execute() + return nil, err + } + closer.AddFunc(certificateAuthorityClose) + + tokenCache, err := clientcredentials.New(ctx, cfg.GetAuthorization().GetProvider().ToConfig(), fileWatcher, logger, tracerProvider, time.Minute) + if err != nil { + closer.Execute() + return nil, err + } + closer.AddFunc(tokenCache.Close) + + return &LinkedHub{ + cfg: cfg, + certificateAuthority: certificateAuthority, + tokenCache: tokenCache, + closer: closer, + expiration: expiration, + }, nil +} + +func (h *LinkedHub) SignIdentityCertificate(ctx context.Context, in *pbCA.SignCertificateRequest, opts ...grpc.CallOption) (*pbCA.SignCertificateResponse, error) { + v, err := h.certificateAuthority.SignIdentityCertificate(ctx, in, opts...) + if err == nil { + h.Refresh(time.Now()) + } + return v, err +} + +func (h *LinkedHub) GetToken(ctx context.Context, key string, urlValues map[string]string, requiredClaims map[string]interface{}) (*oauth2.Token, error) { + v, err := h.tokenCache.GetToken(ctx, key, urlValues, requiredClaims) + if err == nil { + h.Refresh(time.Now()) + h.tokenExpireAt.Store(v.Expiry) + } + return v, err +} + +func (h *LinkedHub) Invalidate() { + h.invalid.Store(true) +} + +func (h *LinkedHub) GetTokenFromOAuth(ctx context.Context, urlValues map[string]string, requiredClaims map[string]interface{}) (*oauth2.Token, error) { + t, err := h.tokenCache.GetTokenFromOAuth(ctx, urlValues, requiredClaims) + if err == nil { + h.Refresh(time.Now()) + } + return t, err +} + +func (h *LinkedHub) Refresh(now time.Time) { + h.expireAt.Store(now.Add(h.expiration)) +} + +func (h *LinkedHub) IsExpired(now time.Time) bool { + if h.invalid.Load() { + return true + } + if h.expireAt.Load().After(now) { + return true + } + tokenExpireAt := h.tokenExpireAt.Load() + if tokenExpireAt.IsZero() { + return false + } + return h.tokenExpireAt.Load().After(now) +} + +func (h *LinkedHub) Close() { + h.closer.Execute() +} diff --git a/device-provisioning-service/service/linkedHubCache.go b/device-provisioning-service/service/linkedHubCache.go new file mode 100644 index 000000000..72ef8fc31 --- /dev/null +++ b/device-provisioning-service/service/linkedHubCache.go @@ -0,0 +1,280 @@ +package service + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/hashicorp/go-multierror" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store/mongodb" + "github.com/plgd-dev/hub/v2/pkg/fn" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + "github.com/plgd-dev/hub/v2/pkg/sync/task/future" + "go.opentelemetry.io/otel/trace" +) + +type LinkedHubCache struct { + expiration time.Duration + store *mongodb.Store + logger log.Logger + fileWatcher *fsnotify.Watcher + wg *sync.WaitGroup + + hubs map[string]*future.Future + mutex sync.Mutex + ctx context.Context + cancel context.CancelFunc + + tracerProvider trace.TracerProvider +} + +func NewLinkedHubCache(ctx context.Context, expiration time.Duration, store *mongodb.Store, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) *LinkedHubCache { + ctx, cancel := context.WithCancel(ctx) + c := &LinkedHubCache{ + ctx: ctx, + cancel: cancel, + expiration: expiration, + hubs: make(map[string]*future.Future), + wg: new(sync.WaitGroup), + logger: logger, + tracerProvider: tracerProvider, + fileWatcher: fileWatcher, + store: store, + } + c.wg.Add(2) + go func() { + defer c.wg.Done() + t := time.NewTicker(expiration) + defer t.Stop() + for { + select { + case <-ctx.Done(): + return + case now := <-t.C: + fn := c.cleanUpExpiredHubs(now) + fn.Execute() + } + } + }() + go func() { + defer c.wg.Done() + err := c.watch() + if err != nil { + logger.Warnf("cannot watch for DB changes in hubs: %v", err) + } + }() + return c +} + +func (c *LinkedHubCache) removeByID(id string) { + f := c.loadFuture(id) + if f == nil { + return + } + v, err := f.Get(c.ctx) + if err != nil { + return + } + h, ok := v.(*LinkedHub) + if !ok { + return + } + h.Invalidate() +} + +func (c *LinkedHubCache) watch() error { + iter, err := c.store.WatchHubs(c.ctx) + if err != nil { + return err + } + for { + _, id, ok := iter.Next(c.ctx) + if !ok { + break + } + c.removeByID(id) + } + err = iter.Err() + errClose := iter.Close() + if err == nil { + err = errClose + } + if errors.Is(err, context.Canceled) { + return nil + } + return err +} + +func (c *LinkedHubCache) tryToRemoveExpiredHubLocked(key string, f *future.Future, now time.Time, wantToRefresh bool) (func(), bool) { + if !f.Ready() { + return nil, false + } + v, err := f.Get(context.Background()) + if err != nil { + delete(c.hubs, key) + return func() { + // do nothing + }, true + } + h, ok := v.(*LinkedHub) + if !ok { + delete(c.hubs, key) + return func() { + // do nothing + }, true + } + + if h.IsExpired(now) { + delete(c.hubs, key) + return h.Close, true + } + if wantToRefresh { + h.Refresh(now) + } + return func() { + // do nothing + }, false +} + +func (c *LinkedHubCache) cleanUpExpiredHubs(now time.Time) fn.FuncList { + var closer fn.FuncList + c.mutex.Lock() + defer c.mutex.Unlock() + for id, f := range c.hubs { + closeHub, ok := c.tryToRemoveExpiredHubLocked(id, f, now, false) + if ok { + closer.AddFunc(closeHub) + } + } + return closer +} + +func (c *LinkedHubCache) getHubs(ctx context.Context, eg *EnrollmentGroup) ([]*LinkedHub, error) { + hubs := make(map[string]*pb.Hub, len(eg.GetHubIds())+2) + err := c.store.LoadHubs(ctx, eg.GetOwner(), &store.HubsQuery{ + HubIdFilter: eg.GetHubIds(), + }, func(ctx context.Context, iter store.HubIter) (err error) { + for { + var cfg pb.Hub + ok := iter.Next(ctx, &cfg) + if !ok { + break + } + hubs[cfg.GetHubId()] = &cfg + } + + return iter.Err() + }) + if err != nil { + return nil, err + } + linkedHubs := make([]*LinkedHub, 0, len(hubs)) + var errs *multierror.Error + for _, hubID := range eg.GetHubIds() { + hub, ok := hubs[hubID] + if !ok { + errs = multierror.Append(errs, fmt.Errorf("cannot create linked hub(hubId: %v): not found", hubID)) + continue + } + h, err := NewLinkedHub(ctx, c.expiration, hub, c.fileWatcher, c.logger, c.tracerProvider) + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("cannot create linked hub(id:%v, hubId: %v): %w", hub.GetId(), hub.GetHubId(), err)) + continue + } + linkedHubs = append(linkedHubs, h) + } + if len(linkedHubs) == 0 { + err := errs.ErrorOrNil() + if err != nil { + return nil, fmt.Errorf("cannot find any hub with ids('%v'): %w", eg.GetHubIds(), err) + } + return nil, fmt.Errorf("cannot find any hub with ids: %v", eg.GetHubIds()) + } + if errs != nil { + c.logger.Debugf("some error occurs during load linked hubs: %v", errs.Error()) + } + return linkedHubs, nil +} + +func (c *LinkedHubCache) loadFuture(key string) *future.Future { + c.mutex.Lock() + defer c.mutex.Unlock() + f, ok := c.hubs[key] + if !ok { + return nil + } + return f +} + +func (c *LinkedHubCache) getFutureToken(key string, now time.Time) (*future.Future, future.SetFunc, fn.FuncList) { + var closer fn.FuncList + c.mutex.Lock() + defer c.mutex.Unlock() + for { + f, ok := c.hubs[key] + if !ok { + fu, set := future.New() + c.hubs[key] = fu + return fu, set, closer + } + closeHub, ok := c.tryToRemoveExpiredHubLocked(key, f, now, true) + if !ok { + return f, nil, closer + } + closer.AddFunc(closeHub) + } +} + +func (c *LinkedHubCache) pullOutAll() map[string]*future.Future { + c.mutex.Lock() + defer c.mutex.Unlock() + hubs := c.hubs + c.hubs = make(map[string]*future.Future) + return hubs +} + +func (c *LinkedHubCache) GetHubs(ctx context.Context, eg *EnrollmentGroup) ([]*LinkedHub, error) { + if _, ok := ctx.Deadline(); !ok { + return nil, errors.New("deadline is not set in ctx") + } + f, set, closer := c.getFutureToken(eg.GetId(), time.Now()) + defer closer.Execute() + if set == nil { + v, err := f.Get(ctx) + if err != nil { + return nil, err + } + h, ok := v.([]*LinkedHub) + if !ok { + return nil, fmt.Errorf("invalid object type(%T) in a future", v) + } + return h, err + } + hubs, err := c.getHubs(ctx, eg) + set(hubs, err) + if err != nil { + return nil, err + } + return hubs, nil +} + +func (c *LinkedHubCache) Close() { + c.cancel() + c.wg.Wait() + for _, f := range c.pullOutAll() { + v, err := f.Get(context.Background()) + if err != nil { + continue + } + h, ok := v.(*LinkedHub) + if !ok { + continue + } + h.Close() + } +} diff --git a/device-provisioning-service/service/log.go b/device-provisioning-service/service/log.go new file mode 100644 index 000000000..d0d3cc592 --- /dev/null +++ b/device-provisioning-service/service/log.go @@ -0,0 +1,82 @@ +package service + +import ( + "context" + "fmt" + "time" + + "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/plgd-dev/go-coap/v3/mux" + coapgwService "github.com/plgd-dev/hub/v2/coap-gateway/service" + coapgwMessage "github.com/plgd-dev/hub/v2/coap-gateway/service/message" + "github.com/plgd-dev/hub/v2/pkg/log" +) + +const ( + certificateCommonName = "certificateCommonName" + enrollmentGroup = "enrollmentGroup" + errorKey = "error" + remoterAddr = "remoteAddress" + localEndpointsKey = "localEndpoints" +) + +type logCoapMessage struct { + Method string `json:"method,omitempty"` + coapgwMessage.JsonCoapMessage +} + +func msgToLogCoapMessage(req *pool.Message, withBody bool) logCoapMessage { + rq := coapgwMessage.ToJson(req, withBody, false) + dumpReq := logCoapMessage{ + JsonCoapMessage: rq, + } + if req.Code() >= codes.GET && req.Code() <= codes.DELETE { + dumpReq.Method = rq.Code + dumpReq.Code = "" + } + return dumpReq +} + +func (s *Session) getLogger() log.Logger { + logger := s.server.logger.With(remoterAddr, s.coapConn.RemoteAddr().String()) + if cn := s.String(); cn != "" { + logger = logger.With(certificateCommonName, cn) + } + if s.enrollmentGroup != nil { + logger = logger.With(enrollmentGroup, s.enrollmentGroup.GetId()) + } + return logger +} + +func (s *Session) loggerWithReqResp(ctx context.Context, logger log.Logger, startTime time.Time, req *mux.Message, resp *pool.Message, err error) log.Logger { + logger = logger.With(log.StartTimeKey, startTime, log.DurationMSKey, log.DurationToMilliseconds(time.Since(startTime))) + if err != nil { + logger = logger.With(errorKey, err.Error()) + } + deadline, ok := ctx.Deadline() + if ok { + logger = logger.With(log.DeadlineKey, deadline) + } + if req != nil { + logger = logger.With(log.RequestKey, msgToLogCoapMessage(req.Message, s.server.config.Log.DumpBody)) + } + if resp != nil { + logger = logger.With(log.ResponseKey, msgToLogCoapMessage(resp, s.server.config.Log.DumpBody)) + } + return logger.With(log.ProtocolKey, "COAP") +} + +func (s *Session) logRequestResponse(ctx context.Context, startTime time.Time, req *mux.Message, resp *pool.Message, err error) { + logger := s.getLogger() + if resp != nil && !coapgwService.WantToLog(resp.Code(), logger) { + return + } + logger = s.loggerWithReqResp(ctx, logger, startTime, req, resp, err) + if resp != nil { + msg := fmt.Sprintf("finished unary call from the device with code %v", resp.Code()) + coapgwService.DefaultCodeToLevel(resp.Code(), logger)(msg) + return + } + logger.Debug("finished unary call from the device") +} diff --git a/device-provisioning-service/service/memory_test.go b/device-provisioning-service/service/memory_test.go new file mode 100644 index 000000000..365de4232 --- /dev/null +++ b/device-provisioning-service/service/memory_test.go @@ -0,0 +1,138 @@ +package service_test + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "fmt" + "net/http" + _ "net/http/pprof" + "os" + "runtime" + "testing" + "time" + + "github.com/google/uuid" + "github.com/plgd-dev/device/v2/schema/csr" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/options" + "github.com/plgd-dev/go-coap/v3/tcp" + caService "github.com/plgd-dev/hub/v2/certificate-authority/test" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + "github.com/plgd-dev/hub/v2/device-provisioning-service/uri" + "github.com/plgd-dev/hub/v2/pkg/config/property/urischeme" + pkgCoapService "github.com/plgd-dev/hub/v2/pkg/net/coap/service" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/plgd-dev/kit/v2/security/generateCertificate" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func connect(t *testing.T, dpsCfg service.Config) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*8) + defer cancel() + + // create connection via mfg certificate + c, err := tcp.Dial(dpsCfg.APIs.COAP.Addr, options.WithTLS(setupTLSConfig(t)), options.WithContext(ctx)) + require.NoError(t, err) + defer func() { + errC := c.Close() + require.NoError(t, errC) + <-c.Done() + }() + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + deviceID, err := uuid.NewRandom() + require.NoError(t, err) + + csrReq, err := generateCertificate.GenerateIdentityCSR(generateCertificate.Configuration{}, deviceID.String(), priv) + require.NoError(t, err) + + req := service.CredentialsRequest{ + CSR: service.CSR{ + Encoding: csr.CertificateEncoding_PEM, + Data: string(csrReq), + }, + } + + resp, err := c.Post(ctx, uri.Credentials, message.AppOcfCbor, bytes.NewReader(toCbor(t, req))) + require.NoError(t, err) + + if resp.Code() != codes.Changed { + fmt.Printf("resp: %+v\n", resp) + return + } + + _, err = c.Post(ctx, uri.CloudConfiguration, message.AppOcfCbor, bytes.NewReader(toCbor(t, service.ProvisionCloudConfigurationRequest{DeviceID: uuid.NewString()}))) + require.NoError(t, err) +} + +func testNConnections(t *testing.T, n int, debugging bool) { + go func() { + server := &http.Server{ + Addr: "localhost:8080", + ReadTimeout: time.Hour, + WriteTimeout: time.Hour, + } + err := server.ListenAndServe() + assert.NoError(t, err) + }() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId) + defer hubShutdown() + caCfg := caService.MakeConfig(t) + caCfg.Signer.ExpiresIn = time.Hour * 10 + caShutdown := caService.New(t, caCfg) + defer caShutdown() + + psk := testPSK + pskFile := writeToTempFile(t, "psk1.key", []byte(psk)) + defer func() { + err := os.Remove(pskFile) + require.NoError(t, err) + }() + + dpsCfg := test.MakeConfig(t) + dpsCfg.APIs.COAP.InactivityMonitor = &pkgCoapService.InactivityMonitor{ + Timeout: time.Second, + } + dpsCfg.EnrollmentGroups[0].PreSharedKeyFile = urischeme.URIScheme(pskFile) + shutDown := test.NewWithContext(ctx, t, dpsCfg) + defer shutDown() + + if debugging { + fmt.Printf("start collecting heap\n") + time.Sleep(time.Second * 10) + } + + for i := 0; i < n; i++ { + fmt.Printf("connect %d\n", i) + connect(t, dpsCfg) + } + + for i := 0; i < 3; i++ { + fmt.Printf("gc %d\n", i) + time.Sleep(time.Second) + runtime.GC() + } + + if debugging { + fmt.Printf("done\n") + time.Sleep(time.Second * 3600) + } +} + +func TestNConnections(t *testing.T) { + n := 10 + debugging := false + testNConnections(t, n, debugging) +} diff --git a/device-provisioning-service/service/message.go b/device-provisioning-service/service/message.go new file mode 100644 index 000000000..cc7676d8f --- /dev/null +++ b/device-provisioning-service/service/message.go @@ -0,0 +1,14 @@ +package service + +import ( + "context" + + "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/message/pool" +) + +func NewMessageWithCode(code codes.Code) *pool.Message { + msg := pool.NewMessage(context.Background()) + msg.SetCode(code) + return msg +} diff --git a/device-provisioning-service/service/ownership.go b/device-provisioning-service/service/ownership.go new file mode 100644 index 000000000..914b5c602 --- /dev/null +++ b/device-provisioning-service/service/ownership.go @@ -0,0 +1,46 @@ +package service + +import ( + "context" + "time" + + "github.com/plgd-dev/device/v2/schema/doxm" + coapCodes "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/plgd-dev/go-coap/v3/mux" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/identity-store/events" +) + +func (RequestHandle) ProcessOwnership(_ context.Context, req *mux.Message, session *Session, _ []*LinkedHub, group *EnrollmentGroup) (*pool.Message, error) { + switch req.Code() { + case coapCodes.GET: + msg, err := provisionOwnership(req, session, group) + session.updateProvisioningRecord(&store.ProvisioningRecord{ + Ownership: &pb.OwnershipStatus{ + Status: &pb.ProvisionStatus{ + Date: time.Now().UnixNano(), + CoapCode: toCoapCode(msg), + ErrorMessage: toErrorStr(err), + }, + Owner: group.GetOwner(), + }, + }) + return msg, err + default: + return nil, statusErrorf(coapCodes.Forbidden, "unsupported command(%v)", req.Code()) + } +} + +func provisionOwnership(req *mux.Message, session *Session, group *EnrollmentGroup) (*pool.Message, error) { + owner := events.OwnerToUUID(group.GetOwner()) + resp := doxm.DoxmUpdate{ + OwnerID: &owner, + } + msgType, data, err := encodeResponse(resp, req.Options()) + if err != nil { + return nil, statusErrorf(coapCodes.BadRequest, "cannot encode ownership response: %w", err) + } + return session.createResponse(coapCodes.Content, req.Token(), msgType, data), nil +} diff --git a/device-provisioning-service/service/ownership_test.go b/device-provisioning-service/service/ownership_test.go new file mode 100644 index 000000000..ab660521c --- /dev/null +++ b/device-provisioning-service/service/ownership_test.go @@ -0,0 +1,43 @@ +package service_test + +import ( + "context" + "testing" + "time" + + "github.com/plgd-dev/device/v2/schema/doxm" + "github.com/plgd-dev/go-coap/v3/options" + "github.com/plgd-dev/go-coap/v3/tcp" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + "github.com/plgd-dev/hub/v2/device-provisioning-service/uri" + "github.com/plgd-dev/hub/v2/identity-store/events" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" +) + +func TestOwnership(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesCertificateAuthority|hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId) + defer hubShutdown() + dpsCfg := test.MakeConfig(t) + shutDown := test.New(t, dpsCfg) + defer shutDown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) + defer cancel() + + c, err := tcp.Dial(dpsCfg.APIs.COAP.Addr, options.WithTLS(setupTLSConfig(t)), options.WithContext(ctx)) + require.NoError(t, err) + defer func() { + errC := c.Close() + require.NoError(t, errC) + }() + + resp, err := c.Get(ctx, uri.Ownership) + require.NoError(t, err) + + var ownership doxm.Doxm + fromCbor(t, resp.Body(), &ownership) + + require.Equal(t, doxm.Doxm{OwnerID: events.OwnerToUUID(test.MakeEnrollmentGroup().Owner)}, ownership) +} diff --git a/device-provisioning-service/service/plgdTime.go b/device-provisioning-service/service/plgdTime.go new file mode 100644 index 000000000..f0733e610 --- /dev/null +++ b/device-provisioning-service/service/plgdTime.go @@ -0,0 +1,41 @@ +package service + +import ( + "context" + "time" + + "github.com/plgd-dev/device/v2/schema/plgdtime" + coapCodes "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/plgd-dev/go-coap/v3/mux" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" +) + +func (RequestHandle) ProcessPlgdTime(_ context.Context, req *mux.Message, session *Session, _ []*LinkedHub, _ *EnrollmentGroup) (*pool.Message, error) { + switch req.Code() { + case coapCodes.GET: + msg, err := provisionPlgdTime(req, session) + session.updateProvisioningRecord(&store.ProvisioningRecord{ + PlgdTime: &pb.ProvisionStatus{ + Date: time.Now().UnixNano(), + CoapCode: toCoapCode(msg), + ErrorMessage: toErrorStr(err), + }, + }) + return msg, err + default: + return nil, statusErrorf(coapCodes.Forbidden, "unsupported command(%v)", req.Code()) + } +} + +func provisionPlgdTime(req *mux.Message, session *Session) (*pool.Message, error) { + resp := plgdtime.PlgdTimeUpdate{ + Time: time.Now().Format(time.RFC3339Nano), + } + msgType, data, err := encodeResponse(resp, req.Options()) + if err != nil { + return nil, statusErrorf(coapCodes.BadRequest, "cannot encode plgdTime response: %w", err) + } + return session.createResponse(coapCodes.Content, req.Token(), msgType, data), nil +} diff --git a/device-provisioning-service/service/plgdTime_test.go b/device-provisioning-service/service/plgdTime_test.go new file mode 100644 index 000000000..a09c243ac --- /dev/null +++ b/device-provisioning-service/service/plgdTime_test.go @@ -0,0 +1,41 @@ +package service_test + +import ( + "context" + "testing" + "time" + + "github.com/plgd-dev/device/v2/schema/plgdtime" + "github.com/plgd-dev/go-coap/v3/options" + "github.com/plgd-dev/go-coap/v3/tcp" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" +) + +func TestPlgdTime(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesCertificateAuthority|hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId) + defer hubShutdown() + dpsCfg := test.MakeConfig(t) + shutDown := test.New(t, dpsCfg) + defer shutDown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) + defer cancel() + + c, err := tcp.Dial(dpsCfg.APIs.COAP.Addr, options.WithTLS(setupTLSConfig(t)), options.WithContext(ctx)) + require.NoError(t, err) + defer func() { + errC := c.Close() + require.NoError(t, errC) + }() + + resp, err := c.Get(ctx, plgdtime.ResourceURI) + require.NoError(t, err) + + var plgdTime plgdtime.PlgdTimeUpdate + fromCbor(t, resp.Body(), &plgdTime) + + require.NotEmpty(t, plgdTime.Time) +} diff --git a/device-provisioning-service/service/provisionCertificate_test.go b/device-provisioning-service/service/provisionCertificate_test.go new file mode 100644 index 000000000..8f03022fb --- /dev/null +++ b/device-provisioning-service/service/provisionCertificate_test.go @@ -0,0 +1,519 @@ +package service_test + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/tls" + "errors" + "fmt" + "io" + "os" + "strconv" + "testing" + "time" + + "github.com/google/uuid" + "github.com/pion/dtls/v2" + deviceClient "github.com/plgd-dev/device/v2/client" + "github.com/plgd-dev/device/v2/pkg/net/coap" + "github.com/plgd-dev/device/v2/schema" + "github.com/plgd-dev/device/v2/schema/credential" + deviceTest "github.com/plgd-dev/device/v2/test" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/plgd-dev/go-coap/v3/mux" + "github.com/plgd-dev/go-coap/v3/udp" + caService "github.com/plgd-dev/hub/v2/certificate-authority/test" + "github.com/plgd-dev/hub/v2/coap-gateway/coapconv" + hubCoapGWTest "github.com/plgd-dev/hub/v2/coap-gateway/test" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + "github.com/plgd-dev/hub/v2/device-provisioning-service/uri" + "github.com/plgd-dev/hub/v2/grpc-gateway/client" + grpcPb "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + isEvents "github.com/plgd-dev/hub/v2/identity-store/events" + "github.com/plgd-dev/hub/v2/pkg/log" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/sdk" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/plgd-dev/kit/v2/codec/cbor" + "github.com/plgd-dev/kit/v2/codec/json" + kitNet "github.com/plgd-dev/kit/v2/net" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func TestProvisioningWithRenewal(t *testing.T) { + coapGWCfg := hubCoapGWTest.MakeConfig(t) + coapGWCfg.APIs.COAP.TLS.Embedded.ClientCertificateRequired = true + coapGWCfg.APIs.COAP.TLS.DisconnectOnExpiredCertificate = false + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId|hubTestService.SetUpServicesCoapGateway| + hubTestService.SetUpServicesResourceAggregate|hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesGrpcGateway, hubTestService.WithCOAPGWConfig(coapGWCfg)) + defer hubShutdown() + + caCfg := caService.MakeConfig(t) + // sign with certificate that will soon expire + // the validity must be longer than the expiration limit (for tests this is 10s), otherwise the certificate will be replaced right away + expiresIn := 30 * time.Second + caCfg.Signer.ValidFrom = caCfgSignerValidFrom + caCfg.Signer.ExpiresIn = time.Hour + expiresIn + caShutdown := caService.New(t, caCfg) + defer caShutdown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := grpcPb.NewGrpcGatewayClient(conn) + + dpsCfg := test.MakeConfig(t) + const expectedTimeCount = 1 + const expectedCredentialsCount = 2 // certificate with short validity should trigger certificate renewal, but nothing else + const expectedOwnershipCount = 1 + const expectedACLsCount = 1 + const expectedCloudConfigurationCount = 1 + waitIterations := 5 // don't end right away, wait some iterations so we know only credentials renewal was triggered and nothing else + rh := test.NewRequestHandlerWithCounter(t, dpsCfg, nil, + func(defaultHandlerCount, processTimeCount, processOwnershipCount, processCloudConfigurationCount, processCredentialsCount, processACLsCount uint64) (bool, error) { + if defaultHandlerCount > 0 || + processTimeCount > expectedTimeCount || + processOwnershipCount > expectedOwnershipCount || + processCloudConfigurationCount > expectedCloudConfigurationCount || + processCredentialsCount > expectedCredentialsCount || + processACLsCount > expectedACLsCount { + return false, fmt.Errorf("invalid counters default(%d:%d) time(%d:%d) owner(%d:%d) cloud(%d:%d) creds(%d:%d) acls(%d:%d)", + defaultHandlerCount, 0, + processTimeCount, expectedTimeCount, + processOwnershipCount, expectedOwnershipCount, + processCloudConfigurationCount, expectedCloudConfigurationCount, + processCredentialsCount, expectedCredentialsCount, + processACLsCount, expectedACLsCount, + ) + } + + done := defaultHandlerCount == 0 && + processTimeCount == expectedTimeCount && + processOwnershipCount == expectedOwnershipCount && + processCloudConfigurationCount == expectedCloudConfigurationCount && + processCredentialsCount == expectedCredentialsCount && + processACLsCount == expectedACLsCount + if done && waitIterations > 0 { + log.Infof("checking for unexpected events") + waitIterations-- + return false, nil + } + return done, nil + }) + + dpsShutDown := test.New(t, rh.Cfg()) + deviceID := hubTest.MustFindDeviceByName(test.TestDeviceObtName) + deviceID, shutdownSim := test.OnboardDpsSim(ctx, t, c, deviceID, rh.Cfg().APIs.COAP.Addr, test.TestDevsimResources) + defer shutdownSim() + dpsShutDown() + + err = test.ForceReprovision(ctx, c, deviceID) + require.NoError(t, err) + + subClient, err := client.New(c).GrpcGatewayClient().SubscribeToEvents(ctx) + require.NoError(t, err) + defer func() { + errC := subClient.CloseSend() + require.NoError(t, errC) + }() + subID, corID := test.SubscribeToEvents(t, subClient, &grpcPb.SubscribeToEvents{ + CorrelationId: "deviceOnline", + Action: &grpcPb.SubscribeToEvents_CreateSubscription_{ + CreateSubscription: &grpcPb.SubscribeToEvents_CreateSubscription{ + EventFilter: []grpcPb.SubscribeToEvents_CreateSubscription_Event{ + grpcPb.SubscribeToEvents_CreateSubscription_DEVICE_METADATA_UPDATED, + }, + }, + }, + }) + + rh.StartDps(service.WithRequestHandler(rh)) + defer rh.StopDps() + + err = rh.Verify(ctx) + require.NoError(t, err) + + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_OFFLINE) + require.NoError(t, err) + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_ONLINE) + require.NoError(t, err) +} + +func TestProvisioningNewCertificateDuringConnectionToHub(t *testing.T) { + defer test.ClearDB(t) + + coapGWCfg := hubCoapGWTest.MakeConfig(t) + coapGWCfg.APIs.COAP.TLS.Embedded.ClientCertificateRequired = true + coapGWCfg.APIs.COAP.TLS.DisconnectOnExpiredCertificate = false + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId|hubTestService.SetUpServicesCoapGateway| + hubTestService.SetUpServicesResourceAggregate|hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesGrpcGateway, hubTestService.WithCOAPGWConfig(coapGWCfg)) + defer hubShutdown() + + caCfg := caService.MakeConfig(t) + // sign with certificate that will soon expire + // the validity must be longer than the expiration limit (for tests this is 10s), otherwise the certificate will be replaced right away + expiresIn := 20 * time.Second + caCfg.Signer.ValidFrom = caCfgSignerValidFrom + caCfg.Signer.ExpiresIn = time.Hour + expiresIn + caShutdown := caService.New(t, caCfg) + defer caShutdown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + token := oauthTest.GetDefaultAccessToken(t) + ctx = kitNetGrpc.CtxWithToken(ctx, token) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := grpcPb.NewGrpcGatewayClient(conn) + + dpsCfg := test.MakeConfig(t) + dpsShutDown := test.New(t, dpsCfg) + defer dpsShutDown() + + deviceID := hubTest.MustFindDeviceByName(test.TestDeviceObtName) + deviceID, shutdownSim := test.OnboardDpsSim(ctx, t, c, deviceID, dpsCfg.APIs.COAP.Addr, test.TestDevsimResources) + defer shutdownSim() + + subClient, err := client.New(c).GrpcGatewayClient().SubscribeToEvents(ctx) + require.NoError(t, err) + defer func(s grpcPb.GrpcGateway_SubscribeToEventsClient) { + err := s.CloseSend() + require.NoError(t, err) + }(subClient) + + _, _ = test.SubscribeToEvents(t, subClient, &grpcPb.SubscribeToEvents{ + CorrelationId: "dpsResource", + Action: &grpcPb.SubscribeToEvents_CreateSubscription_{ + CreateSubscription: &grpcPb.SubscribeToEvents_CreateSubscription{ + EventFilter: []grpcPb.SubscribeToEvents_CreateSubscription_Event{}, + ResourceIdFilter: []*grpcPb.ResourceIdFilter{ + { + ResourceId: commands.NewResourceID(deviceID, test.ResourcePlgdDpsHref), + }, + }, + }, + }, + }) + + // certificate is expired, so we need to wait at least 10s for renewal - minimal time of expiration certification check is 10s + log.Infof("waiting for events\n") + + expectedProvisionStatuses := []string{"renew credentials", "provisioned"} + for _, expectedProvisionStatus := range expectedProvisionStatuses { + ev, err := subClient.Recv() + require.NoError(t, err) + log.Infof("event: %+v\n", ev) + if ev.GetResourceChanged() != nil { + var res test.ResourcePlgdDps + err = cbor.Decode(ev.GetResourceChanged().GetContent().GetData(), &res) + require.NoError(t, err) + require.Equal(t, expectedProvisionStatus, res.ProvisionStatus) + continue + } + require.NoError(t, fmt.Errorf("unexpected event: %+v", ev)) + } +} + +/** + * Test device security on CA change: + * - use owner with the same ID that is used by the DPS but with a different certificate authority + * - keep the connection open + * - onboard by DPS, which will own the device and replace certificates + * - the original connection should now fail when trying to query the device + */ +func TestOwnerWithUnknownCertificateAuthority(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesCertificateAuthority|hubTestService.SetUpServicesResourceDirectory| + hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId|hubTestService.SetUpServicesCoapGateway|hubTestService.SetUpServicesResourceAggregate| + hubTestService.SetUpServicesGrpcGateway) + defer hubShutdown() + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + + dpcCfg := test.MakeConfig(t) + dpsShutDown := test.New(t, dpcCfg) + defer dpsShutDown() + + token := oauthTest.GetDefaultAccessToken(t) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + ctx = kitNetGrpc.CtxWithToken(ctx, token) + + rootCA, err := os.ReadFile(os.Getenv("TEST_DPS_ROOT_CA_CERT_ALT")) + require.NoError(t, err) + rootCAKey, err := os.ReadFile(os.Getenv("TEST_DPS_ROOT_CA_KEY_ALT")) + require.NoError(t, err) + devClient, err := sdk.NewClient(sdk.WithID(isEvents.OwnerToUUID(test.DPSOwner)), + sdk.WithRootCA(rootCA, rootCAKey), sdk.WithValidity("2000-01-01T12:00:00Z", "876000h")) + require.NoError(t, err) + + deviceID := hubTest.MustFindDeviceByName(test.TestDeviceObtName) + deviceID, err = devClient.OwnDevice(ctx, deviceID, deviceClient.WithOTM(deviceClient.OTMType_JustWorks)) + require.NoError(t, err) + + c := grpcPb.NewGrpcGatewayClient(conn) + subClient, err := client.New(c).GrpcGatewayClient().SubscribeToEvents(ctx) + require.NoError(t, err) + defer func(s grpcPb.GrpcGateway_SubscribeToEventsClient) { + errC := s.CloseSend() + require.NoError(t, errC) + }(subClient) + subID, corID := test.SubscribeToEvents(t, subClient, &grpcPb.SubscribeToEvents{ + CorrelationId: "registered", + Action: &grpcPb.SubscribeToEvents_CreateSubscription_{ + CreateSubscription: &grpcPb.SubscribeToEvents_CreateSubscription{ + EventFilter: []grpcPb.SubscribeToEvents_CreateSubscription_Event{ + grpcPb.SubscribeToEvents_CreateSubscription_REGISTERED, + }, + }, + }, + }) + + dpsEndpoint := uri.CoAPsTCPSchemePrefix + dpcCfg.APIs.COAP.Addr + err = devClient.UpdateResource(ctx, deviceID, test.ResourcePlgdDpsHref, test.ResourcePlgdDps{Endpoint: &dpsEndpoint}, nil) + require.NoError(t, err) + + err = test.WaitForRegistered(t, subClient, deviceID, subID, corID) + require.NoError(t, err) + + var resp interface{} + err = devClient.GetResource(ctx, deviceID, test.ResourcePlgdDpsHref, &resp) + require.Error(t, err) + err = devClient.Close(ctx) + require.NoError(t, err) + + devClient, err = sdk.NewClient(sdk.WithID(isEvents.OwnerToUUID(test.DPSOwner))) + require.NoError(t, err) + deviceID, err = devClient.OwnDevice(ctx, deviceID, deviceClient.WithOTM(deviceClient.OTMType_JustWorks)) + require.NoError(t, err) + defer func() { + err = devClient.DisownDevice(ctx, deviceID) + require.NoError(t, err) + err = devClient.Close(ctx) + require.NoError(t, err) + time.Sleep(time.Second * 2) + }() + err = devClient.GetResource(ctx, deviceID, test.ResourcePlgdDpsHref, &resp) + require.NoError(t, err) +} + +type testRequestHandlerWithCustomCredentials struct { + test.RequestHandlerWithDps + service.RequestHandle + + subject uuid.UUID + psk uuid.UUID +} + +func (h *testRequestHandlerWithCustomCredentials) ProcessCredentials(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + msg, err := h.RequestHandle.ProcessCredentials(ctx, req, session, linkedHubs, group) + if err != nil { + return nil, err + } + + if h.psk != uuid.Nil { + credReq, cf, err := decodeRequest(msg) + if err != nil { + return nil, err + } + + credReq.Credentials = append(credReq.Credentials, credential.Credential{ + Subject: h.subject.String(), + Type: credential.CredentialType_SYMMETRIC_PAIR_WISE, + PrivateData: &credential.CredentialPrivateData{ + DataInternal: h.psk, + Encoding: credential.CredentialPrivateDataEncoding_RAW, + }, + }) + + encode, err := coapconv.GetEncoder(cf) + if err != nil { + return nil, err + } + payload, err := encode(credReq) + if err != nil { + return nil, err + } + + msg.SetBody(bytes.NewReader(payload)) + } + return msg, nil +} + +func decodeRequest(msg *pool.Message) (credential.CredentialUpdateRequest, message.MediaType, error) { + cf, err := msg.ContentFormat() + if err != nil { + return credential.CredentialUpdateRequest{}, 0, err + } + + type readerFunc = func(w io.Reader, v interface{}) error + var reader readerFunc + switch cf { + case message.AppJSON: + reader = json.ReadFrom + case message.AppCBOR, message.AppOcfCbor: + reader = cbor.ReadFrom + default: + return credential.CredentialUpdateRequest{}, 0, fmt.Errorf("unsupported type (%v)", cf) + } + + var credReq credential.CredentialUpdateRequest + if err = reader(msg.Body(), &credReq); err != nil { + return credential.CredentialUpdateRequest{}, 0, err + } + return credReq, cf, nil +} + +func dialDTLS(ctx context.Context, addr string, subject, psk []byte, opts ...udp.Option) (*coap.ClientCloseHandler, error) { + cfg := &dtls.Config{ + PSKIdentityHint: subject, + PSK: func([]byte) ([]byte, error) { + return psk[:16], nil + }, + CipherSuites: []dtls.CipherSuiteID{dtls.TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256}, + } + return coap.DialUDPSecure(ctx, addr, cfg, opts...) +} + +func findSecureUDPEndpoint(ctx context.Context) (kitNet.Addr, error) { + eps, err := deviceTest.FindDeviceEndpoints(ctx, test.TestDeviceObtName, deviceTest.IP4) + if err != nil { + return kitNet.Addr{}, err + } + for _, ep := range eps { + addr, err := ep.GetAddr() + if err != nil { + return kitNet.Addr{}, err + } + if addr.GetScheme() == string(schema.UDPSecureScheme) { + return addr, nil + } + } + return kitNet.Addr{}, errors.New("no endpoint found") +} + +/** + * Test device security on a credentials update: + * - add preshared key (PSK) to device and create a connection using PSK + * - force reprovisioning which updates credentials + * - the PSK connection should be closed to force revalidation after a credentials update + */ +func TestDisconnectAfterCredentialsUpdate(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId| + hubTestService.SetUpServicesCertificateAuthority|hubTestService.SetUpServicesCoapGateway|hubTestService.SetUpServicesResourceAggregate| + hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesGrpcGateway) + defer hubShutdown() + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := grpcPb.NewGrpcGatewayClient(conn) + + psk := make([]byte, 16) + _, err = rand.Read(psk) + require.NoError(t, err) + dpsCfg := test.MakeConfig(t) + pskUUID, err := uuid.FromBytes(psk) + require.NoError(t, err) + + subjectUUID, err := uuid.Parse(isEvents.OwnerToUUID(test.DPSOwner)) + require.NoError(t, err) + + h := &testRequestHandlerWithCustomCredentials{ + RequestHandlerWithDps: test.MakeRequestHandlerWithDps(t, dpsCfg), + psk: pskUUID, + subject: subjectUUID, + } + h.StartDps(service.WithRequestHandler(h)) + deviceID := hubTest.MustFindDeviceByName(test.TestDeviceObtName) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) + deviceID, shutdownSim := test.OnboardDpsSim(ctx, t, c, deviceID, h.Cfg().APIs.COAP.Addr, test.TestDevsimResources) + defer shutdownSim() + h.StopDps() + + addr, err := findSecureUDPEndpoint(ctx) + require.NoError(t, err) + deviceAddr := addr.GetHostname() + ":" + strconv.FormatInt(int64(addr.GetPort()), 10) + subject, err := h.subject.MarshalBinary() + require.NoError(t, err) + psk, err = h.psk.MarshalBinary() + require.NoError(t, err) + pskConn, err := dialDTLS(ctx, deviceAddr, subject, psk) + require.NoError(t, err) + // get /oic/sec/cred -> succeeds + err = pskConn.GetResource(ctx, credential.ResourceURI, nil) + require.NoError(t, err) + + subClient, err := client.New(c).GrpcGatewayClient().SubscribeToEvents(ctx) + require.NoError(t, err) + defer func() { + errC := subClient.CloseSend() + require.NoError(t, errC) + }() + subID, corID := test.SubscribeToEvents(t, subClient, &grpcPb.SubscribeToEvents{ + CorrelationId: "deviceOnline", + Action: &grpcPb.SubscribeToEvents_CreateSubscription_{ + CreateSubscription: &grpcPb.SubscribeToEvents_CreateSubscription{ + EventFilter: []grpcPb.SubscribeToEvents_CreateSubscription_Event{ + grpcPb.SubscribeToEvents_CreateSubscription_DEVICE_METADATA_UPDATED, + }, + }, + }, + }) + + // reprovisiong without cred for psk + dpsShutDown := test.New(t, h.Cfg()) + defer dpsShutDown() + err = test.ForceReprovision(ctx, c, deviceID) + require.NoError(t, err) + + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_OFFLINE) + require.NoError(t, err) + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_ONLINE) + require.NoError(t, err) + + // pskConn should be closed during reprovision without cred for psk + closed := false + select { + case <-pskConn.Done(): + closed = true + case <-time.After(time.Second * 10): + } + require.True(t, closed) +} diff --git a/device-provisioning-service/service/provisionDelay_test.go b/device-provisioning-service/service/provisionDelay_test.go new file mode 100644 index 000000000..3582d6583 --- /dev/null +++ b/device-provisioning-service/service/provisionDelay_test.go @@ -0,0 +1,129 @@ +package service_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/plgd-dev/go-coap/v3/mux" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + "go.uber.org/atomic" +) + +type testRequestHandlerWithDelay struct { + test.RequestHandlerWithDps + delayedTime atomic.Bool + waitForTime chan struct{} + delayedOwnership atomic.Bool + waitForOwnership chan struct{} + delayedCloudConfiguration atomic.Bool + waitForCloudConfiguration chan struct{} + delayedCredentials atomic.Bool + waitForCredentials chan struct{} + delayedACLs atomic.Bool + waitForACLs chan struct{} + delay time.Duration + r service.RequestHandle +} + +func newTestRequestHandlerWithDelay(t *testing.T, dpsCfg service.Config, delay time.Duration) *testRequestHandlerWithDelay { + return &testRequestHandlerWithDelay{ + RequestHandlerWithDps: test.MakeRequestHandlerWithDps(t, dpsCfg), + waitForTime: make(chan struct{}), + waitForOwnership: make(chan struct{}), + waitForCloudConfiguration: make(chan struct{}), + waitForCredentials: make(chan struct{}), + waitForACLs: make(chan struct{}), + delay: delay, + } +} + +func (h *testRequestHandlerWithDelay) DefaultHandler(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + return h.r.DefaultHandler(ctx, req, session, linkedHubs, group) +} + +func (h *testRequestHandlerWithDelay) ProcessPlgdTime(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if h.delayedTime.CompareAndSwap(false, true) { + time.Sleep(h.delay) + close(h.waitForTime) + } + <-h.waitForTime + return h.r.ProcessPlgdTime(ctx, req, session, linkedHubs, group) +} + +func (h *testRequestHandlerWithDelay) ProcessOwnership(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if h.delayedOwnership.CompareAndSwap(false, true) { + time.Sleep(h.delay) + close(h.waitForOwnership) + } + <-h.waitForOwnership + return h.r.ProcessOwnership(ctx, req, session, linkedHubs, group) +} + +func (h *testRequestHandlerWithDelay) ProcessCloudConfiguration(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if h.delayedCloudConfiguration.CompareAndSwap(false, true) { + time.Sleep(h.delay) + close(h.waitForCloudConfiguration) + } + <-h.waitForCloudConfiguration + return h.r.ProcessCloudConfiguration(ctx, req, session, linkedHubs, group) +} + +func (h *testRequestHandlerWithDelay) ProcessCredentials(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if h.delayedCredentials.CompareAndSwap(false, true) { + time.Sleep(h.delay) + close(h.waitForCredentials) + } + <-h.waitForCredentials + return h.r.ProcessCredentials(ctx, req, session, linkedHubs, group) +} + +func (h *testRequestHandlerWithDelay) ProcessACLs(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if h.delayedACLs.CompareAndSwap(false, true) { + time.Sleep(h.delay) + close(h.waitForACLs) + } + <-h.waitForACLs + return h.r.ProcessACLs(ctx, req, session, linkedHubs, group) +} + +func (h *testRequestHandlerWithDelay) Verify(ctx context.Context) error { + logCounter := 0 + for { + select { + case <-ctx.Done(): + return fmt.Errorf("unexpected counters delayedTime=%v delayedOwnership=%v delayedCloudConfiguration=%v delayedCredentials=%v delayedACLs=%v", + h.delayedTime.Load(), + h.delayedOwnership.Load(), + h.delayedCloudConfiguration.Load(), + h.delayedCredentials.Load(), + h.delayedACLs.Load()) + case <-time.After(time.Second): + logCounter++ + if logCounter%3 == 0 { + h.Logf("delayedTime=%v delayedOwnership=%v delayedCloudConfiguration=%v delayedCredentials=%v delayedACLs=%v", + h.delayedTime.Load(), + h.delayedOwnership.Load(), + h.delayedCloudConfiguration.Load(), + h.delayedCredentials.Load(), + h.delayedACLs.Load()) + } + } + if h.delayedTime.Load() && + h.delayedOwnership.Load() && + h.delayedCloudConfiguration.Load() && + h.delayedCredentials.Load() && + h.delayedACLs.Load() { + return nil + } + } +} + +func TestProvisioningWithDelay(t *testing.T) { + dpsCfg := test.MakeConfig(t) + rh := newTestRequestHandlerWithDelay(t, dpsCfg, 10*time.Second) + testProvisioningWithDPSHandler(t, rh, time.Minute*5) +} diff --git a/device-provisioning-service/service/provisionFail_test.go b/device-provisioning-service/service/provisionFail_test.go new file mode 100644 index 000000000..4cb2a3e51 --- /dev/null +++ b/device-provisioning-service/service/provisionFail_test.go @@ -0,0 +1,460 @@ +package service_test + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "testing" + "time" + + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/plgd-dev/go-coap/v3/mux" + "github.com/plgd-dev/go-coap/v3/pkg/cache" + caService "github.com/plgd-dev/hub/v2/certificate-authority/test" + hubCoapGWTest "github.com/plgd-dev/hub/v2/coap-gateway/test" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + "github.com/plgd-dev/hub/v2/grpc-gateway/client" + grpcPb "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + "github.com/plgd-dev/hub/v2/pkg/log" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "go.uber.org/atomic" + "go.uber.org/zap/zapcore" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +const caCfgSignerValidFrom = "now-1h" + +type testRequestHandlerWithExpiringCert struct { + service.RequestHandle + + blocking atomic.Bool // block provisioning + expiresIn time.Duration // wait this long on the final call (ProcessCloudConfiguration) so the certificate expires + expired atomic.Bool + logger log.Logger + wait chan struct{} + waiting atomic.Bool +} + +func newTestRequestHandlerWithExpiredCert(expiresIn time.Duration) *testRequestHandlerWithExpiringCert { + return &testRequestHandlerWithExpiringCert{ + expiresIn: expiresIn, + logger: log.NewLogger(log.Config{Level: zapcore.DebugLevel}), + wait: make(chan struct{}), + } +} + +func (h *testRequestHandlerWithExpiringCert) blockProvisioning() { + if h.blocking.CompareAndSwap(false, true) { + h.logger.Debugf("start blocking provisioning") + } +} + +func (h *testRequestHandlerWithExpiringCert) unblockProvisioning() { + if h.blocking.CompareAndSwap(true, false) { + h.logger.Debugf("stop blocking provisioning") + } +} + +func (h *testRequestHandlerWithExpiringCert) ProcessACLs(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if h.expiresIn == 0 { + h.expired.Store(true) + return h.RequestHandle.ProcessACLs(ctx, req, session, linkedHubs, group) + } + if h.blocking.Load() { + // returning nil, nil will result in the resp being discarded and nothing being sent back to the client + return nil, nil //nolint:nilnil + } + if h.waiting.CompareAndSwap(false, true) { + h.blockProvisioning() // block retry of provisioning/certificate refresh until we manually unblock + wait := h.expiresIn + time.Second + time.Sleep(wait) + h.logger.Debugf("certificate expired") + h.expired.Store(true) + } + h.logger.Debugf("process acls") + return h.RequestHandle.ProcessACLs(ctx, req, session, linkedHubs, group) +} + +func (h *testRequestHandlerWithExpiringCert) verify(ctx context.Context) error { + for { + select { + case <-ctx.Done(): + return fmt.Errorf("provisioning timed out: expired=%v", h.expired.Load()) + case <-time.After(time.Second): + h.logger.Debugf("expired=%v", h.expired.Load()) + } + if h.expired.Load() { + return nil + } + } +} + +func TestProvisioningWithExpiringCertificate(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId| + hubTestService.SetUpServicesResourceAggregate|hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesGrpcGateway) + defer hubShutdown() + + coapGWCfg := hubCoapGWTest.MakeConfig(t) + coapGWCfg.APIs.COAP.TLS.Embedded.ClientCertificateRequired = true + coapGWCfg.APIs.COAP.TLS.DisconnectOnExpiredCertificate = true + coapGWCfg.APIs.COAP.OwnerCacheExpiration = time.Second + coapGWCfg.Log.Level = log.DebugLevel + coapGWCfg.Log.DumpBody = true + coapGWShutdown := hubCoapGWTest.New(t, coapGWCfg) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + token := oauthTest.GetDefaultAccessToken(t) + ctx = kitNetGrpc.CtxWithToken(ctx, token) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := grpcPb.NewGrpcGatewayClient(conn) + + caCfg := caService.MakeConfig(t) + caShutdown := caService.New(t, caCfg) + + dpsCfg := test.MakeConfig(t) + dpsCfg.Log.Level = log.DebugLevel + dpsCfg.Log.DumpBody = true + dpsCfg.APIs.COAP.InactivityMonitor.Timeout = time.Minute + rh := newTestRequestHandler(t, dpsCfg, defaultTestDpsHandlerConfig()) + rh.StartDps(service.WithRequestHandler(rh)) + deviceID := hubTest.MustFindDeviceByName(test.TestDeviceObtName) + deviceID, shutdownSim := test.OnboardDpsSim(ctx, t, c, deviceID, dpsCfg.APIs.COAP.Addr, test.TestDevsimResources) + defer shutdownSim() + + // wait for provisioning success + err = rh.Verify(ctx) + require.NoError(t, err) + rh.StopDps() + + shortTimeout := time.Second * 30 // enough time for provisioning to succeed and certificate to expire + shortCtx, shortCancel := context.WithTimeout(context.Background(), shortTimeout) + defer shortCancel() + shortCtx = kitNetGrpc.CtxWithToken(shortCtx, token) + + subClient, err := client.New(c).GrpcGatewayClient().SubscribeToEvents(shortCtx) + require.NoError(t, err) + defer func(s grpcPb.GrpcGateway_SubscribeToEventsClient) { + errC := s.CloseSend() + require.NoError(t, errC) + }(subClient) + + subID, corID := test.SubscribeToEvents(t, subClient, &grpcPb.SubscribeToEvents{ + CorrelationId: "deviceOnline", + Action: &grpcPb.SubscribeToEvents_CreateSubscription_{ + CreateSubscription: &grpcPb.SubscribeToEvents_CreateSubscription{ + EventFilter: []grpcPb.SubscribeToEvents_CreateSubscription_Event{ + grpcPb.SubscribeToEvents_CreateSubscription_DEVICE_METADATA_UPDATED, + }, + }, + }, + }) + + err = test.ForceReprovision(ctx, c, deviceID) + require.NoError(t, err) + + // shutdown coap-gw to force device to reconnect + coapGWShutdown() + + // shutdown coap-gw sets device to offline, wait for it + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_OFFLINE) + require.NoError(t, err) + + // reconfigure CA to use certificate that will expire soon + caShutdown() + // sign with certificate that will soon expire + expiresIn := 13 * time.Second // 10s + 3s, where 10s = expiring limit of the test device, 3s is enough time to finish provisioning steps + caCfg.Signer.ValidFrom = caCfgSignerValidFrom + caCfg.Signer.ExpiresIn = time.Hour + expiresIn + caShutdown = caService.New(t, caCfg) + + h := newTestRequestHandlerWithExpiredCert(expiresIn) + dpsShutDown := test.New(t, dpsCfg, service.WithRequestHandler(h)) + defer dpsShutDown() + + // DPS provisioning should succeed + err = h.verify(shortCtx) + require.NoError(t, err) + + coapGWShutdown = hubCoapGWTest.New(t, coapGWCfg) + defer coapGWShutdown() + + // online msg should not be received because of the expired certificate -> wait for timeout + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_ONLINE) + require.Error(t, err) + + h.unblockProvisioning() + + subClient, err = client.New(c).GrpcGatewayClient().SubscribeToEvents(ctx) + require.NoError(t, err) + defer func(s grpcPb.GrpcGateway_SubscribeToEventsClient) { + errC := s.CloseSend() + require.NoError(t, errC) + }(subClient) + subID, corID = test.SubscribeToEvents(t, subClient, &grpcPb.SubscribeToEvents{ + CorrelationId: "deviceOnline", + Action: &grpcPb.SubscribeToEvents_CreateSubscription_{ + CreateSubscription: &grpcPb.SubscribeToEvents_CreateSubscription{ + EventFilter: []grpcPb.SubscribeToEvents_CreateSubscription_Event{ + grpcPb.SubscribeToEvents_CreateSubscription_DEVICE_METADATA_UPDATED, + }, + }, + }, + }) + + caShutdown() + // sign with valid certificate + caCfg.Signer.ExpiresIn = time.Hour * 2 + caShutdown = caService.New(t, caCfg) + defer caShutdown() + + // online event -> after successful reprovisioning + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_ONLINE) + require.NoError(t, err) +} + +func TestProvisioningWithExpiredCertificate(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId| + hubTestService.SetUpServicesResourceAggregate|hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesGrpcGateway) + defer hubShutdown() + + coapGWCfg := hubCoapGWTest.MakeConfig(t) + coapGWCfg.APIs.COAP.TLS.Embedded.ClientCertificateRequired = true + coapGWShutdown := hubCoapGWTest.New(t, coapGWCfg) + defer coapGWShutdown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + token := oauthTest.GetDefaultAccessToken(t) + ctx = kitNetGrpc.CtxWithToken(ctx, token) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := grpcPb.NewGrpcGatewayClient(conn) + + caCfg := caService.MakeConfig(t) + caShutdown := caService.New(t, caCfg) + + dpsCfg := test.MakeConfig(t) + rh := newTestRequestHandler(t, dpsCfg, defaultTestDpsHandlerConfig()) + rh.StartDps(service.WithRequestHandler(rh)) + deviceID := hubTest.MustFindDeviceByName(test.TestDeviceObtName) + deviceID, shutdownSim := test.OnboardDpsSim(ctx, t, c, deviceID, dpsCfg.APIs.COAP.Addr, test.TestDevsimResources) + defer shutdownSim() + + // wait for provisioning success so we get OFFLINE event first after reprovisioning + err = rh.Verify(ctx) + require.NoError(t, err) + rh.StopDps() + + err = test.ForceReprovision(ctx, c, deviceID) + require.NoError(t, err) + + caShutdown() + // sign with expired certificate + caCfg.Signer.ValidFrom = caCfgSignerValidFrom + caCfg.Signer.ExpiresIn = 0 + caShutdown = caService.New(t, caCfg) + defer caShutdown() + + const expectedSuccessCount = 1 + h := test.NewRequestHandlerWithCounter(t, dpsCfg, nil, + func(defaultHandlerCount, processTimeCount, processOwnershipCount, processCloudConfigurationCount, processCredentialsCount, processACLsCount uint64) (bool, error) { + if defaultHandlerCount > 0 || + processTimeCount > expectedSuccessCount || + processOwnershipCount > expectedSuccessCount || + processCloudConfigurationCount > expectedSuccessCount || + processCredentialsCount > expectedSuccessCount || + processACLsCount > expectedSuccessCount { + return false, fmt.Errorf("invalid counters default(%d:%d) time(%d:%d) owner(%d:%d) cloud(%d:%d) creds(%d:%d) acls(%d:%d)", + defaultHandlerCount, 0, + processTimeCount, expectedSuccessCount, + processOwnershipCount, expectedSuccessCount, + processCloudConfigurationCount, expectedSuccessCount, + processCredentialsCount, expectedSuccessCount, + processACLsCount, expectedSuccessCount, + ) + } + return false, nil + }) + + h.StartDps(service.WithRequestHandler(h)) + defer h.StopDps() + + // DPS provisioning should fail and reprovisioning should be triggered + shortCtx, shortCancel := context.WithTimeout(context.Background(), time.Second*20) + defer shortCancel() + shortCtx = kitNetGrpc.CtxWithToken(shortCtx, token) + err = h.Verify(shortCtx) + require.Error(t, err) +} + +type checkAuthCountsFn func(verifyCertificateCount, verifyConnectionCount uint64) bool + +type testRequestHandlerWithAuthCounter struct { + test.RequestHandlerWithDps + a service.AuthHandler + verifyCertificateCounter atomic.Uint64 + verifyConnectionCounter atomic.Uint64 + checkAuthCounts checkAuthCountsFn + checkFinalAuthCounts checkAuthCountsFn + service.RequestHandle +} + +func newTestRequestHandlerWithAuthCounter(t *testing.T, dpsCfg service.Config, egCache *service.EnrollmentGroupsCache, checkAuthCounts, checkFinalAuthCounts checkAuthCountsFn) *testRequestHandlerWithAuthCounter { + return &testRequestHandlerWithAuthCounter{ + RequestHandlerWithDps: test.MakeRequestHandlerWithDps(t, dpsCfg), + a: service.MakeDefaultAuthHandler(dpsCfg, egCache), + checkAuthCounts: checkAuthCounts, + checkFinalAuthCounts: checkFinalAuthCounts, + } +} + +func (h *testRequestHandlerWithAuthCounter) GetChainsCache() *cache.Cache[uint64, [][]*x509.Certificate] { + return h.a.GetChainsCache() +} + +func (h *testRequestHandlerWithAuthCounter) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + h.verifyCertificateCounter.Inc() + return h.a.VerifyPeerCertificate(rawCerts, verifiedChains) +} + +func (h *testRequestHandlerWithAuthCounter) VerifyConnection(cs tls.ConnectionState) error { + h.verifyConnectionCounter.Inc() + return h.a.VerifyConnection(cs) +} + +func (h *testRequestHandlerWithAuthCounter) verify(ctx context.Context) error { + logCounter := 0 + for h.checkAuthCounts(h.verifyCertificateCounter.Load(), h.verifyConnectionCounter.Load()) { + select { + case <-ctx.Done(): + return errors.New("verification timed out") + case <-time.After(time.Second): + logCounter++ + if logCounter%3 == 0 { + h.Logf("verifyCertificateCounter=%v verifyConnectionCounter=%v", + h.verifyCertificateCounter.Load(), + h.verifyConnectionCounter.Load()) + } + } + } + + h.Logf("final verifyCertificateCounter=%v verifyConnectionCounter=%v", + h.verifyCertificateCounter.Load(), + h.verifyConnectionCounter.Load()) + if !h.checkFinalAuthCounts(h.verifyCertificateCounter.Load(), h.verifyConnectionCounter.Load()) { + return fmt.Errorf("unexpected counters verifyCertificateCounter=%v verifyConnectionCounter=%v", + h.verifyCertificateCounter.Load(), + h.verifyConnectionCounter.Load()) + } + return nil +} + +func TestProvisioningWithDeletedEnrollmentGroup(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesCertificateAuthority|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId| + hubTestService.SetUpServicesCoapGateway|hubTestService.SetUpServicesResourceAggregate|hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesGrpcGateway) + defer hubShutdown() + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + token := oauthTest.GetDefaultAccessToken(t) + ctx = kitNetGrpc.CtxWithToken(ctx, token) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := grpcPb.NewGrpcGatewayClient(conn) + + dpsCfg := test.MakeConfig(t) + dpsShutDown := test.New(t, dpsCfg) + deviceID := hubTest.MustFindDeviceByName(test.TestDeviceObtName) + deviceID, shutdownSim := test.OnboardDpsSim(ctx, t, c, deviceID, dpsCfg.APIs.COAP.Addr, test.TestDevsimResources) + defer shutdownSim() + dpsShutDown() + + subClient, err := client.New(c).GrpcGatewayClient().SubscribeToEvents(ctx) + require.NoError(t, err) + defer func(s grpcPb.GrpcGateway_SubscribeToEventsClient) { + errC := s.CloseSend() + require.NoError(t, errC) + }(subClient) + + subID, corID := test.SubscribeToEvents(t, subClient, &grpcPb.SubscribeToEvents{ + CorrelationId: "deviceOnline", + Action: &grpcPb.SubscribeToEvents_CreateSubscription_{ + CreateSubscription: &grpcPb.SubscribeToEvents_CreateSubscription{ + EventFilter: []grpcPb.SubscribeToEvents_CreateSubscription_Event{ + grpcPb.SubscribeToEvents_CreateSubscription_DEVICE_METADATA_UPDATED, + }, + }, + }, + }) + + err = test.ForceReprovision(ctx, c, deviceID) + require.NoError(t, err) + + dpsCfg.EnrollmentGroups = nil + store, storeTearDown := test.NewMongoStore(t) + defer storeTearDown() + count, err := store.DeleteEnrollmentGroups(ctx, test.DPSOwner, &pb.GetEnrollmentGroupsRequest{}) + require.NoError(t, err) + require.Equal(t, int64(1), count) + count, err = store.DeleteHubs(ctx, test.DPSOwner, &pb.GetHubsRequest{}) + require.NoError(t, err) + require.Equal(t, int64(1), count) + ah := newTestRequestHandlerWithAuthCounter(t, dpsCfg, service.NewEnrollmentGroupsCache(ctx, time.Minute, store, log.Get()), func(verifyCertificateCount, _ uint64) bool { + // verification of certificates was tried multiple times + return verifyCertificateCount < 2 + }, func(_, verifyConnectionCount uint64) bool { + // not verification succeeded, so no connection was ever established + return verifyConnectionCount == 0 + }) + dpsShutDown = test.New(t, dpsCfg, service.WithAuthHandler(ah)) + err = ah.verify(ctx) + require.NoError(t, err) + + dpsShutDown() + + dpsCfg = test.MakeConfig(t) + h := newTestRequestHandler(t, dpsCfg, defaultTestDpsHandlerConfig()) + h.StartDps(service.WithRequestHandler(h)) + defer h.StopDps() + err = h.Verify(ctx) + require.NoError(t, err) + + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_OFFLINE) + require.NoError(t, err) + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_ONLINE) + require.NoError(t, err) +} diff --git a/device-provisioning-service/service/provisionOwnership_test.go b/device-provisioning-service/service/provisionOwnership_test.go new file mode 100644 index 000000000..e6a8396e0 --- /dev/null +++ b/device-provisioning-service/service/provisionOwnership_test.go @@ -0,0 +1,150 @@ +package service_test + +import ( + "context" + "crypto/tls" + "testing" + "time" + + "github.com/plgd-dev/device/v2/schema/acl" + "github.com/plgd-dev/device/v2/schema/configuration" + "github.com/plgd-dev/device/v2/schema/credential" + "github.com/plgd-dev/device/v2/schema/device" + "github.com/plgd-dev/device/v2/schema/platform" + "github.com/plgd-dev/device/v2/schema/resources" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + "github.com/plgd-dev/hub/v2/grpc-gateway/client" + grpcPb "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + isEvents "github.com/plgd-dev/hub/v2/identity-store/events" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/plgd-dev/hub/v2/test/device/ocf" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/sdk" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func TestInvalidOwner(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUp(context.Background(), t) + defer hubShutdown() + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := grpcPb.NewGrpcGatewayClient(conn) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + token := oauthTest.GetDefaultAccessToken(t) + ctx = kitNetGrpc.CtxWithToken(ctx, token) + + corID := "allEvents" + subClient, subID := test.SubscribeToAllEvents(ctx, t, c, corID) + + dpsCfg := test.MakeConfig(t) + dpsShutDown := test.New(t, dpsCfg) + defer dpsShutDown() + deviceID := hubTest.MustFindDeviceByName(test.TestDeviceObtName) + deviceID, shutdownSim := test.OnboardDpsSim(ctx, t, c, deviceID, dpsCfg.APIs.COAP.Addr, test.TestDevsimResources) + defer shutdownSim() + + err = test.ForceReprovision(ctx, c, deviceID) + require.NoError(t, err) + hubTest.WaitForDevice(t, subClient, ocf.NewDevice(deviceID, test.TestDeviceObtName), subID, corID, test.TestDevsimResources) + err = subClient.CloseSend() + require.NoError(t, err) + + subClient, err = client.New(c).GrpcGatewayClient().SubscribeToEvents(ctx) + require.NoError(t, err) + defer func(s grpcPb.GrpcGateway_SubscribeToEventsClient) { + errC := s.CloseSend() + require.NoError(t, errC) + }(subClient) + subID, corID = test.SubscribeToEvents(t, subClient, &grpcPb.SubscribeToEvents{ + CorrelationId: "deviceOnline", + Action: &grpcPb.SubscribeToEvents_CreateSubscription_{ + CreateSubscription: &grpcPb.SubscribeToEvents_CreateSubscription{ + EventFilter: []grpcPb.SubscribeToEvents_CreateSubscription_Event{ + grpcPb.SubscribeToEvents_CreateSubscription_DEVICE_METADATA_UPDATED, + }, + }, + }, + }) + + err = test.ForceReprovision(ctx, c, deviceID) + require.NoError(t, err) + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_OFFLINE) + require.NoError(t, err) + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_ONLINE) + require.NoError(t, err) + + const fakeOwner = "1337" + dc, err := sdk.NewClient(sdk.WithID(isEvents.OwnerToUUID(fakeOwner))) + require.NoError(t, err) + defer func() { + _ = dc.Close(ctx) + }() + + // /oic/d, /oic/p, /oic/res are readable to all + err = dc.GetResource(ctx, deviceID, device.ResourceURI, nil) + require.NoError(t, err) + err = dc.GetResource(ctx, deviceID, platform.ResourceURI, nil) + require.NoError(t, err) + err = dc.GetResource(ctx, deviceID, resources.ResourceURI, nil) + require.NoError(t, err) + + dpsOwnerUUID := isEvents.OwnerToUUID(test.DPSOwner) + // everything else should fail + // - get / update creds + err = dc.GetResource(ctx, deviceID, credential.ResourceURI, nil) + require.Error(t, err) + err = dc.UpdateResource(ctx, deviceID, credential.ResourceURI, credential.CredentialUpdateRequest{ + ResourceOwner: dpsOwnerUUID, + Credentials: []credential.Credential{{ + Type: credential.CredentialType_EMPTY, + }}, + }, nil) + require.Error(t, err) + + // - get / update ACLs + err = dc.GetResource(ctx, deviceID, acl.ResourceURI, nil) + require.Error(t, err) + err = dc.UpdateResource(ctx, deviceID, credential.ResourceURI, acl.UpdateRequest{ + AccessControlList: []acl.AccessControl{ + { + Permission: acl.Permission_WRITE, + Resources: []acl.Resource{ + { + Href: test.ResourcePlgdDpsHref, + }, + }, + Subject: acl.Subject{ + Subject_Device: &acl.Subject_Device{ + DeviceID: dpsOwnerUUID, + }, + }, + }, + }, + }, nil) + require.Error(t, err) + + // - get cloud cfg + err = dc.GetResource(ctx, deviceID, configuration.ResourceURI, nil) + require.Error(t, err) + + // - get / update dps + err = dc.GetResource(ctx, deviceID, test.ResourcePlgdDpsHref, nil) + require.Error(t, err) + err = dc.UpdateResource(ctx, deviceID, test.ResourcePlgdDpsHref, test.ResourcePlgdDps{ForceReprovision: true}, nil) + require.Error(t, err) +} diff --git a/device-provisioning-service/service/provisionRecovery_test.go b/device-provisioning-service/service/provisionRecovery_test.go new file mode 100644 index 000000000..25e75e561 --- /dev/null +++ b/device-provisioning-service/service/provisionRecovery_test.go @@ -0,0 +1,196 @@ +package service_test + +import ( + "context" + "crypto/tls" + "fmt" + "testing" + "time" + + coapCodes "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/plgd-dev/go-coap/v3/message/status" + "github.com/plgd-dev/go-coap/v3/mux" + caService "github.com/plgd-dev/hub/v2/certificate-authority/test" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + "github.com/plgd-dev/hub/v2/grpc-gateway/client" + "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "go.uber.org/atomic" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +type testDpsHandler interface { + service.RequestHandler + Cfg() service.Config + StartDps(opts ...service.Option) + RestartDps(opts ...service.Option) + StopDps() + Verify(ctx context.Context) error + Logf(template string, args ...interface{}) +} + +func testProvisioningWithDPSHandler(t *testing.T, h testDpsHandler, timeout time.Duration, onboardingOpts ...test.Option) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId| + hubTestService.SetUpServicesCoapGateway|hubTestService.SetUpServicesResourceAggregate|hubTestService.SetUpServicesGrpcGateway) + defer hubShutdown() + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := pb.NewGrpcGatewayClient(conn) + + caCfg := caService.MakeConfig(t) + caCfg.Signer.ExpiresIn = time.Hour * 10 + caShutdown := caService.New(t, caCfg) + defer caShutdown() + + deviceID := hubTest.MustFindDeviceByName(test.TestDeviceObtName) + + subClient, err := client.New(c).GrpcGatewayClient().SubscribeToEvents(ctx) + require.NoError(t, err) + defer func() { + errC := subClient.CloseSend() + require.NoError(t, errC) + }() + subID, corID := test.SubscribeToEvents(t, subClient, &pb.SubscribeToEvents{ + CorrelationId: "deviceOnline", + Action: &pb.SubscribeToEvents_CreateSubscription_{ + CreateSubscription: &pb.SubscribeToEvents_CreateSubscription{ + EventFilter: []pb.SubscribeToEvents_CreateSubscription_Event{ + pb.SubscribeToEvents_CreateSubscription_DEVICE_METADATA_UPDATED, + }, + }, + }, + }) + + h.StartDps(service.WithRequestHandler(h)) + defer h.StopDps() + deviceID, shutdownSim := test.OnboardDpsSim(ctx, t, c, deviceID, h.Cfg().APIs.COAP.Addr, test.TestDevsimResources, onboardingOpts...) + defer shutdownSim() + + err = h.Verify(ctx) + require.NoError(t, err) + + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_ONLINE) + require.NoError(t, err) +} + +type testRequestHandlerWithDisconnect struct { + test.RequestHandlerWithDps + disconnectTime atomic.Bool + disconnectOwnership atomic.Bool + disconnectedCredentials atomic.Bool + disconnectedACLs atomic.Bool + disconnectedCloudConfiguration atomic.Bool + r service.RequestHandle +} + +func newTestRequestHandlerWithDisconnect(t *testing.T, dpsCfg service.Config) *testRequestHandlerWithDisconnect { + return &testRequestHandlerWithDisconnect{ + RequestHandlerWithDps: test.MakeRequestHandlerWithDps(t, dpsCfg), + } +} + +func (h *testRequestHandlerWithDisconnect) DefaultHandler(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + return h.r.DefaultHandler(ctx, req, session, linkedHubs, group) +} + +func (h *testRequestHandlerWithDisconnect) ProcessPlgdTime(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if h.disconnectTime.CompareAndSwap(false, true) { + err := session.Close() + require.NoError(h.T(), err) + return nil, status.Errorf(service.NewMessageWithCode(coapCodes.ServiceUnavailable), "ProcessPlgdTime: disconnect") + } + return h.r.ProcessPlgdTime(ctx, req, session, linkedHubs, group) +} + +func (h *testRequestHandlerWithDisconnect) ProcessOwnership(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if h.disconnectOwnership.CompareAndSwap(false, true) { + err := session.Close() + require.NoError(h.T(), err) + return nil, status.Errorf(service.NewMessageWithCode(coapCodes.ServiceUnavailable), "ProcessOwnership: disconnect") + } + return h.r.ProcessOwnership(ctx, req, session, linkedHubs, group) +} + +func (h *testRequestHandlerWithDisconnect) ProcessCredentials(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if h.disconnectedCredentials.CompareAndSwap(false, true) { + err := session.Close() + require.NoError(h.T(), err) + return nil, status.Errorf(service.NewMessageWithCode(coapCodes.ServiceUnavailable), "ProcessCredentials: disconnect") + } + return h.r.ProcessCredentials(ctx, req, session, linkedHubs, group) +} + +func (h *testRequestHandlerWithDisconnect) ProcessACLs(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if h.disconnectedACLs.CompareAndSwap(false, true) { + err := session.Close() + require.NoError(h.T(), err) + return nil, status.Errorf(service.NewMessageWithCode(coapCodes.ServiceUnavailable), "ProcessACLs: disconnect") + } + return h.r.ProcessACLs(ctx, req, session, linkedHubs, group) +} + +func (h *testRequestHandlerWithDisconnect) ProcessCloudConfiguration(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if h.disconnectedCloudConfiguration.CompareAndSwap(false, true) { + err := session.Close() + require.NoError(h.T(), err) + return nil, status.Errorf(service.NewMessageWithCode(coapCodes.ServiceUnavailable), "ProcessCloudConfiguration: disconnect") + } + return h.r.ProcessCloudConfiguration(ctx, req, session, linkedHubs, group) +} + +func (h *testRequestHandlerWithDisconnect) Verify(ctx context.Context) error { + logCounter := 0 + for { + select { + case <-ctx.Done(): + return fmt.Errorf("unexpected counters disconnectTime=%v disconnectedOwnership=%v disconnectedCredentials=%v disconnectedACLs=%v disconnectedCloudConfiguration=%v", + h.disconnectTime.Load(), + h.disconnectOwnership.Load(), + h.disconnectedCredentials.Load(), + h.disconnectedACLs.Load(), + h.disconnectedCloudConfiguration.Load()) + case <-time.After(time.Second): + logCounter++ + if logCounter%3 == 0 { + h.Logf("disconnectTime=%v disconnectedOwnership=%v disconnectedCredentials=%v disconnectedACLs=%v disconnectedCloudConfiguration=%v", + h.disconnectTime.Load(), + h.disconnectOwnership.Load(), + h.disconnectedCredentials.Load(), + h.disconnectedACLs.Load(), + h.disconnectedCloudConfiguration.Load()) + } + } + if h.disconnectTime.Load() && h.disconnectOwnership.Load() && + h.disconnectedCredentials.Load() && + h.disconnectedACLs.Load() && + h.disconnectedCloudConfiguration.Load() { + return nil + } + } +} + +func TestProvisioningWithDisconnect(t *testing.T) { + dpsCfg := test.MakeConfig(t) + rh := newTestRequestHandlerWithDisconnect(t, dpsCfg) + testProvisioningWithDPSHandler(t, rh, time.Minute) +} diff --git a/device-provisioning-service/service/provisionRestart_test.go b/device-provisioning-service/service/provisionRestart_test.go new file mode 100644 index 000000000..140a63585 --- /dev/null +++ b/device-provisioning-service/service/provisionRestart_test.go @@ -0,0 +1,109 @@ +package service_test + +import ( + "context" + "crypto/tls" + "testing" + "time" + + "github.com/plgd-dev/go-coap/v3/message/codes" + caService "github.com/plgd-dev/hub/v2/certificate-authority/test" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + "github.com/plgd-dev/hub/v2/grpc-gateway/client" + "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +// A provisioned device stores data into storage, so when the device process is restarted +// no reprovisioning should be done. The data should be loaded from storage and only cloud +// events should be republished. +func TestReprovisioningAfterRestart(t *testing.T) { + const failLimit = uint64(0) + const expectedTimeCount = failLimit + 1 + const expectedOwnershipCount = failLimit + 1 + const expectedCloudConfigurationCount = failLimit + 1 + const expectedCredentialsCount = failLimit + 1 + const expectedACLsCount = failLimit + 1 + h := test.NewRequestHandlerWithExpectedCounters(t, failLimit, codes.InternalServerError, expectedTimeCount, expectedOwnershipCount, expectedCloudConfigurationCount, expectedCredentialsCount, expectedACLsCount) + + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId| + hubTestService.SetUpServicesCoapGateway|hubTestService.SetUpServicesResourceAggregate|hubTestService.SetUpServicesGrpcGateway) + defer hubShutdown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*2) + defer cancel() + ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := pb.NewGrpcGatewayClient(conn) + + caCfg := caService.MakeConfig(t) + caCfg.Signer.ExpiresIn = time.Hour * 10 + caShutdown := caService.New(t, caCfg) + defer caShutdown() + + dpsShutDown := test.New(t, h.Cfg()) + deviceID := hubTest.MustFindDeviceByName(test.TestDeviceObtName) + deviceID, shutdownSim := test.OnboardDpsSim(ctx, t, c, deviceID, h.Cfg().APIs.COAP.Addr, test.TestDevsimResources) + defer shutdownSim() + dpsShutDown() + + err = test.ForceReprovision(ctx, c, deviceID) + require.NoError(t, err) + + subClient, err := client.New(c).GrpcGatewayClient().SubscribeToEvents(ctx) + require.NoError(t, err) + defer func() { + errC := subClient.CloseSend() + require.NoError(t, errC) + }() + subID, corID := test.SubscribeToEvents(t, subClient, &pb.SubscribeToEvents{ + CorrelationId: "deviceOnline", + Action: &pb.SubscribeToEvents_CreateSubscription_{ + CreateSubscription: &pb.SubscribeToEvents_CreateSubscription{ + EventFilter: []pb.SubscribeToEvents_CreateSubscription_Event{ + pb.SubscribeToEvents_CreateSubscription_DEVICE_METADATA_UPDATED, + }, + }, + }, + }) + + h.StartDps(service.WithRequestHandler(h)) + defer h.StopDps() + + err = h.Verify(ctx) + require.NoError(t, err) + + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_OFFLINE) + require.NoError(t, err) + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_ONLINE) + require.NoError(t, err) + + h.Logf("restarting device") + err = test.RestartDockerContainer(test.TestDockerObtContainerName) + require.NoError(t, err) + + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_OFFLINE) + require.NoError(t, err) + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_ONLINE) + require.NoError(t, err) + + err = h.Verify(ctx) + require.NoError(t, err) +} diff --git a/device-provisioning-service/service/provisionRetry_test.go b/device-provisioning-service/service/provisionRetry_test.go new file mode 100644 index 000000000..9d8916e10 --- /dev/null +++ b/device-provisioning-service/service/provisionRetry_test.go @@ -0,0 +1,178 @@ +package service_test + +import ( + "context" + "crypto/tls" + "fmt" + "testing" + "time" + + "github.com/plgd-dev/go-coap/v3/message/codes" + caService "github.com/plgd-dev/hub/v2/certificate-authority/test" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + "github.com/plgd-dev/hub/v2/grpc-gateway/client" + "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +type testDpsHandlerConfig struct { + failLimit uint64 + failCode codes.Code + expectedCounts map[test.HandlerID]uint64 +} + +func defaultTestDpsHandlerConfig() testDpsHandlerConfig { + return testDpsHandlerConfig{ + failLimit: 0, + failCode: codes.ServiceUnavailable, + expectedCounts: map[test.HandlerID]uint64{ + test.HandlerIDDefault: 0, + test.HandlerIDTime: 1, + test.HandlerIDOwnership: 1, + test.HandlerIDCredentials: 1, + test.HandlerIDACLs: 1, + test.HandlerIDCloudConfiguration: 1, + }, + } +} + +func newTestRequestHandler(t *testing.T, dpsCfg service.Config, handlerCfg testDpsHandlerConfig) *test.RequestHandlerWithCounter { + return test.NewRequestHandlerWithCounter(t, dpsCfg, func(h test.HandlerID, count uint64) codes.Code { + switch h { + case test.HandlerIDTime, test.HandlerIDOwnership, test.HandlerIDCredentials, test.HandlerIDACLs, test.HandlerIDCloudConfiguration: + if count < handlerCfg.failLimit { + return handlerCfg.failCode + } + case test.HandlerIDDefault: + } + return 0 + }, func(defaultHandlerCount, processTimeCount, processOwnershipCount, processCloudConfigurationCount, processCredentialsCount, processACLsCount uint64, + ) (bool, error) { + expDefaultHandlerCount := handlerCfg.expectedCounts[test.HandlerIDDefault] + expTimeCount := handlerCfg.expectedCounts[test.HandlerIDTime] + expOwnershipCount := handlerCfg.expectedCounts[test.HandlerIDOwnership] + expCloudConfigurationCount := handlerCfg.expectedCounts[test.HandlerIDCloudConfiguration] + expCredentialsCount := handlerCfg.expectedCounts[test.HandlerIDCredentials] + expACLsCount := handlerCfg.expectedCounts[test.HandlerIDACLs] + if defaultHandlerCount > expDefaultHandlerCount || + processTimeCount > expTimeCount || + processOwnershipCount > expOwnershipCount || + processCloudConfigurationCount > expCloudConfigurationCount || + processCredentialsCount > expCredentialsCount || + processACLsCount > expACLsCount { + return false, fmt.Errorf("invalid counters: defaultHandlerCounter=(%v:%v) processTimeCount=(%v:%v) processOwnershipCounter=(%v:%v) processCloudConfigurationCounter=(%v:%v) processCredentialsCounter=(%v:%v) processACLsCounter=(%v:%v)", + defaultHandlerCount, expDefaultHandlerCount, + processTimeCount, expTimeCount, + processOwnershipCount, expOwnershipCount, + processCloudConfigurationCount, expCloudConfigurationCount, + processCredentialsCount, expCredentialsCount, + processACLsCount, expACLsCount) + } + return defaultHandlerCount == expDefaultHandlerCount && + processTimeCount == expTimeCount && + processOwnershipCount == expOwnershipCount && + processCloudConfigurationCount == expCloudConfigurationCount && + processCredentialsCount == expCredentialsCount && + processACLsCount == expACLsCount, nil + }) +} + +func TestForceReprovisioning(t *testing.T) { + dpsCfg := test.MakeConfig(t) + rh := newTestRequestHandler(t, dpsCfg, defaultTestDpsHandlerConfig()) + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId| + hubTestService.SetUpServicesCoapGateway|hubTestService.SetUpServicesResourceAggregate|hubTestService.SetUpServicesGrpcGateway) + defer hubShutdown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := pb.NewGrpcGatewayClient(conn) + + caCfg := caService.MakeConfig(t) + caCfg.Signer.ExpiresIn = time.Hour * 10 + caShutdown := caService.New(t, caCfg) + defer caShutdown() + + deviceID := hubTest.MustFindDeviceByName(test.TestDeviceObtName) + dpsShutDown := test.New(t, rh.Cfg()) + deviceID, shutdownSim := test.OnboardDpsSim(ctx, t, c, deviceID, rh.Cfg().APIs.COAP.Addr, test.TestDevsimResources) + defer shutdownSim() + dpsShutDown() + + subClient, err := client.New(c).GrpcGatewayClient().SubscribeToEvents(ctx) + require.NoError(t, err) + defer func() { + errC := subClient.CloseSend() + require.NoError(t, errC) + }() + + err = test.ForceReprovision(ctx, c, deviceID) + require.NoError(t, err) + + subID, corID := test.SubscribeToEvents(t, subClient, &pb.SubscribeToEvents{ + CorrelationId: "deviceOnline", + Action: &pb.SubscribeToEvents_CreateSubscription_{ + CreateSubscription: &pb.SubscribeToEvents_CreateSubscription{ + EventFilter: []pb.SubscribeToEvents_CreateSubscription_Event{ + pb.SubscribeToEvents_CreateSubscription_DEVICE_METADATA_UPDATED, + }, + }, + }, + }) + + rh.StartDps(service.WithRequestHandler(rh)) + defer rh.StopDps() + + err = rh.Verify(ctx) + require.NoError(t, err) + + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_OFFLINE) + require.NoError(t, err) + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_ONLINE) + require.NoError(t, err) +} + +func TestProvisioningTransientFailureRetry(t *testing.T) { + const transientFailLimit = uint64(3) + // the testing device is configured to force full reprovisioning after 3 transient failures of a single step, + // after the first 3 failures of a step it will then always succeeds, so full reprovisioning will be forced 3 times, + // resulting in the following counts + const expectedTimeCount = transientFailLimit + 5 + const expectedOwnershipCount = transientFailLimit + 4 + const expectedCloudConfigurationCount = transientFailLimit + 3 + const expectedCredentialsCount = transientFailLimit + 2 + const expectedACLsCount = transientFailLimit + 1 + rh := test.NewRequestHandlerWithExpectedCounters(t, transientFailLimit, codes.ServiceUnavailable, expectedTimeCount, expectedOwnershipCount, expectedCloudConfigurationCount, expectedCredentialsCount, expectedACLsCount) + testProvisioningWithDPSHandler(t, rh, 5*time.Minute) +} + +func TestProvisioningRetry(t *testing.T) { + const failLimit = uint64(1) + // non transient failure forces full reprovisioning, even previously successful steps are retried + const expectedTimeCount = failLimit + 5 + const expectedOwnershipCount = failLimit + 4 + const expectedCloudConfigurationCount = failLimit + 3 + const expectedCredentialsCount = failLimit + 2 + const expectedACLsCount = failLimit + 1 + rh := test.NewRequestHandlerWithExpectedCounters(t, failLimit, codes.InternalServerError, expectedTimeCount, expectedOwnershipCount, expectedCloudConfigurationCount, expectedCredentialsCount, expectedACLsCount) + testProvisioningWithDPSHandler(t, rh, 3*time.Minute) +} diff --git a/device-provisioning-service/service/provision_test.go b/device-provisioning-service/service/provision_test.go new file mode 100644 index 000000000..e7dbccace --- /dev/null +++ b/device-provisioning-service/service/provision_test.go @@ -0,0 +1,556 @@ +package service_test + +import ( + "context" + "crypto/tls" + "errors" + "io" + "net/http" + "os" + "strconv" + "testing" + "time" + + "github.com/google/uuid" + deviceClient "github.com/plgd-dev/device/v2/client" + "github.com/plgd-dev/device/v2/schema/credential" + "github.com/plgd-dev/device/v2/schema/interfaces" + caService "github.com/plgd-dev/hub/v2/certificate-authority/test" + coapgwTest "github.com/plgd-dev/hub/v2/coap-gateway/test" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service" + httpService "github.com/plgd-dev/hub/v2/device-provisioning-service/service/http" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + "github.com/plgd-dev/hub/v2/device-provisioning-service/uri" + "github.com/plgd-dev/hub/v2/grpc-gateway/client" + grpcPb "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" + "github.com/plgd-dev/hub/v2/identity-store/events" + "github.com/plgd-dev/hub/v2/pkg/config/property/urischeme" + kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/plgd-dev/hub/v2/test/device/ocf" + httpTest "github.com/plgd-dev/hub/v2/test/http" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/sdk" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +const ( + DPSCoapGwHost = "localhost:40002" + DPSHost = "localhost:20030" +) + +func TestProvisioning(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId| + hubTestService.SetUpServicesResourceAggregate|hubTestService.SetUpServicesGrpcGateway) + defer hubShutdown() + + err := test.SendSignalToDocker(test.TestDockerContainerName, "HUP") + require.NoError(t, err) + time.Sleep(time.Second) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + token := oauthTest.GetDefaultAccessToken(t) + ctx = kitNetGrpc.CtxWithToken(ctx, token) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := grpcPb.NewGrpcGatewayClient(conn) + + corID := "allEvents" + subClient, subID := test.SubscribeToAllEvents(ctx, t, c, corID) + + coapgwCfg := coapgwTest.MakeConfig(t) + coapgwCfg.APIs.COAP.Addr = DPSCoapGwHost + coapgwCfg.APIs.COAP.ExternalAddress = DPSCoapGwHost + coapgwShutdown := coapgwTest.New(t, coapgwCfg) + defer coapgwShutdown() + + caCfg := caService.MakeConfig(t) + caCfg.Signer.ExpiresIn = time.Hour * 10 + caShutdown := caService.New(t, caCfg) + defer caShutdown() + + dpcCfg := test.MakeConfig(t) + dpcCfg.APIs.COAP.Addr = DPSHost + dpcCfg.APIs.HTTP.Connection.TLS.ClientCertificateRequired = false + dpcCfg.EnrollmentGroups[0].Hubs[0].Gateways = []string{coapgwCfg.APIs.COAP.Addr} + dpsShutDown := test.New(t, dpcCfg) + defer dpsShutDown() + + deviceID := hubTest.MustFindDeviceByName(test.TestDeviceName) + hubTest.WaitForDevice(t, subClient, ocf.NewDevice(deviceID, test.TestDeviceName), subID, corID, test.TestDevsimResources) + err = subClient.CloseSend() + require.NoError(t, err) + + request := httpgwTest.NewRequest(http.MethodGet, httpService.ProvisioningRecords, nil). + Host(test.DPSHTTPHost).AuthToken(token).Build() + resp := httpgwTest.HTTPDo(t, request) + defer func() { + _ = resp.Body.Close() + }() + + var got []*pb.ProvisioningRecord + for { + var dev pb.ProvisioningRecord + err := httpTest.Unmarshal(resp.StatusCode, resp.Body, &dev) + if errors.Is(err, io.EOF) { + break + } + require.NoError(t, err) + got = append(got, &dev) + } + require.Len(t, got, 1) + require.NotEmpty(t, got[0].GetId()) + require.NotEmpty(t, got[0].GetDeviceId()) + require.NotEmpty(t, got[0].GetEnrollmentGroupId()) + require.NotEmpty(t, got[0].GetCreationDate()) + require.NotEmpty(t, got[0].GetLocalEndpoints()) + require.NotEmpty(t, got[0].GetAcl().GetAccessControlList()) + require.NotEmpty(t, got[0].GetAcl().GetStatus().GetDate()) + require.NotEmpty(t, got[0].GetAcl().GetStatus().GetCoapCode()) + require.Equal(t, "", got[0].GetAcl().GetStatus().GetErrorMessage()) + require.NotEmpty(t, got[0].GetCloud().GetStatus().GetDate()) + require.NotEmpty(t, got[0].GetCloud().GetStatus().GetCoapCode()) + require.Equal(t, "", got[0].GetCloud().GetStatus().GetErrorMessage()) + require.NotEmpty(t, got[0].GetCloud().GetGateways()) + require.NotEmpty(t, got[0].GetCloud().GetProviderName()) + require.NotEmpty(t, got[0].GetCloud().GetGateways()[0].GetId()) + require.NotEmpty(t, got[0].GetCloud().GetGateways()[0].GetUri()) + require.Equal(t, int32(0), got[0].GetCloud().GetSelectedGateway()) + require.NotEmpty(t, got[0].GetAttestation().GetDate()) + require.NotEmpty(t, got[0].GetAttestation().GetX509().GetCertificatePem()) + require.NotEmpty(t, got[0].GetCredential().GetStatus().GetDate()) + require.NotEmpty(t, got[0].GetCredential().GetStatus().GetCoapCode()) + require.NotEmpty(t, got[0].GetCredential().GetIdentityCertificatePem()) + require.NotEmpty(t, got[0].GetCredential().GetCredentials()) + require.Equal(t, "", got[0].GetCredential().GetStatus().GetErrorMessage()) + require.NotEmpty(t, got[0].GetOwnership().GetOwner()) + require.NotEmpty(t, got[0].GetOwnership().GetStatus().GetDate()) + require.NotEmpty(t, got[0].GetOwnership().GetStatus().GetCoapCode()) + require.Equal(t, "", got[0].GetOwnership().GetStatus().GetErrorMessage()) + require.NotEmpty(t, got[0].GetPlgdTime().GetDate()) + require.NotEmpty(t, got[0].GetPlgdTime().GetCoapCode()) + require.Equal(t, "", got[0].GetPlgdTime().GetErrorMessage()) +} + +func TestProvisioningFactoryReset(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId| + hubTestService.SetUpServicesCoapGateway|hubTestService.SetUpServicesResourceAggregate|hubTestService.SetUpServicesGrpcGateway) + defer hubShutdown() + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := grpcPb.NewGrpcGatewayClient(conn) + + caCfg := caService.MakeConfig(t) + caCfg.Signer.ExpiresIn = time.Hour * 10 + caShutdown := caService.New(t, caCfg) + defer caShutdown() + + dpcCfg := test.MakeConfig(t) + dpsShutDown := test.New(t, dpcCfg) + defer dpsShutDown() + + deviceID := hubTest.MustFindDeviceByName(test.TestDeviceObtName) + + token := oauthTest.GetDefaultAccessToken(t) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + ctx = kitNetGrpc.CtxWithToken(ctx, token) + + deviceID, _ = test.OnboardDpsSim(ctx, t, c, deviceID, dpcCfg.APIs.COAP.Addr, test.TestDevsimResources) + + devClient, err := sdk.NewClient(sdk.WithID(events.OwnerToUUID(test.DPSOwner))) + require.NoError(t, err) + defer func() { + _ = devClient.Close(ctx) + }() + + err = devClient.UpdateResource(ctx, deviceID, test.ResourcePlgdDpsHref, test.ResourcePlgdDps{Endpoint: new(string)}, nil) + if err != nil && !errors.Is(err, context.Canceled) { + require.NoError(t, err) + } + time.Sleep(time.Second * 3) + + deviceID = hubTest.MustFindDeviceByName(test.TestDeviceObtName) + deviceID, err = devClient.OwnDevice(ctx, deviceID, deviceClient.WithOTM(deviceClient.OTMType_JustWorks)) + require.NoError(t, err) + defer func() { + _ = devClient.DisownDevice(ctx, deviceID) + }() + var resp test.ResourcePlgdDps + err = devClient.GetResource(ctx, deviceID, test.ResourcePlgdDpsHref, &resp) + require.NoError(t, err) + test.CleanUpDpsResource(&resp) + require.Equal(t, test.ResourcePlgdDps{ + Endpoint: new(string), + Interfaces: []string{interfaces.OC_IF_R, interfaces.OC_IF_RW, interfaces.OC_IF_BASELINE}, + ProvisionStatus: "uninitialized", + ResourceTypes: []string{test.ResourcePlgdDpsType}, + }, resp) +} + +func TestProvisioningWithCloudChange(t *testing.T) { + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId| + hubTestService.SetUpServicesResourceAggregate|hubTestService.SetUpServicesGrpcGateway) + defer hubShutdown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*2) + defer cancel() + ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := grpcPb.NewGrpcGatewayClient(conn) + + caCfg := caService.MakeConfig(t) + caCfg.Signer.ExpiresIn = time.Hour * 10 + caShutdown := caService.New(t, caCfg) + defer caShutdown() + + coapgwShutdown := coapgwTest.SetUp(t) + + dpsCfg := test.MakeConfig(t) + dpsShutDown := test.New(t, dpsCfg) + deviceID := hubTest.MustFindDeviceByName(test.TestDeviceObtName) + deviceID, shutdownSim := test.OnboardDpsSim(ctx, t, c, deviceID, dpsCfg.APIs.COAP.Addr, test.TestDevsimResources) + defer shutdownSim() + + err = test.ForceReprovision(ctx, c, deviceID) + require.NoError(t, err) + + subClient, err := client.New(c).GrpcGatewayClient().SubscribeToEvents(ctx) + require.NoError(t, err) + defer func() { + errC := subClient.CloseSend() + require.NoError(t, errC) + }() + subID, corID := test.SubscribeToEvents(t, subClient, &grpcPb.SubscribeToEvents{ + CorrelationId: "deviceOnline", + Action: &grpcPb.SubscribeToEvents_CreateSubscription_{ + CreateSubscription: &grpcPb.SubscribeToEvents_CreateSubscription{ + EventFilter: []grpcPb.SubscribeToEvents_CreateSubscription_Event{ + grpcPb.SubscribeToEvents_CreateSubscription_DEVICE_METADATA_UPDATED, + }, + }, + }, + }) + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_OFFLINE) + require.NoError(t, err) + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_ONLINE) + require.NoError(t, err) + + dpsShutDown() + coapgwShutdown() + // 10secs after connection to coap-gw is lost, DPS client should attempt full reprovision + + coapgwCfg := coapgwTest.MakeConfig(t) + coapgwCfg.APIs.COAP.Addr = DPSCoapGwHost + coapgwCfg.APIs.COAP.ExternalAddress = DPSCoapGwHost + coapgwShutdown = coapgwTest.New(t, coapgwCfg) + defer coapgwShutdown() + + store, storeTearDown := test.NewMongoStore(t) + defer storeTearDown() + count, err := store.DeleteEnrollmentGroups(ctx, test.DPSOwner, &pb.GetEnrollmentGroupsRequest{}) + require.NoError(t, err) + require.Equal(t, int64(1), count) + count, err = store.DeleteHubs(ctx, test.DPSOwner, &pb.GetHubsRequest{}) + require.NoError(t, err) + require.Equal(t, int64(1), count) + dpsCfg.EnrollmentGroups[0].Hubs[0].Gateways = []string{coapgwCfg.APIs.COAP.Addr} + dpsShutDown = test.New(t, dpsCfg) + defer dpsShutDown() + + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_OFFLINE) + require.NoError(t, err) + err = test.WaitForDeviceStatus(t, subClient, deviceID, subID, corID, commands.Connection_ONLINE) + require.NoError(t, err) +} + +func writeToTempFile(t *testing.T, fileName string, data []byte) string { + f, err := os.CreateTemp("", fileName) + require.NoError(t, err) + defer func() { + err = f.Close() + require.NoError(t, err) + }() + _, err = f.Write(data) + require.NoError(t, err) + return f.Name() +} + +func TestProvisioningWithPSK(t *testing.T) { + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId| + hubTestService.SetUpServicesResourceAggregate|hubTestService.SetUpServicesGrpcGateway) + defer hubShutdown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3600) + defer cancel() + ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := grpcPb.NewGrpcGatewayClient(conn) + + caCfg := caService.MakeConfig(t) + caCfg.Signer.ExpiresIn = time.Hour * 10 + caShutdown := caService.New(t, caCfg) + defer caShutdown() + + coapgwShutdown := coapgwTest.SetUp(t) + defer coapgwShutdown() + dpsCfg := test.MakeConfig(t) + psk := testPSK + pskFile := writeToTempFile(t, "psk.key", []byte(psk)) + defer func() { + err = os.Remove(pskFile) + require.NoError(t, err) + }() + dpsCfg.EnrollmentGroups[0].PreSharedKeyFile = urischeme.URIScheme(pskFile) + dpsCfg.APIs.HTTP.Connection.TLS.ClientCertificateRequired = false + dpsShutDown := test.New(t, dpsCfg) + defer dpsShutDown() + deviceID := hubTest.MustFindDeviceByName(test.TestDeviceObtName) + _, shutdownSim := test.OnboardDpsSim(ctx, t, c, deviceID, dpsCfg.APIs.COAP.Addr, test.TestDevsimResources) + defer shutdownSim() + + addr, err := findSecureUDPEndpoint(ctx) + require.NoError(t, err) + deviceAddr := addr.GetHostname() + ":" + strconv.FormatInt(int64(addr.GetPort()), 10) + + uuid.MustParse(events.OwnerToUUID(dpsCfg.EnrollmentGroups[0].Owner)) + subject, err := uuid.MustParse(events.OwnerToUUID(dpsCfg.EnrollmentGroups[0].Owner)).MarshalBinary() + require.NoError(t, err) + + pskConn, err := dialDTLS(ctx, deviceAddr, subject, []byte(psk)) + require.NoError(t, err) + defer func() { + err = pskConn.Close() + require.NoError(t, err) + }() + // get /oic/sec/cred -> succeeds + err = pskConn.GetResource(ctx, credential.ResourceURI, nil) + require.NoError(t, err) + + token := oauthTest.GetDefaultAccessToken(t) + request := httpgwTest.NewRequest(http.MethodGet, httpService.ProvisioningRecords, nil). + Host(test.DPSHTTPHost).AuthToken(token).Build() + resp := httpgwTest.HTTPDo(t, request) + defer func() { + _ = resp.Body.Close() + }() + + var got []*pb.ProvisioningRecord + for { + var dev pb.ProvisioningRecord + err := httpTest.Unmarshal(resp.StatusCode, resp.Body, &dev) + if errors.Is(err, io.EOF) { + break + } + require.NoError(t, err) + got = append(got, &dev) + } + require.Len(t, got, 1) + require.Equal(t, events.OwnerToUUID(dpsCfg.EnrollmentGroups[0].Owner), got[0].GetCredential().GetPreSharedKey().GetSubjectId()) + require.Equal(t, psk, got[0].GetCredential().GetPreSharedKey().GetKey()) +} + +func TestProvisioningFromNewDPSAddress(t *testing.T) { + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId| + hubTestService.SetUpServicesResourceAggregate|hubTestService.SetUpServicesGrpcGateway) + defer hubShutdown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := grpcPb.NewGrpcGatewayClient(conn) + + caCfg := caService.MakeConfig(t) + caCfg.Signer.ExpiresIn = time.Hour * 10 + caShutdown := caService.New(t, caCfg) + defer caShutdown() + + coapgwShutdown := coapgwTest.SetUp(t) + defer coapgwShutdown() + dpsCfg := test.MakeConfig(t) + psk := testPSK + pskFile := writeToTempFile(t, "psk.key", []byte(psk)) + defer func() { + err = os.Remove(pskFile) + require.NoError(t, err) + }() + dpsCfg.EnrollmentGroups[0].PreSharedKeyFile = urischeme.URIScheme(pskFile) + dpsCfg.APIs.HTTP.Connection.TLS.ClientCertificateRequired = false + dpsShutDown := test.New(t, dpsCfg) + + deviceID := hubTest.MustFindDeviceByName(test.TestDeviceObtName) + _, shutdownSim := test.OnboardDpsSim(ctx, t, c, deviceID, dpsCfg.APIs.COAP.Addr, test.TestDevsimResources) + defer shutdownSim() + + // change DPS to new address from "localhost:40030" to "localhost:50030" and restart DPS + dpsShutDown() + dpsCfg.APIs.COAP.Addr = "localhost:50030" + h := newTestRequestHandler(t, dpsCfg, defaultTestDpsHandlerConfig()) + h.StartDps(service.WithRequestHandler(h)) + defer h.StopDps() + + // connect to device via DTLS with PSK + addr, err := findSecureUDPEndpoint(ctx) + require.NoError(t, err) + deviceAddr := addr.GetHostname() + ":" + strconv.FormatInt(int64(addr.GetPort()), 10) + + subject, err := uuid.MustParse(events.OwnerToUUID(dpsCfg.EnrollmentGroups[0].Owner)).MarshalBinary() + require.NoError(t, err) + + pskConn, err := dialDTLS(ctx, deviceAddr, subject, []byte(psk)) + require.NoError(t, err) + defer func() { + err = pskConn.Close() + require.NoError(t, err) + }() + + // Update DPS address in device + endpoint := uri.CoAPsTCPSchemePrefix + dpsCfg.APIs.COAP.Addr + err = pskConn.UpdateResource(ctx, test.ResourcePlgdDpsHref, test.ResourcePlgdDps{Endpoint: &endpoint}, nil) + require.NoError(t, err) + + // Verify whether the device is connected to the new DPS address and retrieve the updated configurations + err = h.Verify(ctx) + require.NoError(t, err) +} + +// provide 2 coap-gateways in the cloud configuration with the same ID and only the second one is reachable, +// the device should connect to the second one after connecting to the first one fails; this is either done +// by cloud status observer when it detects that the cloud is not logged in after a number of checks or by +// the internal retry timeout in IoTivity +// +// the test is done by setting the cloud status observer to check the cloud status 3 times before trying the next one +// which is shorter than the retry timeout in IoTivity +func TestProvisiongConnectToSecondaryServerByObserver(t *testing.T) { + if !test.TestDeviceObtSupportsTestProperties { + t.Skip("TestDeviceObt does not support test properties") + } + // if cloud is not connected after 3 checks then the next one should be tried + setLowCloudObserverCheckCount := func(ctx context.Context, deviceID string, devClient *deviceClient.Client) error { + // the configuration will be reset by factoryReset when the test ends, so the original configuration will be restored + return devClient.UpdateResource(ctx, deviceID, test.ResourcePlgdDpsHref, test.ResourcePlgdDps{TestProperties: test.ResourcePlgdDpsTest{ + CloudStatusObserver: test.ResourcePlgdDpsTestCloudStatusObserver{ + MaxCount: 5, + }, + }}, nil) + } + dpsCfg := test.MakeConfig(t) + dpsCfg.EnrollmentGroups[0].Hubs[0].Gateways = []string{"localhost:20999"} // should be unreachable + dpsCfg.EnrollmentGroups[0].Hubs[0].Gateways = append(dpsCfg.EnrollmentGroups[0].Hubs[0].Gateways, config.COAP_GW_HOST) + rh := newTestRequestHandler(t, dpsCfg, defaultTestDpsHandlerConfig()) + testProvisioningWithDPSHandler(t, rh, time.Minute, test.WithConfigureDevice(setLowCloudObserverCheckCount)) +} + +// similar to TestProvisiongConnectToSecondaryServerByObserver but we setup the device that it the IoTivity retry timeout +// executes before the cloud status observer detects that the cloud is not logged in +func TestProvisiongConnectToSecondaryServerByRetryTimeout(t *testing.T) { + if !test.TestDeviceObtSupportsTestProperties { + t.Skip("TestDeviceObt does not support test properties") + } + setShortRetry := func(ctx context.Context, deviceID string, devClient *deviceClient.Client) error { + // the configuration will be reset by factoryReset when the test ends, so the original configuration will be restored + return devClient.UpdateResource(ctx, deviceID, test.ResourcePlgdDpsHref, test.ResourcePlgdDps{TestProperties: test.ResourcePlgdDpsTest{ + Iotivity: test.ResourcePlgdDpsTestIotivity{ + Retry: []int64{int64(5 * time.Second / time.Millisecond)}, + }, + }}, nil) + } + dpsCfg := test.MakeConfig(t) + dpsCfg.EnrollmentGroups[0].Hubs[0].Gateways = []string{"localhost:20999"} // should be unreachable + dpsCfg.EnrollmentGroups[0].Hubs[0].Gateways = append(dpsCfg.EnrollmentGroups[0].Hubs[0].Gateways, config.COAP_GW_HOST) + rh := newTestRequestHandler(t, dpsCfg, defaultTestDpsHandlerConfig()) + testProvisioningWithDPSHandler(t, rh, time.Minute, test.WithConfigureDevice(setShortRetry)) +} + +// provide 2 coap-gateways in the cloud configuration with different IDs and only the second one is reachable +// cloud observer should detect the problem and switch to the second one +func TestProvisiongConnectToSecondaryCloudByObserver(t *testing.T) { + if !test.TestDeviceObtSupportsTestProperties { + t.Skip("TestDeviceObt does not support test properties") + } + // if cloud is not connected after 3 checks then the next one should be tried + setLowCloudObserverCheckCount := func(ctx context.Context, deviceID string, devClient *deviceClient.Client) error { + // the configuration will be reset by factoryReset when the test ends, so the original configuration will be restored + return devClient.UpdateResource(ctx, deviceID, test.ResourcePlgdDpsHref, test.ResourcePlgdDps{TestProperties: test.ResourcePlgdDpsTest{ + CloudStatusObserver: test.ResourcePlgdDpsTestCloudStatusObserver{ + MaxCount: 5, + }, + }}, nil) + } + hubCfg := test.MakeHubConfig(uuid.NewString(), "localhost:20999") // should be unreachable + dpsCfg := test.MakeConfig(t) + dpsCfg.EnrollmentGroups[0].Hubs = append([]service.HubConfig{hubCfg}, dpsCfg.EnrollmentGroups[0].Hubs...) + rhCfg := defaultTestDpsHandlerConfig() + rhCfg.expectedCounts[test.HandlerIDCredentials] = 2 + rhCfg.expectedCounts[test.HandlerIDACLs] = 2 + rh := newTestRequestHandler(t, dpsCfg, rhCfg) + testProvisioningWithDPSHandler(t, rh, time.Minute, test.WithConfigureDevice(setLowCloudObserverCheckCount)) +} + +// provide 2 coap-gateways in the cloud configuration with different IDs and only the second one is reachable +// the iotivity retry mechanism should switch to the second one +func TestProvisiongConnectToSecondaryCloudByRetryTimeout(t *testing.T) { + if !test.TestDeviceObtSupportsTestProperties { + t.Skip("TestDeviceObt does not support test properties") + } + setShortRetry := func(ctx context.Context, deviceID string, devClient *deviceClient.Client) error { + // the configuration will be reset by factoryReset when the test ends, so the original configuration will be restored + return devClient.UpdateResource(ctx, deviceID, test.ResourcePlgdDpsHref, test.ResourcePlgdDps{TestProperties: test.ResourcePlgdDpsTest{ + Iotivity: test.ResourcePlgdDpsTestIotivity{ + Retry: []int64{int64(5 * time.Second / time.Millisecond)}, + }, + }}, nil) + } + hubCfg := test.MakeHubConfig(uuid.NewString(), "localhost:20999") // should be unreachable + dpsCfg := test.MakeConfig(t) + dpsCfg.EnrollmentGroups[0].Hubs = append([]service.HubConfig{hubCfg}, dpsCfg.EnrollmentGroups[0].Hubs...) + rhCfg := defaultTestDpsHandlerConfig() + rhCfg.expectedCounts[test.HandlerIDCredentials] = 2 + rhCfg.expectedCounts[test.HandlerIDACLs] = 2 + rh := newTestRequestHandler(t, dpsCfg, rhCfg) + testProvisioningWithDPSHandler(t, rh, time.Minute, test.WithConfigureDevice(setShortRetry)) +} diff --git a/device-provisioning-service/service/requestHandler.go b/device-provisioning-service/service/requestHandler.go new file mode 100644 index 000000000..1363882c4 --- /dev/null +++ b/device-provisioning-service/service/requestHandler.go @@ -0,0 +1,19 @@ +package service + +import ( + "context" + + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/plgd-dev/go-coap/v3/mux" +) + +type RequestHandler interface { + DefaultHandler(ctx context.Context, req *mux.Message, session *Session, linkedHub []*LinkedHub, group *EnrollmentGroup) (*pool.Message, error) + ProcessOwnership(ctx context.Context, req *mux.Message, session *Session, linkedHub []*LinkedHub, group *EnrollmentGroup) (*pool.Message, error) + ProcessCredentials(ctx context.Context, req *mux.Message, session *Session, linkedHub []*LinkedHub, group *EnrollmentGroup) (*pool.Message, error) + ProcessACLs(ctx context.Context, req *mux.Message, session *Session, linkedHub []*LinkedHub, group *EnrollmentGroup) (*pool.Message, error) + ProcessCloudConfiguration(ctx context.Context, req *mux.Message, session *Session, linkedHub []*LinkedHub, group *EnrollmentGroup) (*pool.Message, error) + ProcessPlgdTime(ctx context.Context, req *mux.Message, session *Session, linkedHub []*LinkedHub, group *EnrollmentGroup) (*pool.Message, error) +} + +type RequestHandle struct{} diff --git a/device-provisioning-service/service/service.go b/device-provisioning-service/service/service.go new file mode 100644 index 000000000..d3832daa1 --- /dev/null +++ b/device-provisioning-service/service/service.go @@ -0,0 +1,360 @@ +package service + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "net" + "time" + + "github.com/pion/dtls/v2" + "github.com/plgd-dev/device/v2/schema/plgdtime" + coapCodes "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/plgd-dev/go-coap/v3/message/status" + "github.com/plgd-dev/go-coap/v3/mux" + "github.com/plgd-dev/go-coap/v3/pkg/runner/periodic" + coapgwMessage "github.com/plgd-dev/hub/v2/coap-gateway/service/message" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service/http" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store/mongodb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/uri" + "github.com/plgd-dev/hub/v2/pkg/fn" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + coapService "github.com/plgd-dev/hub/v2/pkg/net/coap/service" + otelClient "github.com/plgd-dev/hub/v2/pkg/opentelemetry/collector/client" + "github.com/plgd-dev/hub/v2/pkg/opentelemetry/otelcoap" + "github.com/plgd-dev/hub/v2/pkg/service" + "github.com/plgd-dev/hub/v2/pkg/sync/task/queue" + otelCodes "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +type Service struct { + config Config + ctx context.Context + cancel context.CancelFunc + taskQueue *queue.Queue + messagePool *pool.Pool + linkedHubCache *LinkedHubCache + store *mongodb.Store + logger log.Logger + authHandler AuthHandler + requestHandler RequestHandler + tracerProvider trace.TracerProvider + enrollmentGroupsCache *EnrollmentGroupsCache +} + +const DPSTag = "dps" + +func (server *Service) onInactivityConnection(cc mux.Conn) { + session, ok := cc.Context().Value(clientKey).(*Session) + errorf := server.logger.With(remoterAddr, cc.RemoteAddr().String()).Errorf + warnf := server.logger.With(remoterAddr, cc.RemoteAddr().String()).Warnf + if ok { + errorf = session.Errorf + warnf("cn: %v: keep alive was reached fail limit:: closing connection", session.String()) + } else { + warnf("keep alive was reached fail limit:: closing connection") + } + if err := cc.Close(); err != nil && !errors.Is(err, context.Canceled) { + errorf("failed to close connection: %w", err) + } +} + +type Options struct { + authHandler AuthHandler + requestHandler RequestHandler +} + +type Option func(o Options) Options + +// Override default authorization handler +func WithAuthHandler(authHandler AuthHandler) Option { + return func(o Options) Options { + if authHandler != nil { + o.authHandler = authHandler + } + return o + } +} + +// Override default request handler +func WithRequestHandler(requestHandler RequestHandler) Option { + return func(o Options) Options { + if requestHandler != nil { + o.requestHandler = requestHandler + } + return o + } +} + +const serviceName = "device-provisioning-service" + +func storePreConfiguredEnrollmentGroups(ctx context.Context, config Config, store *mongodb.Store) { + create := func(g EnrollmentGroupConfig) { + createCtx, cancel := context.WithTimeout(ctx, config.APIs.COAP.InactivityMonitor.Timeout) + defer cancel() + eg, hubs, err := g.ToProto() + if err != nil { + log.Warnf("cannot create pre configured enrollment group/hub: %v", err) + return + } + err = store.UpsertEnrollmentGroup(createCtx, "", eg) + if err != nil { + log.Warnf("cannot store pre configured enrollment group: %v", err) + } + for _, hub := range hubs { + err = store.UpsertHub(createCtx, "", hub) + if err != nil { + log.Warnf("cannot store pre configured hub: %v", err) + } + } + } + for _, g := range config.EnrollmentGroups { + create(g) + } +} + +// New creates server. +func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logger log.Logger, opts ...Option) (*service.Service, error) { + ctx, cancel := context.WithCancel(ctx) + otelClient, err := otelClient.New(ctx, config.Clients.OpenTelemetryCollector.Config, serviceName, fileWatcher, logger) + if err != nil { + cancel() + return nil, fmt.Errorf("cannot create open telemetry collector client: %w", err) + } + otelClient.AddCloseFunc(cancel) + tracerProvider := otelClient.GetTracerProvider() + var closer fn.FuncList + closer.AddFunc(otelClient.Close) + queue, err := queue.New(config.TaskQueue) + if err != nil { + closer.Execute() + return nil, fmt.Errorf("cannot create job queue %w", err) + } + closer.AddFunc(queue.Release) + + store, closeStore, err := NewStore(ctx, config.Clients.Storage.MongoDB, fileWatcher, logger, tracerProvider) + if err != nil { + closer.Execute() + return nil, fmt.Errorf("cannot create store: %w", err) + } + closer.AddFunc(closeStore) + + storePreConfiguredEnrollmentGroups(ctx, config, store) + + enrollmentGroupsCache := NewEnrollmentGroupsCache(ctx, config.Clients.Storage.CacheExpiration, store, logger) + closer.AddFunc(enrollmentGroupsCache.Close) + enrollmentGroupsRunner := periodic.New(ctx.Done(), config.Clients.Storage.CacheExpiration/2) + enrollmentGroupsRunner(func(now time.Time) bool { + enrollmentGroupsCache.CheckExpirations(now) + return true + }) + + optCfg := Options{ + authHandler: MakeDefaultAuthHandler(config, enrollmentGroupsCache), + requestHandler: RequestHandle{}, + } + for _, o := range opts { + optCfg = o(optCfg) + } + runner := periodic.New(ctx.Done(), config.APIs.COAP.InactivityMonitor.Timeout/2) + runner(func(now time.Time) bool { + optCfg.authHandler.GetChainsCache().CheckExpirations(now) + return true + }) + + var httpService *http.Service + if config.APIs.HTTP.Enabled { + httpService, err = http.New(ctx, serviceName, config.APIs.HTTP.Config, fileWatcher, logger, tracerProvider, store) + if err != nil { + closer.Execute() + return nil, fmt.Errorf("cannot create http service: %w", err) + } + } + + linkedHubCache := NewLinkedHubCache(ctx, config.Clients.Storage.CacheExpiration, store, fileWatcher, logger, tracerProvider) + s := Service{ + config: config, + linkedHubCache: linkedHubCache, + taskQueue: queue, + + ctx: ctx, + cancel: cancel, + + messagePool: pool.New(uint32(config.APIs.COAP.MessagePoolSize), 1024), + store: store, + logger: logger, + authHandler: optCfg.authHandler, + requestHandler: optCfg.requestHandler, + tracerProvider: tracerProvider, + enrollmentGroupsCache: enrollmentGroupsCache, + } + + service, err := s.createServices(fileWatcher, logger) + if err != nil { + return nil, fmt.Errorf("cannot coap services: %w", err) + } + if httpService != nil { + service.Add(httpService) + } + service.AddCloseFunc(closer.Execute) + return service, nil +} + +func (RequestHandle) DefaultHandler(_ context.Context, req *mux.Message, _ *Session, _ []*LinkedHub, _ *EnrollmentGroup) (*pool.Message, error) { + path, _ := req.Options().Path() + + /* + switch { + case strings.HasPrefix("/"+path, uri.ResourceRoute): + resourceRouteHandler(req, session) + default: + return nil, statusErrorf(coapCodes.NotFound, "unknown path %v", path) + } + */ + return nil, statusErrorf(coapCodes.NotFound, "unknown path %v", path) +} + +const clientKey = "client" + +func (server *Service) getVerifiedChain(conn net.Conn) (verifiedChains [][]*x509.Certificate) { + var certRaw []byte + switch tlsCon := conn.(type) { + case *tls.Conn: + if len(tlsCon.ConnectionState().PeerCertificates) > 0 { + certRaw = tlsCon.ConnectionState().PeerCertificates[0].Raw + } + case *dtls.Conn: + if len(tlsCon.ConnectionState().PeerCertificates) > 0 { + certRaw = tlsCon.ConnectionState().PeerCertificates[0] + } + default: + server.logger.With(remoterAddr, conn.RemoteAddr().String()).Errorf("unknown connection type: %T", conn) + return nil + } + if len(certRaw) == 0 { + server.logger.With(remoterAddr, conn.RemoteAddr().String()).Debugf("cannot get verified chain: peer certificates are empty") + return nil + } + id := toCRC64(certRaw) + v := server.authHandler.GetChainsCache().Load(id) + if v != nil { + return v.Data() + } + server.logger.With(remoterAddr, conn.RemoteAddr().String()).Debugf("cannot get verified chain: it is not set") + return nil +} + +func (server *Service) coapConnOnNew(coapConn mux.Conn) { + verifiedChains := server.getVerifiedChain(coapConn.NetConn()) + session := newSession(server, coapConn, verifiedChains) + coapConn.SetContextValue(clientKey, session) + coapConn.AddOnClose(func() { + session.OnClose() + }) +} + +func (server *Service) toInternalHandler(w mux.ResponseWriter, r *mux.Message, h func(ctx context.Context, req *mux.Message, session *Session) (*pool.Message, error)) { + session, ok := w.Conn().Context().Value(clientKey).(*Session) + if !ok { + addr := w.Conn().RemoteAddr().String() + log.Errorf("unknown session %v", addr) + return + } + startTime := time.Now() + path, _ := r.Options().Path() + ctx, span := otelcoap.Start(r.Context(), path, r.Code().String(), otelcoap.WithTracerProvider(server.tracerProvider), otelcoap.WithSpanOptions(trace.WithSpanKind(trace.SpanKindServer))) + defer span.End() + r.SetContext(ctx) + otelcoap.MessageReceivedEvent(ctx, r.Message) + + ctx, cancel := context.WithTimeout(r.Context(), session.server.config.APIs.COAP.InactivityMonitor.Timeout) + defer cancel() + resp, errResp := h(ctx, r, session) + if errResp != nil { + s, ok := status.FromError(errResp) + if ok { + m, cleanUp := coapgwMessage.GetErrorResponse(ctx, server.messagePool, s.Code(), r.Token(), errResp) + defer cleanUp() + resp = m + } + defer func() { + span.RecordError(errResp) + span.SetStatus(otelCodes.Error, errResp.Error()) + _ = session.Close() + }() + } + if resp != nil { + if err := session.WriteMessage(resp); err != nil { + session.Errorf("cannot send error: %w", err) + } + otelcoap.MessageSentEvent(r.Context(), resp) + span.SetAttributes(otelcoap.StatusCodeAttr(resp.Code())) + } + session.logRequestResponse(ctx, startTime, r, resp, errResp) +} + +func statusErrorf(code coapCodes.Code, fmt string, args ...interface{}) error { + return status.Errorf(NewMessageWithCode(code), fmt, args...) +} + +func (server *Service) toHandler(h func(ctx context.Context, req *mux.Message, session *Session, linkedHubs []*LinkedHub, group *EnrollmentGroup) (*pool.Message, error)) mux.Handler { + return mux.HandlerFunc(func(w mux.ResponseWriter, r *mux.Message) { + server.toInternalHandler(w, r, func(ctx context.Context, _ *mux.Message, session *Session) (*pool.Message, error) { + session.resolveLocalEndpoints() + if err := session.checkForError(); err != nil { + p, _ := r.Options().Path() + return nil, statusErrorf(coapCodes.Forbidden, "cannot process %v %v: %w", r.Code(), p, err) + } + linkedHubs, group, err := session.getGroupAndLinkedHubs(ctx) + if err != nil { + p, _ := r.Options().Path() + return nil, statusErrorf(coapCodes.BadRequest, "cannot process %v %v: %w", r.Code(), p, err) + } + return h(ctx, r, session, linkedHubs, group) + }) + }) +} + +// createServices setups coap server +func (server *Service) createServices(fileWatcher *fsnotify.Watcher, logger log.Logger) (*service.Service, error) { + setHandlerError := func(uri string, err error) error { + return fmt.Errorf("failed to set %v handler: %w", uri, err) + } + + m := mux.NewRouter() + m.DefaultHandle(server.toHandler(server.requestHandler.DefaultHandler)) + + if err := m.Handle(plgdtime.ResourceURI, server.toHandler(server.requestHandler.ProcessPlgdTime)); err != nil { + return nil, setHandlerError(plgdtime.ResourceURI, err) + } + if err := m.Handle(uri.Ownership, server.toHandler(server.requestHandler.ProcessOwnership)); err != nil { + return nil, setHandlerError(uri.Ownership, err) + } + if err := m.Handle(uri.Credentials, server.toHandler(server.requestHandler.ProcessCredentials)); err != nil { + return nil, setHandlerError(uri.Credentials, err) + } + if err := m.Handle(uri.ACLs, server.toHandler(server.requestHandler.ProcessACLs)); err != nil { + return nil, setHandlerError(uri.ACLs, err) + } + if err := m.Handle(uri.CloudConfiguration, server.toHandler(server.requestHandler.ProcessCloudConfiguration)); err != nil { + return nil, setHandlerError(uri.CloudConfiguration, err) + } + + return coapService.New(server.ctx, server.config.APIs.COAP.Config, m, fileWatcher, logger, + coapService.WithOnNewConnection(server.coapConnOnNew), + coapService.WithOnInactivityConnection(server.onInactivityConnection), + coapService.WithMessagePool(server.messagePool), + coapService.WithOverrideTLS(func(cfg *tls.Config) *tls.Config { + cfg.InsecureSkipVerify = true + cfg.ClientAuth = tls.RequireAnyClientCert + cfg.VerifyPeerCertificate = server.authHandler.VerifyPeerCertificate + cfg.VerifyConnection = server.authHandler.VerifyConnection + return cfg + }), + ) +} diff --git a/device-provisioning-service/service/service_test.go b/device-provisioning-service/service/service_test.go new file mode 100644 index 000000000..75a219ad1 --- /dev/null +++ b/device-provisioning-service/service/service_test.go @@ -0,0 +1,64 @@ +package service_test + +import ( + "context" + "testing" + "time" + + "github.com/plgd-dev/device/v2/schema/plgdtime" + "github.com/plgd-dev/go-coap/v3/options" + "github.com/plgd-dev/go-coap/v3/tcp" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + "github.com/plgd-dev/hub/v2/pkg/config" + "github.com/plgd-dev/hub/v2/pkg/log" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" +) + +func TestServiceServe(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesCertificateAuthority|hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId) + defer hubShutdown() + + log.Infof("%v\n\n", test.MakeConfig(t)) + + cfg := test.MakeConfig(t).String() + var testCfg service.Config + err := config.Parse([]byte(cfg), &testCfg) + require.NoError(t, err) + + shutDown := test.SetUp(t) + defer shutDown() +} + +func TestClientInactivity(t *testing.T) { + defer test.ClearDB(t) + hubShutdown := hubTestService.SetUpServices(context.Background(), t, hubTestService.SetUpServicesCertificateAuthority|hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesId) + defer hubShutdown() + dpsCfg := test.MakeConfig(t) + dpsCfg.APIs.COAP.InactivityMonitor.Timeout = time.Second * 1 + shutDown := test.New(t, dpsCfg) + defer shutDown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + c, err := tcp.Dial(dpsCfg.APIs.COAP.Addr, options.WithTLS(setupTLSConfig(t)), options.WithContext(ctx)) + require.NoError(t, err) + defer func() { + errC := c.Close() + require.NoError(t, errC) + }() + + time.Sleep(time.Second * 2) + + _, err = c.Get(ctx, plgdtime.ResourceURI) + require.NoError(t, err) + + select { + case <-c.Done(): + case <-ctx.Done(): + require.NoError(t, ctx.Err()) + } +} diff --git a/device-provisioning-service/service/session.go b/device-provisioning-service/service/session.go new file mode 100644 index 000000000..d2026b69a --- /dev/null +++ b/device-provisioning-service/service/session.go @@ -0,0 +1,213 @@ +package service + +import ( + "bytes" + "context" + "crypto/x509" + "errors" + "fmt" + "net" + "time" + + "github.com/google/uuid" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/plgd-dev/go-coap/v3/mux" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/pkg/log" + "github.com/plgd-dev/hub/v2/pkg/net/coap" + "go.uber.org/atomic" +) + +// Session represents a setup of connection +type Session struct { + server *Service + coapConn mux.Conn + chains [][]*x509.Certificate + deviceID atomic.String + err error + enrollmentGroup *EnrollmentGroup + manufacturerCertificateID string + localEndpoints atomic.Pointer[[]string] +} + +func toManufacturerCertificateID(publicKey any) (string, error) { + publicKeyRaw, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return "", err + } + return uuid.NewSHA1(uuid.NameSpaceX500, publicKeyRaw).String(), nil +} + +// newSession creates and initializes a session +func newSession(server *Service, coapConn mux.Conn, chains [][]*x509.Certificate) *Session { + // enrollmentGroup can be nil - any request to from this session ends with error and enrollmentGroup is nil + var enrollmentGroup *EnrollmentGroup + var manufacturerCertificateID string + var err error + var deviceID string + if len(chains) == 0 || len(chains[0]) == 0 { + err = errors.New("unable to find enrollment group for empty certificate chain") + } else { + ctx, cancel := context.WithTimeout(coapConn.Context(), server.config.APIs.COAP.InactivityMonitor.Timeout) + defer cancel() + group, ok, err1 := server.enrollmentGroupsCache.GetEnrollmentGroup(ctx, chains) + switch { + case err1 != nil: + err = fmt.Errorf("unable to find enrollment group for the manufacturer certificate %v: %w", chains[0][0].Subject.CommonName, err1) + case ok: + manufacturerCertificateID, err1 = toManufacturerCertificateID(chains[0][0].PublicKey) + if err1 != nil { + err = fmt.Errorf("unable to marshal public key of manufacturer certificate %v: %w", chains[0][0].Subject.CommonName, err1) + } else { + enrollmentGroup = group + } + default: + err = fmt.Errorf("unable to find enrollment group for the manufacturer certificate %v", chains[0][0].Subject.CommonName) + } + } + s := Session{ + server: server, + coapConn: coapConn, + chains: chains, + err: err, + enrollmentGroup: enrollmentGroup, + manufacturerCertificateID: manufacturerCertificateID, + } + s.deviceID.Store(deviceID) + if len(chains) > 0 && len(chains[0]) > 0 { + s.updateProvisioningRecord(&store.ProvisioningRecord{ + Attestation: &pb.Attestation{ + Date: time.Now().UnixNano(), + X509: &pb.X509Attestation{ + CertificatePem: certToPem(chains[0][0]), + CommonName: chains[0][0].Subject.CommonName, + }, + }, + }) + } + return &s +} + +func (s *Session) RemoteAddr() net.Addr { + return s.coapConn.RemoteAddr() +} + +func (s *Session) DeviceID() string { + return s.deviceID.Load() +} + +func (s *Session) SetDeviceID(deviceID string) { + s.deviceID.Store(deviceID) +} + +func (s *Session) String() string { + if len(s.chains) == 0 { + return "" + } + if len(s.chains[0]) == 0 { + return "" + } + return s.chains[0][0].Subject.CommonName +} + +func (s *Session) Context() context.Context { + return s.coapConn.Context() +} + +// Close closes coap connection +func (s *Session) Close() error { + // wait one second for send response + time.Sleep(time.Second) + if err := s.coapConn.Close(); err != nil { + return fmt.Errorf("cannot close session: %w", err) + } + return nil +} + +// OnClose action when coap connection was closed. +func (s *Session) OnClose() { + s.Debugf("session was closed") +} + +func (s *Session) WriteMessage(m *pool.Message) error { + return s.coapConn.WriteMessage(m) +} + +func (s *Session) createResponse(code codes.Code, token message.Token, contentFormat message.MediaType, payload []byte) *pool.Message { + msg := s.server.messagePool.AcquireMessage(s.coapConn.Context()) + msg.SetCode(code) + msg.SetToken(token) + if len(payload) > 0 { + msg.SetContentFormat(contentFormat) + msg.SetBody(bytes.NewReader(payload)) + } + return msg +} + +func (s *Session) Errorf(fmt string, args ...interface{}) { + logger := s.getLogger() + logger.Errorf(fmt, args...) +} + +func (s *Session) Debugf(fmt string, args ...interface{}) { + logger := s.getLogger() + logger.Debugf(fmt, args...) +} + +func (s *Session) getGroupAndLinkedHubs(ctx context.Context) ([]*LinkedHub, *EnrollmentGroup, error) { + if s.enrollmentGroup == nil { + return nil, nil, errors.New("cannot get enrollment group: not found") + } + linkedHub, err := s.server.linkedHubCache.GetHubs(ctx, s.enrollmentGroup) + if err != nil { + return nil, s.enrollmentGroup, fmt.Errorf("cannot get linked hub for enrollment group for %v: %w", s.enrollmentGroup, err) + } + return linkedHub, s.enrollmentGroup, err +} + +func (s *Session) checkForError() error { + return s.err +} + +func (s *Session) updateProvisioningRecord(provisionedDevice *store.ProvisioningRecord) { + if s.err != nil { + return + } + provisionedDevice.EnrollmentGroupId = s.enrollmentGroup.GetId() + provisionedDevice.Id = s.manufacturerCertificateID + provisionedDevice.LocalEndpoints = s.getLocalEndpoints() + provisionedDevice.Owner = s.enrollmentGroup.GetOwner() + + err := s.server.store.UpdateProvisioningRecord(s.Context(), provisionedDevice.GetOwner(), provisionedDevice) + if err != nil { + s.Errorf("cannot update record about provisioned device at DB: %v", err) + } +} + +func (s *Session) resolveLocalEndpoints() { + v := []string{} + if !s.localEndpoints.CompareAndSwap(nil, &v) { + return + } + endpoints, err := coap.GetEndpointsFromDeviceResource(s.coapConn.Context(), s.coapConn) + if err != nil { + s.Errorf("cannot get local endpoints: %w", err) + return + } + s.localEndpoints.Store(&endpoints) + s.getLogger().With(log.LocalEndpointsKey, endpoints).Debug("local endpoints retrieval successful.") +} + +func (s *Session) getLocalEndpoints() []string { + localEndpoints := s.localEndpoints.Load() + if localEndpoints == nil { + return nil + } + if len(*localEndpoints) == 0 { + return nil + } + return *localEndpoints +} diff --git a/device-provisioning-service/store/enrollmentGroup.go b/device-provisioning-service/store/enrollmentGroup.go new file mode 100644 index 000000000..ca497856d --- /dev/null +++ b/device-provisioning-service/store/enrollmentGroup.go @@ -0,0 +1,14 @@ +package store + +import ( + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" +) + +const ( + IDKey = "_id" // must match with pb.ProvisioningRecord.Id, pb.EnrollmentGroup.Id tag + HubIDKey = "hubId" // must match with pb.ProvisioningRecord.HubId + HubIDsKey = "hubIds" // must match with pb.EnrollmentGroup.HubId + AttestationMechanismX509LeadCertificateNameKey = "attestationMechanism.x509.leadCertificateName" // must match with all tags in path pb.EnrollmentGroup.AttestationMechanism.X509.LeadCertificateName +) + +type EnrollmentGroup = pb.EnrollmentGroup diff --git a/device-provisioning-service/store/hub.go b/device-provisioning-service/store/hub.go new file mode 100644 index 000000000..215af261e --- /dev/null +++ b/device-provisioning-service/store/hub.go @@ -0,0 +1,7 @@ +package store + +import ( + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" +) + +type Hub = pb.Hub diff --git a/device-provisioning-service/store/mongodb/bulkWriter.go b/device-provisioning-service/store/mongodb/bulkWriter.go new file mode 100644 index 000000000..c9031988b --- /dev/null +++ b/device-provisioning-service/store/mongodb/bulkWriter.go @@ -0,0 +1,303 @@ +package mongodb + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/pkg/log" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type bulkWriter struct { + col *mongo.Collection + documentLimit uint16 // https://www.mongodb.com/docs/manual/reference/limits/#mongodb-limit-Write-Command-Batch-Limit-Size - must be <= 100000 + throttleTime time.Duration + flushTimeout time.Duration + logger log.Logger + + closed atomic.Bool + done chan struct{} + trigger chan bool + + mutex sync.Mutex + models map[string]*store.ProvisioningRecord + wg sync.WaitGroup +} + +func newBulkWriter(col *mongo.Collection, documentLimit uint16, throttleTime time.Duration, flushTimeout time.Duration, logger log.Logger) *bulkWriter { + r := &bulkWriter{ + col: col, + documentLimit: documentLimit, + throttleTime: throttleTime, + flushTimeout: flushTimeout, + done: make(chan struct{}), + trigger: make(chan bool, 1), + logger: logger, + } + + r.wg.Add(1) + go func() { + defer r.wg.Done() + r.run() + }() + return r +} + +func toProvisioningRecordFilter(provisionedDevice *store.ProvisioningRecord) bson.M { + res := bson.M{"_id": provisionedDevice.GetId()} + return res +} + +func getProvisioningRecordCreationDate(defaultTime time.Time, provisionedDevice *store.ProvisioningRecord) int64 { + ret := defaultTime.UTC().UnixNano() + if provisionedDevice.GetAttestation().GetDate() > 0 && provisionedDevice.GetAttestation().GetDate() < ret { + ret = provisionedDevice.GetAttestation().GetDate() + } + if provisionedDevice.GetCloud().GetStatus().GetDate() > 0 && provisionedDevice.GetCloud().GetStatus().GetDate() < ret { + ret = provisionedDevice.GetCloud().GetStatus().GetDate() + } + if provisionedDevice.GetAcl().GetStatus().GetDate() > 0 && provisionedDevice.GetAcl().GetStatus().GetDate() < ret { + ret = provisionedDevice.GetAcl().GetStatus().GetDate() + } + if provisionedDevice.GetCredential().GetStatus().GetDate() > 0 && provisionedDevice.GetCredential().GetStatus().GetDate() < ret { + ret = provisionedDevice.GetCredential().GetStatus().GetDate() + } + if provisionedDevice.GetOwnership().GetStatus().GetDate() > 0 && provisionedDevice.GetOwnership().GetStatus().GetDate() < ret { + ret = provisionedDevice.GetOwnership().GetStatus().GetDate() + } + if provisionedDevice.GetPlgdTime().GetDate() > 0 && provisionedDevice.GetPlgdTime().GetDate() < ret { + ret = provisionedDevice.GetPlgdTime().GetDate() + } + return ret +} + +func setValueByDate(key, datePath string, dateOperator string, date int64, value interface{}) bson.M { + return bson.M{ + "$set": bson.M{ + key: bson.M{ + "$ifNull": bson.A{ + bson.M{ + "$cond": bson.M{ + "if": bson.M{ + dateOperator: bson.A{"$" + datePath, date}, + }, + "then": value, + "else": "$" + key, + }, + }, value, + }, + }, + }, + } +} + +func updateProvisioningRecord(provisionedDevice *store.ProvisioningRecord) []bson.M { + creationDate := provisionedDevice.GetCreationDate() + if creationDate == 0 { + creationDate = getProvisioningRecordCreationDate(time.Now(), provisionedDevice) + } + ret := []bson.M{ + {"$set": bson.M{ + "_id": provisionedDevice.GetId(), + store.EnrollmentGroupIDKey: provisionedDevice.GetEnrollmentGroupId(), + store.DeviceIDKey: provisionedDevice.GetDeviceId(), + store.LocalEndpointsKey: provisionedDevice.GetLocalEndpoints(), + store.OwnerKey: provisionedDevice.GetOwner(), + }}, + } + ret = append(ret, setValueByDate(store.CreationDateKey, store.CreationDateKey, "$gt", creationDate, creationDate)) + if provisionedDevice.GetAttestation() != nil { + ret = append(ret, setValueByDate(store.AttestationKey, store.AttestationKey+"."+store.DateKey, "$lt", provisionedDevice.GetAttestation().GetDate(), provisionedDevice.GetAttestation())) + } + if provisionedDevice.GetCloud() != nil { + ret = append(ret, setValueByDate(store.CloudKey, store.CloudKey+"."+store.StatusKey+"."+store.DateKey, "$lt", provisionedDevice.GetCloud().GetStatus().GetDate(), provisionedDevice.GetCloud())) + } + if provisionedDevice.GetAcl() != nil { + ret = append(ret, setValueByDate(store.ACLKey, store.ACLKey+"."+store.StatusKey+"."+store.DateKey, "$lt", provisionedDevice.GetAcl().GetStatus().GetDate(), provisionedDevice.GetAcl())) + } + if provisionedDevice.GetCredential() != nil { + ret = append(ret, setValueByDate(store.CredentialKey, store.CredentialKey+"."+store.StatusKey+"."+store.DateKey, "$lt", provisionedDevice.GetCredential().GetStatus().GetDate(), provisionedDevice.GetCredential())) + } + if provisionedDevice.GetOwnership() != nil { + ret = append(ret, setValueByDate(store.OwnershipKey, store.OwnershipKey+"."+store.StatusKey+"."+store.DateKey, "$lt", provisionedDevice.GetOwnership().GetStatus().GetDate(), provisionedDevice.GetOwnership())) + } + if provisionedDevice.GetPlgdTime() != nil { + ret = append(ret, setValueByDate(store.PlgdTimeKey, store.PlgdTimeKey+"."+store.DateKey, "$lt", provisionedDevice.GetPlgdTime().GetDate(), provisionedDevice.GetPlgdTime())) + } + return ret +} + +func convertProvisioningRecordToWriteModel(provisionedDevice *store.ProvisioningRecord) mongo.WriteModel { + return mongo.NewUpdateOneModel().SetFilter(toProvisioningRecordFilter(provisionedDevice)).SetUpdate(updateProvisioningRecord(provisionedDevice)).SetUpsert(true) +} + +func setEmptyField(field *string, value string) { + if *field == "" { + *field = value + } +} + +func setNonEmptyValue(field *string, value string) { + if value != "" { + *field = value + } +} + +func mergeLatestUpdateProvisioningRecord(toUpdate *store.ProvisioningRecord, latest *store.ProvisioningRecord) *store.ProvisioningRecord { + if toUpdate == nil { + return latest + } + + setEmptyField(&toUpdate.Owner, latest.GetOwner()) + setEmptyField(&toUpdate.EnrollmentGroupId, latest.GetEnrollmentGroupId()) + setEmptyField(&toUpdate.DeviceId, latest.GetDeviceId()) + + if len(toUpdate.GetLocalEndpoints()) == 0 { + toUpdate.LocalEndpoints = latest.GetLocalEndpoints() + } + if latest.GetAttestation().GetDate() > toUpdate.GetAttestation().GetDate() { + toUpdate.Attestation = latest.GetAttestation() + } + if latest.GetCloud().GetStatus().GetDate() > toUpdate.GetCloud().GetStatus().GetDate() { + toUpdate.Cloud = latest.GetCloud() + } + if latest.GetAcl().GetStatus().GetDate() > toUpdate.GetAcl().GetStatus().GetDate() { + toUpdate.Acl = latest.GetAcl() + } + if latest.GetCredential().GetStatus().GetDate() > toUpdate.GetCredential().GetStatus().GetDate() { + toUpdate.Credential = latest.GetCredential() + } + if latest.GetOwnership().GetStatus().GetDate() > toUpdate.GetOwnership().GetStatus().GetDate() { + toUpdate.Ownership = latest.GetOwnership() + } + if latest.GetPlgdTime().GetDate() > toUpdate.GetPlgdTime().GetDate() { + toUpdate.PlgdTime = latest.GetPlgdTime() + } + if latest.GetCreationDate() < toUpdate.GetCreationDate() { + toUpdate.CreationDate = latest.GetCreationDate() + setNonEmptyValue(&toUpdate.EnrollmentGroupId, latest.GetEnrollmentGroupId()) + setNonEmptyValue(&toUpdate.Owner, latest.GetOwner()) + setNonEmptyValue(&toUpdate.DeviceId, latest.GetDeviceId()) + if len(latest.GetLocalEndpoints()) > 0 { + toUpdate.LocalEndpoints = latest.GetLocalEndpoints() + } + } + return toUpdate +} + +func (b *bulkWriter) popProvisioningRecords() map[string]*store.ProvisioningRecord { + b.mutex.Lock() + defer b.mutex.Unlock() + models := b.models + b.models = nil + return models +} + +func (b *bulkWriter) push(provisioningRecords ...*store.ProvisioningRecord) { + b.mutex.Lock() + defer b.mutex.Unlock() + if b.models == nil { + b.models = make(map[string]*store.ProvisioningRecord) + } + for _, provisioningRecord := range provisioningRecords { + b.models[provisioningRecord.GetId()] = mergeLatestUpdateProvisioningRecord(b.models[provisioningRecord.GetId()], provisioningRecord) + } + select { + case b.trigger <- true: + default: + } +} + +func (b *bulkWriter) numProvisioningRecords() int { + b.mutex.Lock() + defer b.mutex.Unlock() + return len(b.models) +} + +func (b *bulkWriter) run() { + ticker := time.NewTicker(b.throttleTime) + tickerRunning := true + defer ticker.Stop() + for { + select { + case <-ticker.C: + if b.tryBulkWrite() == 0 && tickerRunning { + ticker.Stop() + tickerRunning = false + } + case <-b.trigger: + if !tickerRunning { + ticker.Reset(b.throttleTime) + tickerRunning = true + } + if b.numProvisioningRecords() > int(b.documentLimit) { + b.tryBulkWrite() + } + case <-b.done: + return + } + } +} + +func (b *bulkWriter) bulkWrite() (int, error) { + provisioningRecords := b.popProvisioningRecords() + if len(provisioningRecords) == 0 { + return 0, nil + } + ctx := context.Background() + if b.flushTimeout != 0 { + ctx1, cancel := context.WithTimeout(context.Background(), b.flushTimeout) + defer cancel() + ctx = ctx1 + } + m := make([]mongo.WriteModel, 0, int(b.documentLimit)+1) + + var errors []error + for _, provisioningRecord := range provisioningRecords { + m = append(m, convertProvisioningRecordToWriteModel(provisioningRecord)) + if b.documentLimit == 0 || len(m)%int(b.documentLimit) == 0 { + _, err := b.col.BulkWrite(ctx, m, options.BulkWrite().SetOrdered(false)) + if err != nil { + errors = append(errors, err) + } + m = m[:0] + } + } + + if len(m) > 0 { + _, err := b.col.BulkWrite(ctx, m, options.BulkWrite().SetOrdered(false)) + if err != nil { + errors = append(errors, err) + } + } + var err error + if len(errors) == 1 { + err = errors[0] + } else if len(errors) > 0 { + err = fmt.Errorf("%v", errors) + } + return len(provisioningRecords), err +} + +func (b *bulkWriter) tryBulkWrite() int { + n, err := b.bulkWrite() + if err != nil { + b.logger.Errorf("failed to bulk update provisioning records: %w", err) + } + return n +} + +func (b *bulkWriter) Close() { + if !b.closed.CompareAndSwap(false, true) { + return + } + close(b.done) + b.wg.Wait() + b.tryBulkWrite() +} diff --git a/device-provisioning-service/store/mongodb/config.go b/device-provisioning-service/store/mongodb/config.go new file mode 100644 index 000000000..b2d839864 --- /dev/null +++ b/device-provisioning-service/store/mongodb/config.go @@ -0,0 +1,38 @@ +package mongodb + +import ( + "fmt" + "time" + + pkgMongo "github.com/plgd-dev/hub/v2/pkg/mongodb" +) + +const minDuration = time.Millisecond * 100 + +type BulkWriteConfig struct { + Timeout time.Duration `yaml:"timeout"` + ThrottleTime time.Duration `yaml:"throttleTime"` + DocumentLimit uint16 `yaml:"documentLimit"` +} + +func (c *BulkWriteConfig) Validate() error { + if c.Timeout <= minDuration { + return fmt.Errorf("timeout('%v')", c.Timeout) + } + if c.ThrottleTime <= minDuration { + return fmt.Errorf("throttleTime('%v')", c.ThrottleTime) + } + return nil +} + +type Config struct { + Mongo pkgMongo.Config `yaml:",inline"` + BulkWrite BulkWriteConfig `yaml:"bulkWrite"` +} + +func (c *Config) Validate() error { + if err := c.BulkWrite.Validate(); err != nil { + return fmt.Errorf("bulkWrite.%w", err) + } + return c.Mongo.Validate() +} diff --git a/device-provisioning-service/store/mongodb/enrollmentGroups.go b/device-provisioning-service/store/mongodb/enrollmentGroups.go new file mode 100644 index 000000000..21666ac19 --- /dev/null +++ b/device-provisioning-service/store/mongodb/enrollmentGroups.go @@ -0,0 +1,259 @@ +package mongodb + +import ( + "context" + "errors" + "fmt" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const enrollmentGroupsCol = "enrollmentGroups" + +func (s *Store) CreateEnrollmentGroup(ctx context.Context, owner string, enrollmentGroup *store.EnrollmentGroup) error { + if err := enrollmentGroup.Validate(owner); err != nil { + return fmt.Errorf("invalid value: %w", err) + } + _, err := s.Collection(enrollmentGroupsCol).InsertOne(ctx, enrollmentGroup) + if err != nil { + return err + } + return nil +} + +func (s *Store) UpsertEnrollmentGroup(ctx context.Context, owner string, enrollmentGroup *store.EnrollmentGroup) error { + return s.updateEnrollmentGroup(ctx, owner, enrollmentGroup, true) +} + +func (s *Store) UpdateEnrollmentGroup(ctx context.Context, owner string, enrollmentGroup *store.EnrollmentGroup) error { + return s.updateEnrollmentGroup(ctx, owner, enrollmentGroup, false) +} + +func (s *Store) updateEnrollmentGroup(ctx context.Context, owner string, enrollmentGroup *store.EnrollmentGroup, upsert bool) error { + if err := enrollmentGroup.Validate(owner); err != nil { + return fmt.Errorf("invalid value: %w", err) + } + filter, _ := toEnrollmentGroupFilter(owner, &store.EnrollmentGroupsQuery{ + IdFilter: []string{enrollmentGroup.GetId()}, + }) + res, err := s.Collection(enrollmentGroupsCol).UpdateOne(ctx, filter, bson.M{"$set": enrollmentGroup}, options.Update().SetUpsert(upsert)) + if err != nil { + return err + } + if res.UpsertedCount > 0 && upsert { + return nil + } + if res.ModifiedCount == 0 && res.MatchedCount == 0 { + return mongo.ErrNilDocument + } + return nil +} + +func addOwnerToFilter(owner string, q bson.D) bson.D { + if owner != "" { + return append(q, bson.E{Key: store.OwnerKey, Value: owner}) + } + return q +} + +func toEnrollmentGroupIDFilter(owner string, queries *store.EnrollmentGroupsQuery) ([]bson.D, *mongo.IndexModel) { + or := []bson.D{} + for _, q := range queries.GetIdFilter() { + or = append(or, addOwnerToFilter(owner, bson.D{ + {Key: store.IDKey, Value: q}, + })) + } + var hint *mongo.IndexModel + if len(or) > 0 { + if owner != "" { + hint = &IDOwnerKeyQueryIndex + } else { + hint = &IDKeyQueryIndex + } + } + return or, hint +} + +func toEnrollmentGroupHubIDFilter(owner string, queries *store.EnrollmentGroupsQuery, disableHintIn bool, or []bson.D, hintIn *mongo.IndexModel) (filter []bson.D, hint *mongo.IndexModel, disableHint bool) { + disableHint = disableHintIn + hint = hintIn + if len(queries.GetHubIdFilter()) == 0 { + return or, hint, disableHint + } + or = append(or, addOwnerToFilter(owner, bson.D{ + {Key: store.HubIDsKey, Value: bson.M{ + "$in": queries.GetHubIdFilter(), + }}, + })) + if hint == nil { + if owner != "" { + hint = &HubIDOwnerKeyQueryIndex + } else { + hint = &HubIDKeyQueryIndex + } + } else { + hint = nil + disableHint = true + } + return or, hint, disableHint +} + +func toEnrollmentGroupAttestationMechanismX509CertificateNamesFilter(owner string, queries *store.EnrollmentGroupsQuery, disableHintIn bool, or []bson.D, hintIn *mongo.IndexModel) (filter []bson.D, hint *mongo.IndexModel, disableHint bool) { + disableHint = disableHintIn + hint = hintIn + if len(queries.GetAttestationMechanismX509CertificateNames()) == 0 { + return or, hint, disableHint + } + or = append(or, addOwnerToFilter(owner, bson.D{ + {Key: store.AttestationMechanismX509LeadCertificateNameKey, Value: bson.M{ + "$in": queries.GetAttestationMechanismX509CertificateNames(), + }}, + })) + if hint == nil && !disableHint { + if owner != "" { + hint = &AttestationMechanismX509CertificateNamesOwnerKeyQueryIndex + } else { + hint = &AttestationMechanismX509CertificateNamesKeyQueryIndex + } + } else { + hint = nil + disableHint = true + } + return or, hint, disableHint +} + +func toEnrollmentGroupFilter(owner string, queries *store.EnrollmentGroupsQuery) (bson.D, *mongo.IndexModel) { + or, hint := toEnrollmentGroupIDFilter(owner, queries) + or, hint, disableHint := toEnrollmentGroupHubIDFilter(owner, queries, false, or, hint) + or, hint, _ = toEnrollmentGroupAttestationMechanismX509CertificateNamesFilter(owner, queries, disableHint, or, hint) + + switch len(or) { + case 0: + return addOwnerToFilter(owner, bson.D{}), nil + case 1: + return or[0], hint + } + return bson.D{{Key: "$or", Value: or}}, hint +} + +type DocumentKey struct { + ID string `bson:"_id"` +} + +type StreamEnrollmentGroupEvent struct { + OperationType string `bson:"operationType"` + DocumentKey *DocumentKey `bson:"documentKey"` +} + +type watchIterator struct { + ctx context.Context + iter *mongo.ChangeStream +} + +func (i *watchIterator) Next(ctx context.Context) (event store.Event, id string, ok bool) { + if !i.iter.Next(ctx) { + return "", "", false + } + var v StreamEnrollmentGroupEvent + if err := i.iter.Decode(&v); err != nil { + return "", "", false + } + if v.DocumentKey != nil { + id = v.DocumentKey.ID + } + switch v.OperationType { + case "delete": + event = store.EventDelete + case "update": + event = store.EventUpdate + } + return event, id, true +} + +func (i *watchIterator) Err() error { + return i.iter.Err() +} + +func (i *watchIterator) Close() error { + return i.iter.Close(i.ctx) +} + +func (s *Store) watch(ctx context.Context, col *mongo.Collection) (*watchIterator, error) { + stream, err := col.Watch(ctx, mongo.Pipeline{ + {{Key: "$match", Value: bson.D{{Key: "operationType", Value: bson.M{"$in": []string{"delete", "update"}}}}}}, + }) + if err != nil { + return nil, err + } + i := watchIterator{ + iter: stream, + ctx: ctx, + } + return &i, err +} + +func (s *Store) WatchEnrollmentGroups(ctx context.Context) (store.WatchEnrollmentGroupIter, error) { + return s.watch(ctx, s.Collection(enrollmentGroupsCol)) +} + +func (s *Store) DeleteEnrollmentGroups(ctx context.Context, owner string, query *store.EnrollmentGroupsQuery) (int64, error) { + opts := options.Delete() + filter, hint := toEnrollmentGroupFilter(owner, query) + if hint != nil { + opts.SetHint(hint.Keys) + } + res, err := s.Collection(enrollmentGroupsCol).DeleteMany(ctx, filter, opts) + if err != nil { + return -1, fmt.Errorf("cannot remove enrollment groups for owner %v with filter %v: %w", owner, query.GetIdFilter(), err) + } + if res.DeletedCount == 0 { + return -1, fmt.Errorf("cannot remove enrollment groups for owner %v with filter %v: not found", owner, query.GetIdFilter()) + } + return res.DeletedCount, nil +} + +func (s *Store) LoadEnrollmentGroups(ctx context.Context, owner string, query *store.EnrollmentGroupsQuery, h store.LoadEnrollmentGroupsFunc) error { + col := s.Collection(enrollmentGroupsCol) + opts := options.Find() + filter, hint := toEnrollmentGroupFilter(owner, query) + if hint != nil { + opts.SetHint(hint.Keys) + } + iter, err := col.Find(ctx, filter, opts) + if errors.Is(err, mongo.ErrNilDocument) { + return nil + } + if err != nil { + return err + } + + i := enrollmentGroupsIterator{ + iter: iter, + } + err = h(ctx, &i) + + errClose := iter.Close(ctx) + if err == nil { + return errClose + } + return err +} + +type enrollmentGroupsIterator struct { + iter *mongo.Cursor +} + +func (i *enrollmentGroupsIterator) Next(ctx context.Context, s *store.EnrollmentGroup) bool { + if !i.iter.Next(ctx) { + return false + } + err := i.iter.Decode(s) + return err == nil +} + +func (i *enrollmentGroupsIterator) Err() error { + return i.iter.Err() +} diff --git a/device-provisioning-service/store/mongodb/enrollmentGroups_test.go b/device-provisioning-service/store/mongodb/enrollmentGroups_test.go new file mode 100644 index 000000000..2a79984bd --- /dev/null +++ b/device-provisioning-service/store/mongodb/enrollmentGroups_test.go @@ -0,0 +1,481 @@ +package mongodb_test + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/base64" + "strconv" + "testing" + "time" + + "github.com/google/uuid" + "github.com/plgd-dev/device/v2/pkg/security/generateCertificate" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/stretchr/testify/require" +) + +const anotherOwner = "anotherOwner" + +func createCACertificate(t require.TestingT, commonName string) string { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + var cfg generateCertificate.Configuration + cfg.Subject.CommonName = commonName + cfg.ValidFor = time.Hour * 24 + cfg.BasicConstraints.MaxPathLen = 1000 + rootCA, err := generateCertificate.GenerateRootCA(cfg, priv) + require.NoError(t, err) + + return "data:;base64," + base64.StdEncoding.EncodeToString(rootCA) +} + +func TestStoreCreateEnrollmentGroup(t *testing.T) { + id := uuid.NewString() + eg := test.NewEnrollmentGroup(t, id, test.DPSOwner) + eg1 := test.NewEnrollmentGroup(t, id, anotherOwner) + type args struct { + owner string + eg *store.EnrollmentGroup + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "invalid ID", + args: args{ + eg: &store.EnrollmentGroup{}, + }, + wantErr: true, + }, + { + name: "valid", + args: args{ + owner: eg.GetOwner(), + eg: eg, + }, + }, + { + name: "duplicity", + args: args{ + owner: eg.GetOwner(), + eg: eg, + }, + wantErr: true, + }, + { + name: anotherOwner, + args: args{ + owner: eg1.GetOwner(), + eg: eg1, + }, + wantErr: true, + }, + } + + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx := context.Background() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := s.CreateEnrollmentGroup(ctx, tt.args.owner, tt.args.eg) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestStoreUpdateEnrollmentGroup(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + ctx := context.Background() + id := uuid.NewString() + eg := test.NewEnrollmentGroup(t, id, test.DPSOwner) + eg1 := test.NewEnrollmentGroup(t, id, anotherOwner) + err := s.CreateEnrollmentGroup(ctx, eg.GetOwner(), eg) + require.NoError(t, err) + eg.Name = "abcd" + type args struct { + owner string + eg *store.EnrollmentGroup + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "invalid ID", + args: args{ + owner: eg.GetOwner(), + eg: &store.EnrollmentGroup{}, + }, + wantErr: true, + }, + { + name: "valid", + args: args{ + owner: eg.GetOwner(), + eg: eg, + }, + }, + { + name: "duplicity", + args: args{ + owner: eg.GetOwner(), + eg: eg, + }, + }, + { + name: "not exist", + args: args{ + owner: eg.GetOwner(), + eg: &store.EnrollmentGroup{Id: "notExist"}, + }, + wantErr: true, + }, + { + name: "another user tries to replace original owner", + args: args{ + owner: eg1.GetOwner(), + eg: eg1, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := s.UpdateEnrollmentGroup(ctx, tt.args.owner, tt.args.eg) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestStoreWatchEnrollmentGroup(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + ctx := context.Background() + watcher, err := s.WatchEnrollmentGroups(ctx) + require.NoError(t, err) + eg := test.NewEnrollmentGroup(t, uuid.NewString(), test.DPSOwner) + err = s.CreateEnrollmentGroup(ctx, eg.GetOwner(), eg) + require.NoError(t, err) + egUpd := pb.EnrollmentGroup{ + Id: eg.GetId(), + AttestationMechanism: eg.GetAttestationMechanism(), + HubIds: eg.GetHubIds(), + Name: "myName", + Owner: eg.GetOwner(), + } + err = s.UpdateEnrollmentGroup(ctx, egUpd.GetOwner(), &egUpd) + require.NoError(t, err) + _, err = s.DeleteEnrollmentGroups(ctx, eg.GetOwner(), &pb.GetEnrollmentGroupsRequest{ + IdFilter: []string{eg.GetId()}, + }) + require.NoError(t, err) + expectedEvents := []struct { + event store.Event + id string + }{ + { + event: store.EventUpdate, + id: eg.GetId(), + }, + { + event: store.EventDelete, + id: eg.GetId(), + }, + } + for _, expectedEvent := range expectedEvents { + event, id, ok := watcher.Next(ctx) + require.True(t, ok) + require.Equal(t, expectedEvent.id, id) + require.Equal(t, expectedEvent.event, event) + } + require.NoError(t, watcher.Close()) +} + +func TestStoreDeleteEnrollmentGroup(t *testing.T) { + egs := []*store.EnrollmentGroup{ + test.NewEnrollmentGroup(t, uuid.NewString(), test.DPSOwner), + test.NewEnrollmentGroup(t, uuid.NewString(), test.DPSOwner), + test.NewEnrollmentGroup(t, uuid.NewString(), anotherOwner), + } + type args struct { + owner string + query *store.EnrollmentGroupsQuery + } + tests := []struct { + name string + args args + wantErr bool + count int64 + }{ + { + name: "invalid cloudId", + args: args{ + owner: egs[0].GetOwner(), + query: &store.EnrollmentGroupsQuery{ + IdFilter: []string{"notFound"}, + }, + }, + wantErr: true, + }, + + { + name: "valid", + args: args{ + owner: egs[0].GetOwner(), + query: &store.EnrollmentGroupsQuery{ + IdFilter: []string{egs[0].GetId()}, + }, + }, + count: 1, + }, + { + name: "valid multiple", + args: args{ + owner: egs[0].GetOwner(), + query: &store.EnrollmentGroupsQuery{ + IdFilter: []string{egs[0].GetId(), egs[1].GetId()}, + }, + }, + count: 1, + }, + { + name: "delete all owner", + args: args{ + owner: egs[0].GetOwner(), + }, + wantErr: true, + }, + { + name: "delete all another owner", + args: args{ + owner: egs[2].GetOwner(), + }, + count: 1, + }, + } + + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx := context.Background() + for _, eg := range egs { + err := s.UpsertEnrollmentGroup(ctx, eg.GetOwner(), eg) + require.NoError(t, err) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + count, err := s.DeleteEnrollmentGroups(ctx, tt.args.owner, tt.args.query) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.count, count) + }) + } +} + +type testEnrollmentGroupHandler struct { + lcs pb.EnrollmentGroups +} + +func (h *testEnrollmentGroupHandler) Handle(ctx context.Context, iter store.EnrollmentGroupIter) (err error) { + for { + var eg store.EnrollmentGroup + if !iter.Next(ctx, &eg) { + break + } + h.lcs = append(h.lcs, &eg) + } + return iter.Err() +} + +func TestStoreLoadEnrollmentGroups(t *testing.T) { + eg0 := test.NewEnrollmentGroup(t, uuid.NewString(), test.DPSOwner) + eg0.AttestationMechanism.X509.LeadCertificateName = "a" + eg0.HubIds = []string{"75eacc2f-ac28-4a42-a155-164393970ba4"} + eg0.AttestationMechanism.X509.CertificateChain = createCACertificate(t, eg0.GetAttestationMechanism().GetX509().GetLeadCertificateName()) + eg1 := test.NewEnrollmentGroup(t, uuid.NewString(), test.DPSOwner) + eg1.AttestationMechanism.X509.LeadCertificateName = "b" + eg1.HubIds = []string{"75eacc2f-ac28-4a42-a155-164393970ba4"} + eg1.AttestationMechanism.X509.CertificateChain = createCACertificate(t, eg1.GetAttestationMechanism().GetX509().GetLeadCertificateName()) + eg2 := test.NewEnrollmentGroup(t, uuid.NewString(), test.DPSOwner) + eg2.AttestationMechanism.X509.LeadCertificateName = "b" + eg2.AttestationMechanism.X509.CertificateChain = createCACertificate(t, eg2.GetAttestationMechanism().GetX509().GetLeadCertificateName()) + eg3 := test.NewEnrollmentGroup(t, uuid.NewString(), test.DPSOwner) + eg3.AttestationMechanism.X509.LeadCertificateName = "c" + eg3.AttestationMechanism.X509.CertificateChain = createCACertificate(t, eg3.GetAttestationMechanism().GetX509().GetLeadCertificateName()) + eg4 := test.NewEnrollmentGroup(t, uuid.NewString(), anotherOwner) + enrollmentGroups := pb.EnrollmentGroups{ + eg0, + eg1, + eg2, + eg3, + eg4, + } + + type args struct { + owner string + query *store.EnrollmentGroupsQuery + } + tests := []struct { + name string + args args + wantErr bool + want pb.EnrollmentGroups + }{ + { + name: "all", + args: args{ + query: nil, + }, + want: enrollmentGroups, + }, + { + name: "all owner", + args: args{ + owner: enrollmentGroups[0].GetOwner(), + query: nil, + }, + want: enrollmentGroups[:4], + }, + + { + name: "all another owner", + args: args{ + owner: enrollmentGroups[4].GetOwner(), + query: nil, + }, + want: enrollmentGroups[4:], + }, + { + name: "id", + args: args{ + owner: enrollmentGroups[1].GetOwner(), + query: &store.EnrollmentGroupsQuery{IdFilter: []string{enrollmentGroups[1].GetId()}}, + }, + want: []*store.EnrollmentGroup{enrollmentGroups[1]}, + }, + { + name: "hubids", + args: args{ + owner: enrollmentGroups[0].GetOwner(), + query: &store.EnrollmentGroupsQuery{HubIdFilter: enrollmentGroups[0].GetHubIds()}, + }, + want: []*store.EnrollmentGroup{enrollmentGroups[0], enrollmentGroups[1]}, + }, + { + name: "certificateName", + args: args{ + owner: enrollmentGroups[1].GetOwner(), + query: &store.EnrollmentGroupsQuery{AttestationMechanismX509CertificateNames: []string{enrollmentGroups[1].GetAttestationMechanism().GetX509().GetLeadCertificateName()}}, + }, + want: []*store.EnrollmentGroup{enrollmentGroups[1], enrollmentGroups[2]}, + }, + { + name: "multiple queries", + args: args{ + owner: enrollmentGroups[0].GetOwner(), + query: &store.EnrollmentGroupsQuery{AttestationMechanismX509CertificateNames: []string{enrollmentGroups[0].GetAttestationMechanism().GetX509().GetLeadCertificateName(), enrollmentGroups[3].GetAttestationMechanism().GetX509().GetLeadCertificateName()}}, + }, + want: []*store.EnrollmentGroup{enrollmentGroups[0], enrollmentGroups[3]}, + }, + { + name: "not found", + args: args{ + owner: enrollmentGroups[0].GetOwner(), + query: &store.EnrollmentGroupsQuery{IdFilter: []string{"not found"}}, + }, + }, + } + + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx := context.Background() + for _, l := range enrollmentGroups { + err := s.CreateEnrollmentGroup(ctx, l.GetOwner(), l) + require.NoError(t, err) + } + + for _, v := range tests { + tt := v + t.Run(tt.name, func(t *testing.T) { + var h testEnrollmentGroupHandler + err := s.LoadEnrollmentGroups(ctx, tt.args.owner, tt.args.query, h.Handle) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Len(t, h.lcs, len(tt.want)) + h.lcs.Sort() + want := make(pb.EnrollmentGroups, len(tt.want)) + copy(want, tt.want) + want.Sort() + + for i := range h.lcs { + hubTest.CheckProtobufs(t, want[i], h.lcs[i], hubTest.RequireToCheckFunc(require.Equal)) + } + }) + } +} + +func BenchmarkEnrollmentGroups(b *testing.B) { + numEnrollmentGroups := 1000 + ctx := context.Background() + b.ResetTimer() + s, cleanUpStore := test.NewMongoStore(b) + defer cleanUpStore() + ratioSharedCertificateNames := numEnrollmentGroups / 1000 + if ratioSharedCertificateNames < 1 { + ratioSharedCertificateNames = 1 + } + owner := uuid.NewString() + for i := 0; i < numEnrollmentGroups; i++ { + commonName := strconv.Itoa(i % ratioSharedCertificateNames) + err := s.CreateEnrollmentGroup(ctx, owner, &store.EnrollmentGroup{ + Id: uuid.NewString(), + AttestationMechanism: &pb.AttestationMechanism{ + X509: &pb.X509Configuration{ + CertificateChain: createCACertificate(b, commonName), + LeadCertificateName: commonName, + }, + }, + Owner: owner, + HubIds: []string{uuid.NewString()}, + }) + require.NoError(b, err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + var h testEnrollmentGroupHandler + b.StartTimer() + err := s.LoadEnrollmentGroups(ctx, owner, &pb.GetEnrollmentGroupsRequest{AttestationMechanismX509CertificateNames: []string{strconv.Itoa(i % ratioSharedCertificateNames)}}, h.Handle) + b.StopTimer() + require.NoError(b, err) + require.NotEmpty(b, h.lcs) + } +} diff --git a/device-provisioning-service/store/mongodb/hubs.go b/device-provisioning-service/store/mongodb/hubs.go new file mode 100644 index 000000000..853de7f49 --- /dev/null +++ b/device-provisioning-service/store/mongodb/hubs.go @@ -0,0 +1,121 @@ +package mongodb + +import ( + "context" + "errors" + "fmt" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const hubsCol = "hubs" + +func (s *Store) CreateHub(ctx context.Context, owner string, hub *store.Hub) error { + if err := hub.Validate(owner); err != nil { + return fmt.Errorf("invalid value: %w", err) + } + _, err := s.Collection(hubsCol).InsertOne(ctx, hub) + if err != nil { + return err + } + return nil +} + +func (s *Store) updateHub(ctx context.Context, owner string, hub *store.Hub, upsert bool) error { + if err := hub.Validate(owner); err != nil { + return fmt.Errorf("invalid value: %w", err) + } + res, err := s.Collection(hubsCol).UpdateOne(ctx, toHubFilter(owner, &store.HubsQuery{ + IdFilter: []string{hub.GetId()}, + }), bson.M{"$set": hub}, options.Update().SetUpsert(upsert)) + if err != nil { + return err + } + if res.UpsertedCount > 0 && upsert { + return nil + } + if res.ModifiedCount == 0 && res.MatchedCount == 0 { + return mongo.ErrNilDocument + } + return nil +} + +func (s *Store) UpsertHub(ctx context.Context, owner string, hub *store.Hub) error { + return s.updateHub(ctx, owner, hub, true) +} + +func (s *Store) UpdateHub(ctx context.Context, owner string, hub *store.Hub) error { + return s.updateHub(ctx, owner, hub, false) +} + +func toHubFilter(owner string, queries *store.HubsQuery) bson.D { + or := []bson.D{} + for _, q := range queries.GetIdFilter() { + or = append(or, addOwnerToFilter(owner, bson.D{{Key: store.IDKey, Value: q}})) + } + for _, q := range queries.GetHubIdFilter() { + or = append(or, addOwnerToFilter(owner, bson.D{{Key: store.HubIDKey, Value: q}})) + } + switch len(or) { + case 0: + return addOwnerToFilter(owner, bson.D{}) + case 1: + return or[0] + } + return bson.D{{Key: "$or", Value: or}} +} + +func (s *Store) DeleteHubs(ctx context.Context, owner string, query *store.HubsQuery) (int64, error) { + res, err := s.Collection(hubsCol).DeleteMany(ctx, toHubFilter(owner, query)) + if err != nil { + return -1, fmt.Errorf("cannot remove hubs for owner %v with filter %v: %w", owner, query.GetIdFilter(), err) + } + if res.DeletedCount == 0 { + return -1, fmt.Errorf("cannot remove hubs for owner %v with filter %v: not found", owner, query.GetIdFilter()) + } + return res.DeletedCount, nil +} + +func (s *Store) LoadHubs(ctx context.Context, owner string, query *store.HubsQuery, h store.LoadHubsFunc) error { + iter, err := s.Collection(hubsCol).Find(ctx, toHubFilter(owner, query)) + if errors.Is(err, mongo.ErrNilDocument) { + return nil + } + if err != nil { + return err + } + + i := hubsIterator{ + iter: iter, + } + err = h(ctx, &i) + + errClose := iter.Close(ctx) + if err == nil { + return errClose + } + return err +} + +type hubsIterator struct { + iter *mongo.Cursor +} + +func (i *hubsIterator) Next(ctx context.Context, s *store.Hub) bool { + if !i.iter.Next(ctx) { + return false + } + err := i.iter.Decode(s) + return err == nil +} + +func (i *hubsIterator) Err() error { + return i.iter.Err() +} + +func (s *Store) WatchHubs(ctx context.Context) (store.WatchHubIter, error) { + return s.watch(ctx, s.Collection(hubsCol)) +} diff --git a/device-provisioning-service/store/mongodb/hubs_test.go b/device-provisioning-service/store/mongodb/hubs_test.go new file mode 100644 index 000000000..67b1d1a5b --- /dev/null +++ b/device-provisioning-service/store/mongodb/hubs_test.go @@ -0,0 +1,335 @@ +package mongodb_test + +import ( + "context" + "testing" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/stretchr/testify/require" +) + +func TestStoreCreateHub(t *testing.T) { + hubID := "id" + owner := "owner" + type args struct { + owner string + hub *store.Hub + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "invalid ID", + args: args{ + owner: owner, + hub: &store.Hub{}, + }, + wantErr: true, + }, + { + name: "valid", + args: args{ + owner: owner, + hub: test.NewHub(hubID, owner), + }, + }, + { + name: "duplicity", + args: args{ + owner: owner, + hub: &store.Hub{ + Id: hubID, + }, + }, + wantErr: true, + }, + } + + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx := context.Background() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := s.CreateHub(ctx, tt.args.owner, tt.args.hub) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestStoreUpdateHub(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + const owner = "owner" + + ctx := context.Background() + hub := test.NewHub("id", owner) + err := s.CreateHub(ctx, owner, hub) + require.NoError(t, err) + + hub.Gateways = []string{"abc"} + + type args struct { + owner string + hub *store.Hub + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "invalid ID", + args: args{ + owner: owner, + hub: &store.Hub{}, + }, + wantErr: true, + }, + { + name: "valid", + args: args{ + owner: owner, + hub: hub, + }, + }, + { + name: "duplicity", + args: args{ + owner: owner, + hub: hub, + }, + }, + { + name: "not exist", + args: args{ + owner: owner, + hub: &store.Hub{Id: "notExist"}, + }, + wantErr: true, + }, + { + name: "another owner", + args: args{ + owner: anotherOwner, + hub: hub, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := s.UpdateHub(ctx, tt.args.owner, tt.args.hub) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestStoreDeleteHub(t *testing.T) { + const owner = "owner" + hubIDs := []string{"0", "1", "2"} + owners := []string{owner, owner, anotherOwner} + + type args struct { + owner string + query *store.HubsQuery + } + tests := []struct { + name string + args args + wantErr bool + count int64 + }{ + { + name: "invalid cloudId", + args: args{ + owner: owner, + query: &store.HubsQuery{ + IdFilter: []string{"notFound"}, + }, + }, + wantErr: true, + }, + { + name: "valid", + args: args{ + owner: owner, + query: &store.HubsQuery{ + IdFilter: []string{hubIDs[0]}, + }, + }, + count: 1, + }, + { + name: "valid multiple", + args: args{ + owner: owner, + query: &store.HubsQuery{ + IdFilter: []string{hubIDs[1], hubIDs[2]}, + }, + }, + count: 1, + }, + { + name: "another owner", + args: args{ + owner: "owner", + query: &store.HubsQuery{ + IdFilter: []string{hubIDs[2]}, + }, + }, + wantErr: true, + }, + } + + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx := context.Background() + for idx, id := range hubIDs { + err := s.CreateHub(ctx, owners[idx], test.NewHub(id, owners[idx])) + require.NoError(t, err) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := s.DeleteHubs(ctx, tt.args.owner, tt.args.query) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.count, got) + }) + } +} + +type testHubHandler struct { + lcs pb.Hubs +} + +func (h *testHubHandler) Handle(ctx context.Context, iter store.HubIter) (err error) { + for { + var hub store.Hub + if !iter.Next(ctx, &hub) { + break + } + h.lcs = append(h.lcs, &hub) + } + return iter.Err() +} + +func TestStoreLoadHubs(t *testing.T) { + const owner = "owner" + hubs := pb.Hubs{ + test.NewHub("id0", owner), + test.NewHub("id1", owner), + test.NewHub("id2", anotherOwner), + } + + type args struct { + owner string + query *store.HubsQuery + } + tests := []struct { + name string + args args + wantErr bool + want pb.Hubs + }{ + { + name: "all", + args: args{ + owner: owner, + query: nil, + }, + want: hubs[:2], + }, + { + name: "id", + args: args{ + owner: owner, + query: &store.HubsQuery{IdFilter: []string{hubs[1].GetId()}}, + }, + want: []*store.Hub{hubs[1]}, + }, + { + name: "hubId", + args: args{ + owner: owner, + query: &store.HubsQuery{HubIdFilter: []string{hubs[1].GetHubId()}}, + }, + want: []*store.Hub{hubs[1]}, + }, + { + name: "multiple queries", + args: args{ + owner: owner, + query: &store.HubsQuery{ + IdFilter: []string{hubs[0].GetId(), hubs[2].GetId()}, + HubIdFilter: []string{hubs[1].GetHubId()}, + }, + }, + want: []*store.Hub{hubs[0], hubs[1]}, + }, + { + name: "not found", + args: args{ + owner: owner, + query: &store.HubsQuery{IdFilter: []string{"not found"}}, + }, + }, + { + name: "hubId - another owner", + args: args{ + owner: anotherOwner, + query: &store.HubsQuery{HubIdFilter: []string{hubs[1].GetHubId()}}, + }, + }, + } + + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx := context.Background() + for _, l := range hubs { + err := s.CreateHub(ctx, l.GetOwner(), l) + require.NoError(t, err) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var h testHubHandler + err := s.LoadHubs(ctx, tt.args.owner, tt.args.query, h.Handle) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Len(t, h.lcs, len(tt.want)) + h.lcs.Sort() + want := make(pb.Hubs, len(tt.want)) + copy(want, tt.want) + want.Sort() + + for i := range h.lcs { + hubTest.CheckProtobufs(t, want[i], h.lcs[i], hubTest.RequireToCheckFunc(require.Equal)) + } + }) + } +} diff --git a/device-provisioning-service/store/mongodb/provisionedRecords.go b/device-provisioning-service/store/mongodb/provisionedRecords.go new file mode 100644 index 000000000..d1c993593 --- /dev/null +++ b/device-provisioning-service/store/mongodb/provisionedRecords.go @@ -0,0 +1,118 @@ +package mongodb + +import ( + "context" + "errors" + "fmt" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +const provisionedRecordsCol = "provisionedRecords" + +func validateProvisioningRecord(owner string, provisionedRecord *store.ProvisioningRecord) error { + if provisionedRecord.GetId() == "" { + return errors.New("empty provisioned record ID") + } + if owner != "" && provisionedRecord.GetOwner() != owner { + return fmt.Errorf("owner('%v') expects %v", provisionedRecord.GetOwner(), owner) + } + if provisionedRecord.GetAttestation() != nil && provisionedRecord.GetAttestation().GetDate() == 0 { + return errors.New("empty attestation date") + } + if provisionedRecord.GetAcl() != nil && provisionedRecord.GetAcl().GetStatus().GetDate() == 0 { + return errors.New("empty ACL date") + } + if provisionedRecord.GetCloud() != nil && provisionedRecord.GetCloud().GetStatus().GetDate() == 0 { + return errors.New("empty cloud date") + } + if provisionedRecord.GetCredential() != nil && provisionedRecord.GetCredential().GetStatus().GetDate() == 0 { + return errors.New("empty credential status date") + } + return nil +} + +func (s *Store) UpdateProvisioningRecord(_ context.Context, owner string, provisionedRecord *store.ProvisioningRecord) error { + if err := validateProvisioningRecord(owner, provisionedRecord); err != nil { + return err + } + s.bulkWriter.push(provisionedRecord) + return nil +} + +func toProvisioningRecordsQueryFilter(owner string, queries *store.ProvisioningRecordsQuery) bson.D { + or := []bson.D{} + for _, q := range queries.GetIdFilter() { + or = append(or, addOwnerToFilter(owner, bson.D{{Key: store.IDKey, Value: q}})) + } + for _, q := range queries.GetEnrollmentGroupIdFilter() { + or = append(or, addOwnerToFilter(owner, bson.D{{Key: store.EnrollmentGroupIDKey, Value: q}})) + } + for _, q := range queries.GetDeviceIdFilter() { + or = append(or, addOwnerToFilter(owner, bson.D{{Key: store.DeviceIDKey, Value: q}})) + } + switch len(or) { + case 0: + return addOwnerToFilter(owner, bson.D{}) + case 1: + return or[0] + } + return bson.D{{Key: "$or", Value: or}} +} + +func (s *Store) DeleteProvisioningRecords(ctx context.Context, owner string, query *store.ProvisioningRecordsQuery) (int64, error) { + q := toProvisioningRecordsQueryFilter(owner, query) + res, err := s.Collection(provisionedRecordsCol).DeleteMany(ctx, q) + if err != nil { + return 0, fmt.Errorf("cannot remove device provision records for owner %v: %w", owner, err) + } + return res.DeletedCount, nil +} + +func (s *Store) LoadProvisioningRecords(ctx context.Context, owner string, query *store.ProvisioningRecordsQuery, h store.LoadProvisioningRecordsFunc) error { + col := s.Collection(provisionedRecordsCol) + iter, err := col.Find(ctx, toProvisioningRecordsQueryFilter(owner, query)) + if errors.Is(err, mongo.ErrNilDocument) { + return nil + } + if err != nil { + return err + } + + i := provisioningRecordsIterator{ + iter: iter, + } + err = h(ctx, &i) + + errClose := iter.Close(ctx) + if err == nil { + return errClose + } + return err +} + +type provisioningRecordsIterator struct { + iter *mongo.Cursor + err error +} + +func (i *provisioningRecordsIterator) Next(ctx context.Context, s *store.ProvisioningRecord) bool { + if !i.iter.Next(ctx) { + return false + } + err := i.iter.Decode(s) + if err != nil { + i.err = err + return false + } + return true +} + +func (i *provisioningRecordsIterator) Err() error { + if i.err != nil { + return i.err + } + return i.iter.Err() +} diff --git a/device-provisioning-service/store/mongodb/provisionedRecords_test.go b/device-provisioning-service/store/mongodb/provisionedRecords_test.go new file mode 100644 index 000000000..9aa1997ac --- /dev/null +++ b/device-provisioning-service/store/mongodb/provisionedRecords_test.go @@ -0,0 +1,477 @@ +package mongodb_test + +import ( + "context" + "strconv" + "sync" + "testing" + "time" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/device-provisioning-service/test" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func constDate() time.Time { + return time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC) +} + +func constDate1() time.Time { + return time.Date(2006, time.January, 5, 15, 4, 5, 0, time.UTC) +} + +func TestStoreUpdateProvisioningRecord(t *testing.T) { + const owner = "owner" + type args struct { + owner string + sub *store.ProvisioningRecord + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "invalid ID", + args: args{ + sub: &store.ProvisioningRecord{ + Owner: owner, + // Id: "deviceIDnotFound", + EnrollmentGroupId: "enrollmentGroupID", + Credential: &pb.CredentialStatus{ + IdentityCertificatePem: "certificate", + }, + Attestation: &pb.Attestation{ + Date: constDate().UnixNano(), + }, + }, + }, + wantErr: true, + }, + { + name: "valid", + args: args{ + owner: owner, + sub: &store.ProvisioningRecord{ + Owner: owner, + Id: "mfgID", + EnrollmentGroupId: "enrollmentGroupID", + Attestation: &pb.Attestation{ + Date: constDate().UnixNano(), + }, + }, + }, + }, + { + name: "valid - owner is not set in filter", + args: args{ + sub: &store.ProvisioningRecord{ + Owner: anotherOwner, + Id: "mfgID", + EnrollmentGroupId: "enrollmentGroupID", + Attestation: &pb.Attestation{ + Date: constDate().UnixNano(), + }, + }, + }, + }, + { + name: "invalid owner", + args: args{ + owner: anotherOwner, + sub: &store.ProvisioningRecord{ + Owner: anotherOwner, + // Id: "deviceIDnotFound", + EnrollmentGroupId: "enrollmentGroupID", + Credential: &pb.CredentialStatus{ + IdentityCertificatePem: "certificate", + }, + Attestation: &pb.Attestation{ + Date: constDate().UnixNano(), + }, + }, + }, + wantErr: true, + }, + } + + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx := context.Background() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := s.UpdateProvisioningRecord(ctx, tt.args.owner, tt.args.sub) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + err = s.FlushBulkWriter() + require.NoError(t, err) + }) + } +} + +func TestStoreDeleteProvisioningRecords(t *testing.T) { + const owner = "owner" + type args struct { + query *store.ProvisioningRecordsQuery + owner string + } + tests := []struct { + name string + args args + wantCount int64 + }{ + { + name: "invalid cloudId", + args: args{ + owner: owner, + query: &store.ProvisioningRecordsQuery{ + IdFilter: []string{"notFound"}, + }, + }, + wantCount: 0, + }, + { + name: "another owner", + args: args{ + owner: anotherOwner, + query: &store.ProvisioningRecordsQuery{ + IdFilter: []string{"mfgID"}, + }, + }, + wantCount: 0, + }, + { + name: "valid", + args: args{ + owner: owner, + query: &store.ProvisioningRecordsQuery{ + IdFilter: []string{"mfgID"}, + }, + }, + wantCount: 1, + }, + } + + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx := context.Background() + err := s.UpdateProvisioningRecord(ctx, owner, &store.ProvisioningRecord{ + Owner: owner, + Id: "mfgID", + EnrollmentGroupId: "enrollmentGroupID", + Attestation: &pb.Attestation{ + Date: constDate().UnixNano(), + }, + }) + require.NoError(t, err) + err = s.FlushBulkWriter() + require.NoError(t, err) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := s.DeleteProvisioningRecords(ctx, tt.args.owner, tt.args.query) + require.NoError(t, err) + assert.Equal(t, tt.wantCount, got) + }) + } +} + +type testProvisioningRecordHandler struct { + lcs pb.ProvisioningRecords +} + +func (h *testProvisioningRecordHandler) Handle(ctx context.Context, iter store.ProvisioningRecordIter) (err error) { + for { + var sub store.ProvisioningRecord + if !iter.Next(ctx, &sub) { + break + } + h.lcs = append(h.lcs, &sub) + } + return iter.Err() +} + +func TestStoreLoadProvisioningRecords(t *testing.T) { + const owner = "owner" + upds := pb.ProvisioningRecords{ + { + Id: "mfgID", + EnrollmentGroupId: "enrollmentGroupID", + CreationDate: constDate().UnixNano(), + Credential: &pb.CredentialStatus{ + IdentityCertificatePem: "certificate", + Status: &pb.ProvisionStatus{ + Date: constDate().UnixNano(), + }, + }, + Acl: &pb.ACLStatus{ + Status: &pb.ProvisionStatus{ + Date: constDate1().UnixNano(), + }, + }, + Cloud: &pb.CloudStatus{ + Status: &pb.ProvisionStatus{ + Date: constDate1().UnixNano(), + }, + }, + Attestation: &pb.Attestation{ + Date: constDate1().UnixNano(), + }, + Owner: owner, + }, + { + Id: "mfgID", + EnrollmentGroupId: "enrollmentGroupID", + CreationDate: constDate1().UnixNano(), + Credential: &pb.CredentialStatus{ + IdentityCertificatePem: "certificate1", + Status: &pb.ProvisionStatus{ + Date: constDate1().UnixNano(), + }, + }, + Owner: owner, + }, + { + Id: "mfgID", + EnrollmentGroupId: "enrollmentGroupID", + CreationDate: constDate1().UnixNano(), + Credential: &pb.CredentialStatus{ + IdentityCertificatePem: "certificate", + Status: &pb.ProvisionStatus{ + Date: constDate().UnixNano(), + }, + }, + Attestation: &pb.Attestation{ + Date: constDate().UnixNano(), + }, + Acl: &pb.ACLStatus{ + Status: &pb.ProvisionStatus{ + Date: constDate().UnixNano(), + }, + }, + Cloud: &pb.CloudStatus{ + Status: &pb.ProvisionStatus{ + Date: constDate().UnixNano(), + }, + }, + Owner: owner, + }, + { + Id: "mfgID2", + EnrollmentGroupId: "enrollmentGroupID", + CreationDate: constDate().UnixNano(), + Credential: &pb.CredentialStatus{ + IdentityCertificatePem: "certificate", + Status: &pb.ProvisionStatus{ + Date: constDate().UnixNano(), + }, + }, + Attestation: &pb.Attestation{ + Date: constDate().UnixNano(), + }, + Owner: owner, + }, + { + Id: "mfgID3", + EnrollmentGroupId: "enrollmentGroupID1", + CreationDate: constDate().UnixNano(), + Credential: &pb.CredentialStatus{ + IdentityCertificatePem: "certificate", + Status: &pb.ProvisionStatus{ + Date: constDate().UnixNano(), + }, + }, + Attestation: &pb.Attestation{ + Date: constDate().UnixNano(), + }, + Owner: owner, + }, + } + + lcs := pb.ProvisioningRecords{ + { + Id: "mfgID", + EnrollmentGroupId: "enrollmentGroupID", + CreationDate: constDate().UnixNano(), + Credential: &pb.CredentialStatus{ + IdentityCertificatePem: "certificate1", + Status: &pb.ProvisionStatus{ + Date: constDate1().UnixNano(), + }, + }, + Attestation: &pb.Attestation{ + Date: constDate1().UnixNano(), + }, + Acl: &pb.ACLStatus{ + Status: &pb.ProvisionStatus{ + Date: constDate1().UnixNano(), + }, + }, + Cloud: &pb.CloudStatus{ + Status: &pb.ProvisionStatus{ + Date: constDate1().UnixNano(), + }, + }, + Owner: owner, + }, + upds[len(upds)-2], + upds[len(upds)-1], + } + + type args struct { + owner string + query *store.ProvisioningRecordsQuery + } + tests := []struct { + name string + args args + wantErr bool + want pb.ProvisioningRecords + }{ + { + name: "all", + args: args{ + owner: owner, + query: nil, + }, + want: lcs, + }, + { + name: "all - another owner", + args: args{ + owner: anotherOwner, + query: nil, + }, + }, + { + name: "id", + args: args{ + owner: owner, + query: &store.ProvisioningRecordsQuery{IdFilter: []string{lcs[1].GetId()}}, + }, + want: []*store.ProvisioningRecord{lcs[1]}, + }, + { + name: "enrollmentGroupID", + args: args{ + owner: owner, + query: &store.ProvisioningRecordsQuery{EnrollmentGroupIdFilter: []string{lcs[0].GetEnrollmentGroupId()}}, + }, + want: []*store.ProvisioningRecord{lcs[0], lcs[1]}, + }, + { + name: "multiple queries", + args: args{ + owner: owner, + query: &store.ProvisioningRecordsQuery{EnrollmentGroupIdFilter: []string{lcs[0].GetEnrollmentGroupId(), lcs[2].GetEnrollmentGroupId()}}, + }, + want: lcs, + }, + { + name: "not found", + args: args{ + owner: owner, + query: &store.ProvisioningRecordsQuery{IdFilter: []string{"not found"}}, + }, + }, + } + + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx := context.Background() + for idx, l := range upds { + err := s.UpdateProvisioningRecord(ctx, l.GetOwner(), l) + require.NoError(t, err) + if idx%2 == 0 { + time.Sleep(time.Second * 2) + } + } + time.Sleep(time.Second) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var h testProvisioningRecordHandler + err := s.LoadProvisioningRecords(ctx, tt.args.owner, tt.args.query, h.Handle) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Len(t, h.lcs, len(tt.want)) + h.lcs.Sort() + tt.want.Sort() + + for i := range h.lcs { + hubTest.CheckProtobufs(t, tt.want[i], h.lcs[i], hubTest.RequireToCheckFunc(require.Equal)) + } + }) + } +} + +func BenchmarkProvisioningRecords(b *testing.B) { + data := make([]*store.ProvisioningRecord, 0, 5001) + const owner = "owner" + dataCap := cap(data) + for i := 0; i < dataCap; i++ { + data = append(data, &store.ProvisioningRecord{ + Id: "mfgID" + strconv.Itoa(i), + EnrollmentGroupId: "enrollmentGroupID", + CreationDate: constDate().UnixNano(), + Credential: &pb.CredentialStatus{ + IdentityCertificatePem: "certificate", + Status: &pb.ProvisionStatus{ + Date: constDate().UnixNano(), + }, + }, + Acl: &pb.ACLStatus{ + Status: &pb.ProvisionStatus{ + Date: constDate1().UnixNano(), + }, + }, + Cloud: &pb.CloudStatus{ + Status: &pb.ProvisionStatus{ + Date: constDate1().UnixNano(), + }, + }, + Attestation: &pb.Attestation{ + Date: constDate1().UnixNano(), + }, + Owner: owner, + }) + } + + ctx := context.Background() + b.ResetTimer() + s, cleanUpStore := test.NewMongoStore(b) + defer cleanUpStore() + for i := uint32(0); i < uint32(b.N); i++ { + b.StopTimer() + err := s.Clear(ctx) + require.NoError(b, err) + b.StartTimer() + func() { + var wg sync.WaitGroup + wg.Add(len(data)) + for _, l := range data { + go func(l *pb.ProvisioningRecord) { + defer wg.Done() + err := s.UpdateProvisioningRecord(ctx, l.GetOwner(), l) + assert.NoError(b, err) + }(l) + } + wg.Wait() + err := s.FlushBulkWriter() + require.NoError(b, err) + }() + } +} diff --git a/device-provisioning-service/store/mongodb/store.go b/device-provisioning-service/store/mongodb/store.go new file mode 100644 index 000000000..7cd573847 --- /dev/null +++ b/device-provisioning-service/store/mongodb/store.go @@ -0,0 +1,117 @@ +package mongodb + +import ( + "context" + "crypto/tls" + "fmt" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + "github.com/plgd-dev/hub/v2/pkg/log" + pkgMongo "github.com/plgd-dev/hub/v2/pkg/mongodb" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.opentelemetry.io/otel/trace" +) + +type Store struct { + *pkgMongo.Store + bulkWriter *bulkWriter +} + +var EnrollmentGroupIDKeyQueryIndex = mongo.IndexModel{ + Keys: bson.D{ + {Key: store.EnrollmentGroupIDKey, Value: 1}, + }, +} + +var DeviceIDKeyQueryIndex = mongo.IndexModel{ + Keys: bson.D{ + {Key: store.DeviceIDKey, Value: 1}, + }, +} + +var AttestationMechanismX509CertificateNamesKeyQueryIndex = mongo.IndexModel{ + Keys: bson.D{ + {Key: store.AttestationMechanismX509LeadCertificateNameKey, Value: 1}, + }, +} + +var AttestationMechanismX509CertificateNamesOwnerKeyQueryIndex = mongo.IndexModel{ + Keys: bson.D{ + {Key: store.AttestationMechanismX509LeadCertificateNameKey, Value: 1}, + {Key: store.OwnerKey, Value: 1}, + }, +} + +var IDKeyQueryIndex = mongo.IndexModel{ + Keys: bson.D{ + {Key: store.IDKey, Value: 1}, + }, +} + +var IDOwnerKeyQueryIndex = mongo.IndexModel{ + Keys: bson.D{ + {Key: store.IDKey, Value: 1}, + {Key: store.OwnerKey, Value: 1}, + }, +} + +var HubIDKeyQueryIndex = mongo.IndexModel{ + Keys: bson.D{ + {Key: store.HubIDsKey, Value: 1}, + }, +} + +var HubIDOwnerKeyQueryIndex = mongo.IndexModel{ + Keys: bson.D{ + {Key: store.HubIDsKey, Value: 1}, + {Key: store.OwnerKey, Value: 1}, + }, +} + +func NewStore(ctx context.Context, cfg Config, tls *tls.Config, logger log.Logger, tracerProvider trace.TracerProvider) (*Store, error) { + m, err := pkgMongo.NewStore(ctx, &cfg.Mongo, tls, tracerProvider) + if err != nil { + return nil, err + } + bulkWriter := newBulkWriter(m.Collection(provisionedRecordsCol), cfg.BulkWrite.DocumentLimit, cfg.BulkWrite.ThrottleTime, cfg.BulkWrite.Timeout, logger) + s := Store{Store: m, bulkWriter: bulkWriter} + err = s.EnsureIndex(ctx, provisionedRecordsCol, EnrollmentGroupIDKeyQueryIndex, DeviceIDKeyQueryIndex) + if err != nil { + return nil, err + } + err = s.EnsureIndex(ctx, enrollmentGroupsCol, AttestationMechanismX509CertificateNamesOwnerKeyQueryIndex, AttestationMechanismX509CertificateNamesKeyQueryIndex, HubIDOwnerKeyQueryIndex, IDOwnerKeyQueryIndex, HubIDKeyQueryIndex) + if err != nil { + return nil, err + } + + s.SetOnClear(s.clearDatabases) + return &s, nil +} + +func (s *Store) clearDatabases(ctx context.Context) error { + var errors []error + if err := s.Collection(provisionedRecordsCol).Drop(ctx); err != nil { + errors = append(errors, err) + } + if err := s.Collection(enrollmentGroupsCol).Drop(ctx); err != nil { + errors = append(errors, err) + } + if err := s.Collection(hubsCol).Drop(ctx); err != nil { + errors = append(errors, err) + } + if len(errors) > 0 { + return fmt.Errorf("cannot clear: %v", errors) + } + return nil +} + +func (s *Store) Close(ctx context.Context) error { + s.bulkWriter.Close() + return s.Store.Close(ctx) +} + +func (s *Store) FlushBulkWriter() error { + _, err := s.bulkWriter.bulkWrite() + return err +} diff --git a/device-provisioning-service/store/provisioningRecord.go b/device-provisioning-service/store/provisioningRecord.go new file mode 100644 index 000000000..b9468b500 --- /dev/null +++ b/device-provisioning-service/store/provisioningRecord.go @@ -0,0 +1,23 @@ +package store + +import ( + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" +) + +const ( + EnrollmentGroupIDKey = "enrollmentGroupId" // must match with pb.ProvisioningRecord.EnrollmentGroupID tag + DeviceIDKey = "deviceId" // must match with pb.ProvisioningRecord.DeviceID tag + AttestationKey = "attestation" // must match with pb.ProvisioningRecord.Attestation tag + DateKey = "date" // must match with pb.ProvisioningRecord.Date tag + CloudKey = "cloud" // must match with pb.ProvisioningRecord.Cloud tag + ACLKey = "acl" // must match with pb.ProvisioningRecord.Acl tag + CredentialKey = "credential" // must match with pb.ProvisioningRecord.Credential tag + PlgdTimeKey = "plgdTime" // must match with pb.ProvisioningRecord.PlgdTime tag + OwnershipKey = "ownership" // must match with pb.ProvisioningRecord.Ownership tag + StatusKey = "status" // must match with pb.ProvisioningRecord.Status tag + CreationDateKey = "creationDate" // must match with pb.ProvisioningRecord.CreationDate tag + LocalEndpointsKey = "localEndpoints" // must match with pb.ProvisioningRecord.LocalEndpoints tag + OwnerKey = "owner" // must match with pb.ProvisioningRecord.Owner tag +) + +type ProvisioningRecord = pb.ProvisioningRecord diff --git a/device-provisioning-service/store/store.go b/device-provisioning-service/store/store.go new file mode 100644 index 000000000..ace4dd111 --- /dev/null +++ b/device-provisioning-service/store/store.go @@ -0,0 +1,75 @@ +package store + +import ( + "context" + + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" +) + +type ( + ProvisioningRecordsQuery = pb.GetProvisioningRecordsRequest + EnrollmentGroupsQuery = pb.GetEnrollmentGroupsRequest + HubsQuery = pb.GetHubsRequest +) + +type ProvisioningRecordIter interface { + Next(ctx context.Context, provisioningRecord *ProvisioningRecord) bool + Err() error +} + +type EnrollmentGroupIter interface { + Next(ctx context.Context, enrollmentGroup *EnrollmentGroup) bool + Err() error +} + +type WatchEnrollmentGroupIter interface { + Next(ctx context.Context) (event Event, id string, ok bool) + Err() error + Close() error +} + +type WatchHubIter interface { + Next(ctx context.Context) (event Event, id string, ok bool) + Err() error + Close() error +} + +type HubIter interface { + Next(ctx context.Context, hub *Hub) bool + Err() error +} + +type ( + LoadProvisioningRecordsFunc = func(ctx context.Context, iter ProvisioningRecordIter) (err error) + LoadEnrollmentGroupsFunc = func(ctx context.Context, iter EnrollmentGroupIter) (err error) + LoadHubsFunc = func(ctx context.Context, iter HubIter) (err error) +) + +type Event string + +const ( + EventDelete Event = "delete" + EventUpdate Event = "update" +) + +type Store interface { + UpdateProvisioningRecord(ctx context.Context, owner string, sub *ProvisioningRecord) error + DeleteProvisioningRecords(ctx context.Context, owner string, query *ProvisioningRecordsQuery) (int64, error) + LoadProvisioningRecords(ctx context.Context, owner string, query *ProvisioningRecordsQuery, h LoadProvisioningRecordsFunc) error + + CreateEnrollmentGroup(ctx context.Context, owner string, enrollmentGroup *EnrollmentGroup) error + UpdateEnrollmentGroup(ctx context.Context, owner string, enrollmentGroup *EnrollmentGroup) error + DeleteEnrollmentGroups(ctx context.Context, owner string, query *EnrollmentGroupsQuery) (int64, error) + LoadEnrollmentGroups(ctx context.Context, owner string, query *EnrollmentGroupsQuery, h LoadEnrollmentGroupsFunc) error + // returned iterator need to be close after use. + WatchEnrollmentGroups(ctx context.Context) (WatchEnrollmentGroupIter, error) + + CreateHub(ctx context.Context, owner string, hub *Hub) error + UpdateHub(ctx context.Context, owner string, hub *Hub) error + DeleteHubs(ctx context.Context, owner string, query *HubsQuery) (int64, error) + LoadHubs(ctx context.Context, owner string, query *HubsQuery, h LoadHubsFunc) error + // returned iterator need to be close after use. + WatchHubs(ctx context.Context) (WatchHubIter, error) + + Close(ctx context.Context) error +} diff --git a/device-provisioning-service/test/onboardDpsSim.go b/device-provisioning-service/test/onboardDpsSim.go new file mode 100644 index 000000000..f2ccf68a7 --- /dev/null +++ b/device-provisioning-service/test/onboardDpsSim.go @@ -0,0 +1,289 @@ +package test + +import ( + "context" + "testing" + "time" + + deviceClient "github.com/plgd-dev/device/v2/client" + "github.com/plgd-dev/device/v2/schema" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/hub/v2/device-provisioning-service/uri" + "github.com/plgd-dev/hub/v2/grpc-gateway/client" + "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + isEvents "github.com/plgd-dev/hub/v2/identity-store/events" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/plgd-dev/hub/v2/test/device" + "github.com/plgd-dev/hub/v2/test/device/ocf" + hubTestPb "github.com/plgd-dev/hub/v2/test/pb" + "github.com/plgd-dev/hub/v2/test/sdk" + "github.com/stretchr/testify/require" +) + +func SubscribeToEvents(t *testing.T, subClient pb.GrpcGateway_SubscribeToEventsClient, sub *pb.SubscribeToEvents) (string, string) { + err := subClient.Send(sub) + require.NoError(t, err) + ev, err := subClient.Recv() + require.NoError(t, err) + expectedEvent := &pb.Event{ + SubscriptionId: ev.GetSubscriptionId(), + CorrelationId: ev.GetCorrelationId(), + Type: &pb.Event_OperationProcessed_{ + OperationProcessed: &pb.Event_OperationProcessed{ + ErrorStatus: &pb.Event_OperationProcessed_ErrorStatus{ + Code: pb.Event_OperationProcessed_ErrorStatus_OK, + }, + }, + }, + } + hubTest.CheckProtobufs(t, expectedEvent, ev, hubTest.RequireToCheckFunc(require.Equal)) + return ev.GetSubscriptionId(), ev.GetCorrelationId() +} + +func SubscribeToAllEvents(ctx context.Context, t *testing.T, c pb.GrpcGatewayClient, correllationID string) (pb.GrpcGateway_SubscribeToEventsClient, string) { + subClient, err := client.New(c).GrpcGatewayClient().SubscribeToEvents(ctx) + require.NoError(t, err) + err = subClient.Send(&pb.SubscribeToEvents{ + CorrelationId: correllationID, + Action: &pb.SubscribeToEvents_CreateSubscription_{ + CreateSubscription: &pb.SubscribeToEvents_CreateSubscription{}, + }, + }) + require.NoError(t, err) + ev, err := subClient.Recv() + require.NoError(t, err) + expectedEvent := &pb.Event{ + SubscriptionId: ev.GetSubscriptionId(), + CorrelationId: correllationID, + Type: &pb.Event_OperationProcessed_{ + OperationProcessed: &pb.Event_OperationProcessed{ + ErrorStatus: &pb.Event_OperationProcessed_ErrorStatus{ + Code: pb.Event_OperationProcessed_ErrorStatus_OK, + }, + }, + }, + } + hubTest.CheckProtobufs(t, expectedEvent, ev, hubTest.RequireToCheckFunc(require.Equal)) + return subClient, ev.GetSubscriptionId() +} + +func waitForEvents(t *testing.T, client pb.GrpcGateway_SubscribeToEventsClient, events map[string]*pb.Event) error { + for { + ev, err := client.Recv() + if err != nil { + return err + } + expectedEvent, ok := events[hubTestPb.GetEventID(ev)] + if !ok { + t.Logf("unexpected event %+v", ev) + continue + } + hubTestPb.CmpEvent(t, expectedEvent, ev, "") + delete(events, hubTestPb.GetEventID(ev)) + if len(events) == 0 { + return nil + } + } +} + +func WaitForDeviceStatus(t *testing.T, client pb.GrpcGateway_SubscribeToEventsClient, deviceID, subID, correlationID string, status commands.Connection_Status) error { + var dmus []*pb.Event_DeviceMetadataUpdated + + dmus = append(dmus, &pb.Event_DeviceMetadataUpdated{ + DeviceMetadataUpdated: hubTestPb.MakeDeviceMetadataUpdated(deviceID, status, hubTest.StringToApplicationProtocol(config.ACTIVE_COAP_SCHEME), true, commands.TwinSynchronization_OUT_OF_SYNC, ""), + }) + if status == commands.Connection_ONLINE { + dmus = append(dmus, &pb.Event_DeviceMetadataUpdated{ + DeviceMetadataUpdated: hubTestPb.MakeDeviceMetadataUpdated(deviceID, status, hubTest.StringToApplicationProtocol(config.ACTIVE_COAP_SCHEME), true, commands.TwinSynchronization_SYNCING, ""), + }) + dmus = append(dmus, &pb.Event_DeviceMetadataUpdated{ + DeviceMetadataUpdated: hubTestPb.MakeDeviceMetadataUpdated(deviceID, status, hubTest.StringToApplicationProtocol(config.ACTIVE_COAP_SCHEME), true, commands.TwinSynchronization_IN_SYNC, ""), + }) + } + + expectedEvents := make(map[string]*pb.Event) + for _, dmu := range dmus { + expectedEvents[hubTestPb.GetEventID(&pb.Event{Type: dmu})] = &pb.Event{ + SubscriptionId: subID, + CorrelationId: correlationID, + Type: dmu, + } + } + return waitForEvents(t, client, expectedEvents) +} + +func WaitForRegistered(t *testing.T, client pb.GrpcGateway_SubscribeToEventsClient, deviceID, subID, correlationID string) error { + dr := &pb.Event_DeviceRegistered_{ + DeviceRegistered: &pb.Event_DeviceRegistered{ + DeviceIds: []string{deviceID}, + EventMetadata: &isEvents.EventMetadata{ + HubId: config.HubID(), + }, + }, + } + expectedEvents := map[string]*pb.Event{ + hubTestPb.GetEventID(&pb.Event{Type: dr}): { + SubscriptionId: subID, + CorrelationId: correlationID, + Type: dr, + }, + } + return waitForEvents(t, client, expectedEvents) +} + +func ForceReprovision(ctx context.Context, c pb.GrpcGatewayClient, deviceID string) error { + data, err := ToJSON(ResourcePlgdDps{ForceReprovision: true}) + if err != nil { + return err + } + _, err = c.UpdateResource(ctx, &pb.UpdateResourceRequest{ + ResourceId: commands.NewResourceID(deviceID, ResourcePlgdDpsHref), + Content: &pb.Content{ + ContentType: message.AppJSON.String(), + Data: data, + }, + }) + return err +} + +type configureDpsDevice = func(ctx context.Context, deviceID string, client *deviceClient.Client) error + +// onboardConfig represents the configuration options available for onboarding. +type onboardConfig struct { + sdkID string + sdkRootCA struct { + certificate []byte + key []byte + } + validFrom string + validFor string + configureDevice configureDpsDevice +} + +// Option interface used for setting optional onboardConfig properties. +type Option interface { + apply(*onboardConfig) +} + +type optionFunc func(*onboardConfig) + +func (o optionFunc) apply(c *onboardConfig) { + o(c) +} + +// WithSdkID creates Option that overrides the default device ID used by the SDK client. +func WithSdkID(id string) Option { + return optionFunc(func(cfg *onboardConfig) { + cfg.sdkID = id + }) +} + +// WithSdkRootCA creates Option that overrides the certificate authority used by the SDK client. +func WithSdkRootCA(certificate, key []byte) Option { + return optionFunc(func(cfg *onboardConfig) { + cfg.sdkRootCA.certificate = certificate + cfg.sdkRootCA.key = key + }) +} + +// WithValidity creates Option that overrides the ValidFrom timestamp and CertExpiry +// interval used by the SDK client when generating certificates. +func WithValidity(validFrom, validFor string) Option { + return optionFunc(func(cfg *onboardConfig) { + cfg.validFrom = validFrom + cfg.validFor = validFor + }) +} + +// WithConfigureDevice sets a function that will be called after the device is owned, but before the device is onboarded. +func WithConfigureDevice(configureDevice configureDpsDevice) Option { + return optionFunc(func(cfg *onboardConfig) { + cfg.configureDevice = configureDevice + }) +} + +func newOnboardConfig(opts ...Option) *onboardConfig { + cfg := &onboardConfig{ + sdkID: isEvents.OwnerToUUID(DPSOwner), + validFrom: "2000-01-01T12:00:00Z", + validFor: "876000h", // 100 years + } + for _, opt := range opts { + opt.apply(cfg) + } + return cfg +} + +func (cfg *onboardConfig) sdkOptions() []sdk.Option { + var sdkOptions []sdk.Option + sdkOptions = append(sdkOptions, sdk.WithID(cfg.sdkID)) + if cfg.sdkRootCA.certificate != nil && cfg.sdkRootCA.key != nil { + sdkOptions = append(sdkOptions, sdk.WithRootCA(cfg.sdkRootCA.certificate, cfg.sdkRootCA.key)) + } + if cfg.validFrom != "" { + sdkOptions = append(sdkOptions, sdk.WithValidity(cfg.validFrom, cfg.validFor)) + } + return sdkOptions +} + +func OnboardDpsSim(ctx context.Context, t *testing.T, gc pb.GrpcGatewayClient, deviceID, dpsEndpoint string, expectedResources []schema.ResourceLink, opts ...Option) (string, func()) { + d := ocf.NewDevice(deviceID, TestDeviceName) + cleanup := OnboardDpsSimDevice(ctx, t, gc, d, dpsEndpoint, expectedResources, opts...) + return d.GetID(), cleanup +} + +func OnboardDpsSimDevice(ctx context.Context, t *testing.T, gc pb.GrpcGatewayClient, d device.Device, dpsEndpoint string, expectedResources []schema.ResourceLink, opts ...Option) func() { + onboardCfg := newOnboardConfig(opts...) + sdkOptions := onboardCfg.sdkOptions() + cloudSID := config.HubID() + require.NotEmpty(t, cloudSID) + devClient, err := sdk.NewClient(sdkOptions...) + require.NoError(t, err) + defer func() { + _ = devClient.Close(ctx) + }() + + deviceID, err := devClient.OwnDevice(ctx, d.GetID(), deviceClient.WithOTM(deviceClient.OTMType_JustWorks)) + require.NoError(t, err) + d.SetID(deviceID) + + if onboardCfg.configureDevice != nil { + err = onboardCfg.configureDevice(ctx, d.GetID(), devClient) + require.NoError(t, err) + } + + onboard := func() { + var v interface{} + endpoint := uri.CoAPsTCPSchemePrefix + dpsEndpoint + err = devClient.UpdateResource(ctx, d.GetID(), ResourcePlgdDpsHref, ResourcePlgdDps{Endpoint: &endpoint}, &v) + require.NoError(t, err) + } + if len(expectedResources) > 0 { + subClient, err := client.New(gc).GrpcGatewayClient().SubscribeToEvents(ctx) + require.NoError(t, err) + subID, corID := SubscribeToEvents(t, subClient, &pb.SubscribeToEvents{ + CorrelationId: "allEvents", + Action: &pb.SubscribeToEvents_CreateSubscription_{ + CreateSubscription: &pb.SubscribeToEvents_CreateSubscription{}, + }, + }) + onboard() + hubTest.WaitForDevice(t, subClient, d, subID, corID, expectedResources) + err = subClient.CloseSend() + require.NoError(t, err) + } else { + onboard() + } + + return func() { + client, err := sdk.NewClient(sdkOptions...) + require.NoError(t, err) + err = client.DisownDevice(ctx, d.GetID()) + require.NoError(t, err) + err = client.Close(ctx) + require.NoError(t, err) + time.Sleep(time.Second * 2) + } +} diff --git a/device-provisioning-service/test/provisionHandler.go b/device-provisioning-service/test/provisionHandler.go new file mode 100644 index 000000000..0c23c9af2 --- /dev/null +++ b/device-provisioning-service/test/provisionHandler.go @@ -0,0 +1,348 @@ +package test + +import ( + "context" + "fmt" + "strconv" + "sync" + "testing" + "time" + + "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/plgd-dev/go-coap/v3/message/status" + "github.com/plgd-dev/go-coap/v3/mux" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service" + "github.com/plgd-dev/hub/v2/pkg/log" + "go.uber.org/atomic" + "go.uber.org/zap/zapcore" +) + +type RequestHandlerWithDps struct { + t *testing.T + lock sync.Mutex + dpsCfg service.Config + dpsShutdown func() + logger log.Logger + started bool +} + +func (h *RequestHandlerWithDps) T() *testing.T { + return h.t +} + +func (h *RequestHandlerWithDps) Cfg() service.Config { + return h.dpsCfg +} + +func (h *RequestHandlerWithDps) IsStarted() bool { + h.lock.Lock() + defer h.lock.Unlock() + return h.started +} + +func (h *RequestHandlerWithDps) StartDps(opts ...service.Option) { + if h.dpsShutdown != nil { + return + } + h.Logf("start provisioning") + h.dpsShutdown = New(h.t, h.dpsCfg, opts...) + h.lock.Lock() + h.started = true + h.lock.Unlock() +} + +func (h *RequestHandlerWithDps) StopDps() { + h.Logf("stop provisioning") + var dpsShutdown func() + h.lock.Lock() + dpsShutdown = h.dpsShutdown + h.dpsShutdown = nil + h.lock.Unlock() + if dpsShutdown != nil { + dpsShutdown() + } +} + +func (h *RequestHandlerWithDps) RestartDps(opts ...service.Option) { + h.StopDps() + h.Logf("restart provisioning") + dpsShutdown := New(h.t, h.dpsCfg, opts...) + h.lock.Lock() + h.dpsShutdown = dpsShutdown + h.lock.Unlock() +} + +func (h *RequestHandlerWithDps) Logf(template string, args ...interface{}) { + h.logger.Debugf(template, args...) +} + +func MakeRequestHandlerWithDps(t *testing.T, dpsCfg service.Config) RequestHandlerWithDps { + return RequestHandlerWithDps{ + t: t, + dpsCfg: dpsCfg, + logger: log.NewLogger(log.Config{Level: zapcore.DebugLevel}), + } +} + +type HandlerID int + +const ( + HandlerIDDefault HandlerID = iota + HandlerIDTime + HandlerIDOwnership + HandlerIDCredentials + HandlerIDACLs + HandlerIDCloudConfiguration +) + +type ( + CheckCountFn func(h HandlerID, count uint64) codes.Code + CheckFinalCountsFn func(defaultHandlerCount, processTimeCount, processOwnershipCount, processCredentialsCount, + processACLsCount, processCloudConfigurationCount uint64) (bool, error) +) + +type RequestHandlerWithCounter struct { + RequestHandlerWithDps + // request callbacks + defaultHandlerCounter atomic.Uint64 + processOwnershipCounter atomic.Uint64 + processTimeCounter atomic.Uint64 + processACLsCounter atomic.Uint64 + processCloudConfigurationCounter atomic.Uint64 + processCredentialsCounter atomic.Uint64 + checkCount CheckCountFn // get return code for given handler based on the number of previous failures + checkFinalCounts CheckFinalCountsFn // verify final handler failure counter values + r service.RequestHandle +} + +func NewRequestHandlerWithCounter(t *testing.T, dpsCfg service.Config, checkCount CheckCountFn, checkFinalCounts CheckFinalCountsFn) *RequestHandlerWithCounter { + return &RequestHandlerWithCounter{ + RequestHandlerWithDps: MakeRequestHandlerWithDps(t, dpsCfg), + checkCount: checkCount, + checkFinalCounts: checkFinalCounts, + } +} + +// we know the counter is invalid when the value is higher +func NewRequestHandlerWithExpectedCounters(t *testing.T, failLimit uint64, failCode codes.Code, expectedTimeCount, expectedOwnershipCount, + expectedCloudConfigurationCount, expectedCredentialsCount, expectedACLsCount uint64, +) *RequestHandlerWithCounter { + dpsCfg := MakeConfig(t) + return NewRequestHandlerWithCounter(t, dpsCfg, func(h HandlerID, count uint64) codes.Code { + switch h { + case HandlerIDTime, + HandlerIDOwnership, + HandlerIDCloudConfiguration, + HandlerIDCredentials, + HandlerIDACLs: + if count < failLimit { + return failCode + } + case HandlerIDDefault: + } + return 0 + }, func(defaultHandlerCount, processTimeCount, processOwnershipCount, processCloudConfigurationCount, processCredentialsCount, processACLsCount uint64) (bool, error) { + if defaultHandlerCount > 0 || + (processTimeCount > expectedTimeCount) || + (processOwnershipCount > expectedOwnershipCount) || + (processCloudConfigurationCount > expectedCloudConfigurationCount) || + (processCredentialsCount > expectedCredentialsCount) || + (processACLsCount > expectedACLsCount) { + return false, + fmt.Errorf("invalid counters: defaultHandlerCounter=%v expectedTimeCount=%v(exp %v) processOwnershipCounter=%v(exp %v) processCloudConfigurationCounter=%v(exp %v) processCredentialsCounter=%v(exp %v) processACLsCounter=%v(exp %v) ", + defaultHandlerCount, + processTimeCount, expectedTimeCount, + processOwnershipCount, expectedOwnershipCount, + processCloudConfigurationCount, expectedCloudConfigurationCount, + processCredentialsCount, expectedCredentialsCount, + processACLsCount, expectedACLsCount, + ) + } + return defaultHandlerCount == 0 && + (processTimeCount == expectedTimeCount) && + (processOwnershipCount == expectedOwnershipCount) && + (processCloudConfigurationCount == expectedCloudConfigurationCount) && + (processCredentialsCount == expectedCredentialsCount) && + (processACLsCount == expectedACLsCount), nil + }) +} + +func (h *RequestHandlerWithCounter) DefaultHandler(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if !h.IsStarted() { + return nil, status.Errorf(service.NewMessageWithCode(codes.ServiceUnavailable), "DefaultHandler: DPS not started") + } + + c := h.defaultHandlerCounter.Load() + if h.checkCount != nil { + if code := h.checkCount(HandlerIDDefault, c); code != 0 { + h.defaultHandlerCounter.Inc() + return nil, status.Errorf(service.NewMessageWithCode(code), "DefaultHandler: force retry") + } + } + msg, err := h.r.DefaultHandler(ctx, req, session, linkedHubs, group) + if err != nil { + h.Logf("DefaultHandler: %s", err.Error()) + // internal error -> return ServiceUnavailable to force a single step retry + return nil, status.Errorf(service.NewMessageWithCode(codes.ServiceUnavailable), "DefaultHandler: %s", err.Error()) + } + h.defaultHandlerCounter.Inc() + h.Logf("DefaultHandler: %v", h.defaultHandlerCounter.Load()) + return msg, nil +} + +func (h *RequestHandlerWithCounter) ProcessPlgdTime(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if !h.IsStarted() { + return nil, status.Errorf(service.NewMessageWithCode(codes.ServiceUnavailable), "ProcessPlgdTime DPS not started") + } + + c := h.processTimeCounter.Load() + if h.checkCount != nil { + if code := h.checkCount(HandlerIDTime, c); code != 0 { + h.processTimeCounter.Inc() + h.Logf("ProcessPlgdTime: %v", h.processTimeCounter.Load()) + return nil, status.Errorf(service.NewMessageWithCode(code), "ProcessPlgdTime: force retry") + } + } + msg, err := h.r.ProcessPlgdTime(ctx, req, session, linkedHubs, group) + if err != nil { + h.Logf("ProcessPlgdTime: %s", err.Error()) + // internal error -> return ServiceUnavailable to force a single step retry + return nil, status.Errorf(service.NewMessageWithCode(codes.ServiceUnavailable), "ProcessPlgdTime: %s", err.Error()) + } + h.processTimeCounter.Inc() + h.Logf("ProcessPlgdTime: %v", h.processTimeCounter.Load()) + return msg, nil +} + +func (h *RequestHandlerWithCounter) ProcessOwnership(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if !h.IsStarted() { + return nil, status.Errorf(service.NewMessageWithCode(codes.ServiceUnavailable), "ProcessOwnership: DPS not started") + } + + c := h.processOwnershipCounter.Load() + if h.checkCount != nil { + if code := h.checkCount(HandlerIDOwnership, c); code != 0 { + h.processOwnershipCounter.Inc() + h.Logf("ProcessOwnership: %v", h.processOwnershipCounter.Load()) + return nil, status.Errorf(service.NewMessageWithCode(code), "ProcessOwnership: force retry") + } + } + msg, err := h.r.ProcessOwnership(ctx, req, session, linkedHubs, group) + if err != nil { + h.Logf("ProcessOwnership: %s", err.Error()) + // internal error -> return ServiceUnavailable to force a single step retry + return nil, status.Errorf(service.NewMessageWithCode(codes.ServiceUnavailable), "ProcessOwnership: %s", err.Error()) + } + h.processOwnershipCounter.Inc() + h.Logf("ProcessOwnership: %v", h.processOwnershipCounter.Load()) + return msg, nil +} + +func (h *RequestHandlerWithCounter) ProcessCredentials(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if !h.IsStarted() { + return nil, status.Errorf(service.NewMessageWithCode(codes.ServiceUnavailable), "ProcessCredentials: DPS not started") + } + + c := h.processCredentialsCounter.Load() + if h.checkCount != nil { + if code := h.checkCount(HandlerIDCredentials, c); code != 0 { + h.processCredentialsCounter.Inc() + h.Logf("ProcessCredentials: %v", h.processCredentialsCounter.Load()) + return nil, status.Errorf(service.NewMessageWithCode(code), "ProcessCredentials: force retry") + } + } + msg, err := h.r.ProcessCredentials(ctx, req, session, linkedHubs, group) + if err != nil { + h.Logf("ProcessCredentials: %s", err.Error()) + // internal error -> return ServiceUnavailable to force a single step retry + return nil, status.Errorf(service.NewMessageWithCode(codes.ServiceUnavailable), "ProcessCredentials: %s", err.Error()) + } + h.processCredentialsCounter.Inc() + h.Logf("ProcessCredentials: %v", h.processCredentialsCounter.Load()) + return msg, nil +} + +func (h *RequestHandlerWithCounter) ProcessACLs(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if !h.IsStarted() { + return nil, status.Errorf(service.NewMessageWithCode(codes.ServiceUnavailable), "ProcessACLs: DPS not started") + } + + c := h.processACLsCounter.Load() + if h.checkCount != nil { + if code := h.checkCount(HandlerIDACLs, c); code != 0 { + h.processACLsCounter.Inc() + h.Logf("ProcessACLs: %v", h.processACLsCounter.Load()) + return nil, status.Errorf(service.NewMessageWithCode(code), "ProcessACLs: force retry") + } + } + msg, err := h.r.ProcessACLs(ctx, req, session, linkedHubs, group) + if err != nil { + h.Logf("ProcessACLs: %s", err.Error()) + // internal error -> return ServiceUnavailable to force a single step retry + return nil, status.Errorf(service.NewMessageWithCode(codes.ServiceUnavailable), "ProcessACLs: %s", err.Error()) + } + h.processACLsCounter.Inc() + h.Logf("ProcessACLs: %v", h.processACLsCounter.Load()) + return msg, nil +} + +func (h *RequestHandlerWithCounter) ProcessCloudConfiguration(ctx context.Context, req *mux.Message, session *service.Session, linkedHubs []*service.LinkedHub, group *service.EnrollmentGroup) (*pool.Message, error) { + if !h.IsStarted() { + return nil, status.Errorf(service.NewMessageWithCode(codes.ServiceUnavailable), "ProcessCloudConfiguration: DPS not started") + } + + c := h.processCloudConfigurationCounter.Load() + if h.checkCount != nil { + if code := h.checkCount(HandlerIDCloudConfiguration, c); code != 0 { + h.processCloudConfigurationCounter.Inc() + h.Logf("ProcessCloudConfiguration: %v", h.processCloudConfigurationCounter.Load()) + return nil, status.Errorf(service.NewMessageWithCode(code), "ProcessCloudConfiguration: force retry") + } + } + msg, err := h.r.ProcessCloudConfiguration(ctx, req, session, linkedHubs, group) + if err != nil { + h.Logf("ProcessCloudConfiguration: %w", err) + // internal error -> return ServiceUnavailable to force a single step retry + return nil, status.Errorf(service.NewMessageWithCode(codes.ServiceUnavailable), "ProcessCloudConfiguration: %s", err.Error()) + } + h.processCloudConfigurationCounter.Inc() + h.Logf("ProcessCloudConfiguration: %v", h.processCloudConfigurationCounter.Load()) + return msg, nil +} + +func (h *RequestHandlerWithCounter) encode() string { + return "defaultHandlerCounter=" + strconv.FormatUint(h.defaultHandlerCounter.Load(), 10) + " " + + "processTimeCounter=" + strconv.FormatUint(h.processTimeCounter.Load(), 10) + " " + + "processOwnershipCounter=" + strconv.FormatUint(h.processOwnershipCounter.Load(), 10) + " " + + "processCloudConfigurationCounter=" + strconv.FormatUint(h.processCloudConfigurationCounter.Load(), 10) + " " + + "processCredentialsCounter=" + strconv.FormatUint(h.processCredentialsCounter.Load(), 10) + " " + + "processACLsCounter=" + strconv.FormatUint(h.processACLsCounter.Load(), 10) +} + +func (h *RequestHandlerWithCounter) Verify(ctx context.Context) error { + logCounter := 0 + for { + select { + case <-ctx.Done(): + return fmt.Errorf("unexpected counters %s", h.encode()) + case <-time.After(time.Second): + logCounter++ + if logCounter%3 == 0 { + h.Logf(h.encode()) + } + } + done, err := h.checkFinalCounts(h.defaultHandlerCounter.Load(), + h.processTimeCounter.Load(), + h.processOwnershipCounter.Load(), + h.processCloudConfigurationCounter.Load(), + h.processCredentialsCounter.Load(), + h.processACLsCounter.Load()) + if err != nil { + return fmt.Errorf("counter verification failed: %w", err) + } + if done { + return nil + } + } +} diff --git a/device-provisioning-service/test/test.go b/device-provisioning-service/test/test.go new file mode 100644 index 000000000..ba741fd6a --- /dev/null +++ b/device-provisioning-service/test/test.go @@ -0,0 +1,457 @@ +package test + +import ( + "context" + "os" + "sync" + "testing" + "time" + + deviceClient "github.com/plgd-dev/device/v2/client" + "github.com/plgd-dev/device/v2/schema" + "github.com/plgd-dev/device/v2/schema/collection" + "github.com/plgd-dev/device/v2/schema/configuration" + "github.com/plgd-dev/device/v2/schema/device" + "github.com/plgd-dev/device/v2/schema/interfaces" + "github.com/plgd-dev/device/v2/schema/platform" + "github.com/plgd-dev/device/v2/test/resource/types" + "github.com/plgd-dev/hub/v2/device-provisioning-service/pb" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service" + "github.com/plgd-dev/hub/v2/device-provisioning-service/service/http" + "github.com/plgd-dev/hub/v2/device-provisioning-service/store" + storeMongo "github.com/plgd-dev/hub/v2/device-provisioning-service/store/mongodb" + "github.com/plgd-dev/hub/v2/identity-store/events" + "github.com/plgd-dev/hub/v2/pkg/config/property/urischeme" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + "github.com/plgd-dev/hub/v2/pkg/mongodb" + pkgCoapService "github.com/plgd-dev/hub/v2/pkg/net/coap/service" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + cmClient "github.com/plgd-dev/hub/v2/pkg/security/certManager/client" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/plgd-dev/hub/v2/test/sdk" + "github.com/plgd-dev/kit/v2/security" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" +) + +const ( + DPSHost = "localhost:40030" + DPSHTTPHost = "localhost:40031" + DPSEnrollmentGroupID = "6aa1aa8e-2b91-48ee-bfbc-a4e22d8e20d8" + DPSOwner = "1" +) + +var ( + TestDockerContainerName string + TestDeviceName string + TestDockerObtContainerName string + TestDeviceObtName string + TestDeviceObtSupportsTestProperties bool + + TestDevsimResources []schema.ResourceLink +) + +const ( + TestResourceSwitchesHref = "/switches" + ResourcePlgdDpsHref = "/plgd/dps" + ResourcePlgdDpsType = "x.plgd.dps.conf" +) + +type ResourcePlgdDpsTestCloudStatusObserver struct { + MaxCount int64 `json:"maxCount,omitempty"` +} + +type ResourcePlgdDpsTestIotivity struct { + Retry []int64 `json:"retry,omitempty"` +} + +type ResourcePlgdDpsTest struct { + CloudStatusObserver ResourcePlgdDpsTestCloudStatusObserver `json:"cloudStatusObserver,omitempty"` + Iotivity ResourcePlgdDpsTestIotivity `json:"iotivity,omitempty"` +} + +type DpsEndpoint struct { + URI string `json:"uri,omitempty"` + Name string `json:"name,omitempty"` +} + +type ResourcePlgdDps struct { + Endpoint *string `json:"endpoint,omitempty"` + EndpointName *string `json:"endpointName,omitempty"` + Endpoints []DpsEndpoint `json:"endpoints,omitempty"` + LastError uint64 `json:"lastErrorCode,omitempty"` + ProvisionStatus string `json:"provisionStatus,omitempty"` + ForceReprovision bool `json:"forceReprovision,omitempty"` + Interfaces []string `json:"if,omitempty"` + ResourceTypes []string `json:"rt,omitempty"` + TestProperties ResourcePlgdDpsTest `json:"test,omitempty"` +} + +func CleanUpDpsResource(dps *ResourcePlgdDps) { + dps.TestProperties = ResourcePlgdDpsTest{} +} + +func LightResourceInstanceHref(id string) string { + return "/light/" + id +} + +func checkDpsResourceForTestProperties() bool { + deviceID := hubTest.MustFindDeviceByName(TestDeviceObtName) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + devClient, err := sdk.NewClient(sdk.WithID(events.OwnerToUUID(DPSOwner))) + if err != nil { + return false + } + defer func() { + _ = devClient.Close(ctx) + }() + deviceID, err = devClient.OwnDevice(ctx, deviceID, deviceClient.WithOTM(deviceClient.OTMType_JustWorks)) + if err != nil { + return false + } + defer func() { + _ = devClient.DisownDevice(ctx, deviceID) + }() + var resp map[string]interface{} + err = devClient.GetResource(ctx, deviceID, ResourcePlgdDpsHref, &resp) + if err != nil { + return false + } + + _, ok := resp["test"] + return ok +} + +func init() { + TestDockerContainerName = "dps-devsim" + TestDeviceName = TestDockerContainerName + "-" + hubTest.MustGetHostname() + TestDockerObtContainerName = "dps-devsim-obt" + TestDeviceObtName = TestDockerObtContainerName + "-" + hubTest.MustGetHostname() + TestDeviceObtSupportsTestProperties = checkDpsResourceForTestProperties() + + TestDevsimResources = []schema.ResourceLink{ + { + Href: platform.ResourceURI, + ResourceTypes: []string{platform.ResourceType}, + Interfaces: []string{interfaces.OC_IF_R, interfaces.OC_IF_BASELINE}, + Policy: &schema.Policy{ + BitMask: 3, + }, + }, + + { + Href: device.ResourceURI, + ResourceTypes: []string{types.DEVICE_CLOUD, device.ResourceType}, + Interfaces: []string{interfaces.OC_IF_R, interfaces.OC_IF_BASELINE}, + Policy: &schema.Policy{ + BitMask: 3, + }, + }, + + { + Href: configuration.ResourceURI, + ResourceTypes: []string{configuration.ResourceType}, + Interfaces: []string{interfaces.OC_IF_RW, interfaces.OC_IF_BASELINE}, + Policy: &schema.Policy{ + BitMask: 3, + }, + }, + + { + Href: LightResourceInstanceHref("1"), + ResourceTypes: []string{types.CORE_LIGHT}, + Interfaces: []string{interfaces.OC_IF_RW, interfaces.OC_IF_BASELINE}, + Policy: &schema.Policy{ + BitMask: 3, + }, + }, + + { + Href: TestResourceSwitchesHref, + ResourceTypes: []string{collection.ResourceType}, + Interfaces: []string{interfaces.OC_IF_LL, interfaces.OC_IF_CREATE, interfaces.OC_IF_B, interfaces.OC_IF_BASELINE}, + Policy: &schema.Policy{ + BitMask: 3, + }, + }, + { + Href: ResourcePlgdDpsHref, + ResourceTypes: []string{ResourcePlgdDpsType}, + Interfaces: []string{interfaces.OC_IF_R, interfaces.OC_IF_RW, interfaces.OC_IF_BASELINE}, + Policy: &schema.Policy{ + BitMask: 3, + }, + }, + } +} + +func MakeConfig(t require.TestingT) service.Config { + var cfg service.Config + cfg.Log.DumpBody = true + cfg.Log = log.MakeDefaultConfig() + cfg.TaskQueue.GoPoolSize = 1600 + cfg.TaskQueue.Size = 2 * 1024 * 1024 + cfg.APIs.COAP.Addr = DPSHost + cfg.APIs.COAP.MaxMessageSize = 256 * 1024 + cfg.APIs.COAP.MessagePoolSize = 1000 + cfg.APIs.COAP.Protocols = []pkgCoapService.Protocol{pkgCoapService.TCP, pkgCoapService.UDP} + cfg.APIs.COAP.InactivityMonitor = &pkgCoapService.InactivityMonitor{ + Timeout: time.Second * 20, + } + cfg.APIs.COAP.BlockwiseTransfer.Enabled = true + cfg.APIs.COAP.BlockwiseTransfer.SZX = "1024" + cfg.APIs.HTTP = MakeHTTPConfig() + tlsServerCfg := config.MakeTLSServerConfig() + cfg.APIs.COAP.TLS.Embedded.CertFile = tlsServerCfg.CertFile + cfg.APIs.COAP.TLS.Embedded.KeyFile = tlsServerCfg.KeyFile + cfg.Clients.Storage = MakeStorageConfig() + cfg.Clients.OpenTelemetryCollector = pkgHttp.OpenTelemetryCollectorConfig{ + Config: config.MakeOpenTelemetryCollectorClient(), + } + + cfg.EnrollmentGroups = append(cfg.EnrollmentGroups, MakeEnrollmentGroup()) + err := cfg.Validate() + require.NoError(t, err) + + return cfg +} + +func MakeEnrollmentGroup() service.EnrollmentGroupConfig { + var cfg service.EnrollmentGroupConfig + cfg.ID = DPSEnrollmentGroupID + cfg.Owner = DPSOwner + cfg.AttestationMechanism.X509.CertificateChain = urischeme.URIScheme(os.Getenv("TEST_DPS_INTERMEDIATE_CA_CERT")) + cfg.Hubs = []service.HubConfig{MakeHubConfig(config.HubID(), config.COAP_GW_HOST)} + return cfg +} + +func MakeAuthorizationConfig() service.AuthorizationConfig { + var cfg service.AuthorizationConfig + authCfg := config.MakeAuthorizationConfig() + + cfg.OwnerClaim = "sub" + cfg.Provider.Name = config.DEVICE_PROVIDER + cfg.Provider.Config.Authority = authCfg.Endpoints[0].Authority + cfg.Provider.Config.Audience = authCfg.Audience + cfg.Provider.Config.HTTP = authCfg.Endpoints[0].HTTP + cfg.Provider.Config.ClientID = config.OAUTH_MANAGER_CLIENT_ID + cfg.Provider.Config.ClientSecretFile = config.CA_POOL + return cfg +} + +func MakeHubConfig(hubID string, gateways ...string) service.HubConfig { + var cfg service.HubConfig + cfg.Authorization = MakeAuthorizationConfig() + cfg.CertificateAuthority.Connection = config.MakeGrpcClientConfig(config.CERTIFICATE_AUTHORITY_HOST) + cfg.Gateways = gateways + cfg.HubID = hubID + return cfg +} + +func SetUp(t *testing.T) (tearDown func()) { + return New(t, MakeConfig(t)) +} + +func New(t *testing.T, cfg service.Config, opts ...service.Option) func() { + return NewWithContext(context.Background(), t, cfg, opts...) +} + +func checkForClosedSockets(t require.TestingT, cfg service.Config) { + protocolClosed := make([]bool, len(cfg.APIs.COAP.Config.Protocols)) + // wait for all sockets to be closed - max 3 minutes = 900*200 + for j := 0; j < 900; j++ { + allClosed := true + for i, protocol := range cfg.APIs.COAP.Config.Protocols { + if protocolClosed[i] { + continue + } + protocolClosed[i] = hubTest.IsListenSocketClosed(t, string(protocol), cfg.APIs.COAP.Config.Addr) + if protocolClosed[i] { + continue + } + allClosed = false + break + } + if allClosed { + break + } + time.Sleep(time.Millisecond * 200) + } +} + +// New creates test dps-gateway. +func NewWithContext(ctx context.Context, t *testing.T, cfg service.Config, opts ...service.Option) func() { + logger := log.NewLogger(cfg.Log) + + fileWatcher, err := fsnotify.NewWatcher(logger) + require.NoError(t, err) + + s, err := service.New(ctx, cfg, fileWatcher, logger, opts...) + require.NoError(t, err) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + _ = s.Serve() + }() + + return func() { + _ = s.Close() + wg.Wait() + err = fileWatcher.Close() + require.NoError(t, err) + + checkForClosedSockets(t, cfg) + } +} + +func MakeStorageConfig() service.StorageConfig { + return service.StorageConfig{ + CacheExpiration: time.Second, + MongoDB: storeMongo.Config{ + Mongo: mongodb.Config{ + MaxPoolSize: 16, + MaxConnIdleTime: time.Minute * 4, + URI: config.MONGODB_URI, + Database: "deviceProvisioning", + TLS: config.MakeTLSClientConfig(), + }, + BulkWrite: storeMongo.BulkWriteConfig{ + Timeout: time.Minute, + ThrottleTime: time.Millisecond * 500, + DocumentLimit: 1000, + }, + }, + } +} + +func MakeHTTPConfig() service.HTTPConfig { + return service.HTTPConfig{ + Enabled: true, + Config: http.Config{ + Connection: config.MakeListenerConfig(DPSHTTPHost), + Authorization: http.AuthorizationConfig{ + OwnerClaim: "sub", + Config: config.MakeValidatorConfig(), + }, + Server: config.MakeHttpServerConfig(), + }, + } +} + +func NewMongoStore(t require.TestingT) (*storeMongo.Store, func()) { + cfg := MakeConfig(t) + logger := log.NewLogger(cfg.Log) + + fileWatcher, err := fsnotify.NewWatcher(logger) + require.NoError(t, err) + + certManager, err := cmClient.New(cfg.Clients.Storage.MongoDB.Mongo.TLS, fileWatcher, logger) + require.NoError(t, err) + + ctx := context.Background() + store, err := storeMongo.NewStore(ctx, cfg.Clients.Storage.MongoDB, certManager.GetTLSConfig(), logger, noop.NewTracerProvider()) + require.NoError(t, err) + + cleanUp := func() { + err := store.Clear(ctx) + require.NoError(t, err) + _ = store.Close(ctx) + certManager.Close() + + err = fileWatcher.Close() + require.NoError(t, err) + } + + return store, cleanUp +} + +func NewHTTPService(ctx context.Context, t *testing.T, store *storeMongo.Store) (*http.Service, func()) { + cfg := MakeConfig(t) + cfg.APIs.HTTP.Connection.TLS.ClientCertificateRequired = false + logger := log.NewLogger(cfg.Log) + + fileWatcher, err := fsnotify.NewWatcher(logger) + require.NoError(t, err) + + s, err := http.New(ctx, "dps-http", cfg.APIs.HTTP.Config, fileWatcher, logger, noop.NewTracerProvider(), store) + require.NoError(t, err) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + _ = s.Serve() + }() + + cleanUp := func() { + err = s.Close() + require.NoError(t, err) + wg.Wait() + err = fileWatcher.Close() + require.NoError(t, err) + } + + return s, cleanUp +} + +func ClearDB(t require.TestingT) { + _, shutdown := NewMongoStore(t) + defer shutdown() +} + +func NewHub(id, owner string) *pb.Hub { + return &store.Hub{ + Owner: owner, + Id: id, + HubId: id, + Gateways: []string{"coaps+tcp://1234"}, + Name: "name", + CertificateAuthority: &pb.GrpcClientConfig{ + Grpc: &pb.GrpcConnectionConfig{ + Address: "1234", + Tls: &pb.TlsConfig{ + UseSystemCaPool: true, + }, + }, + }, + Authorization: &pb.AuthorizationConfig{ + OwnerClaim: "sub", + Provider: &pb.AuthorizationProviderConfig{ + Name: "plgd", + Authority: "authority", + ClientId: "clientId", + ClientSecret: os.Getenv("TEST_ROOT_CA_CERT"), + Http: &pb.HttpConfig{ + Tls: &pb.TlsConfig{ + UseSystemCaPool: true, + }, + }, + }, + }, + } +} + +func NewEnrollmentGroup(t *testing.T, id, owner string) *pb.EnrollmentGroup { + certs, err := security.LoadX509(os.Getenv("TEST_DPS_INTERMEDIATE_CA_CERT")) + require.NoError(t, err) + require.NotEmpty(t, certs) + + return &pb.EnrollmentGroup{ + Id: id, + Owner: owner, + AttestationMechanism: &pb.AttestationMechanism{ + X509: &pb.X509Configuration{ + CertificateChain: os.Getenv("TEST_DPS_INTERMEDIATE_CA_CERT"), + LeadCertificateName: certs[0].Subject.CommonName, + }, + }, + HubIds: []string{id}, + PreSharedKey: "data:,1234567890123456", + Name: "name", + } +} diff --git a/device-provisioning-service/test/util.go b/device-provisioning-service/test/util.go new file mode 100644 index 000000000..12bc409f2 --- /dev/null +++ b/device-provisioning-service/test/util.go @@ -0,0 +1,30 @@ +package test + +import ( + "os/exec" + + "github.com/plgd-dev/kit/v2/codec/json" +) + +func RestartDockerContainer(dockerName string) error { + cmd := exec.Command("docker", "restart", dockerName) + return cmd.Run() +} + +func SendSignalToDocker(dockerName, signal string) error { + cmd := exec.Command("docker", "kill", "-s", signal, dockerName) + return cmd.Run() +} + +func SendSignalToProcess(pid, signal string) error { + cmd := exec.Command("kill", "-s", signal, pid) + return cmd.Run() +} + +func ToJSON(v interface{}) ([]byte, error) { + data, err := json.Encode(v) + if err != nil { + return nil, err + } + return data, nil +} diff --git a/device-provisioning-service/uri/uri.go b/device-provisioning-service/uri/uri.go new file mode 100644 index 000000000..76e2e8d50 --- /dev/null +++ b/device-provisioning-service/uri/uri.go @@ -0,0 +1,16 @@ +package uri + +// COAP Service URIs. +const ( + API = "/api" + APIV1 = API + "/v1" + + CoAPsTCPSchemePrefix = "coaps+tcp://" + + Provisioning = APIV1 + "/provisioning" + Resources = Provisioning + "/resources" + ACLs = Provisioning + "/acls" + Credentials = Provisioning + "/credentials" + CloudConfiguration = Provisioning + "/cloud-configuration" + Ownership = Provisioning + "/ownership" +) diff --git a/device-provisioning-service/workflow.puml b/device-provisioning-service/workflow.puml new file mode 100644 index 000000000..6f068a95e --- /dev/null +++ b/device-provisioning-service/workflow.puml @@ -0,0 +1,168 @@ +@startuml workflow +autonumber + +actor Operator as op +participant "Onboarding Tool" as obt +box "Device" #LightBlue +participant "Device Application" as device +participant "DPS Library" as dpc +end box +box "plgd hub" #DeepSkyBlue +participant "Device Provisioning Service" as dps +participant "Certificate Authority Service" as ca +end box +participant "OAuth2.0 Server" as os + +note over of device + Unique manufacturer certificate + set during the production +end note +== Configuration == +op -> dps: Configure Enrollment Group +== DPS Connection == +alt DPS Address set by the device - Call Home + device -> dpc: Set DPS address +else DPS address configured by the tool running discovery - Multicast +obt --> dpc: Discover x.com.plgd.dps.conf +dpc -> obt: Here I am +note over dpc + Devices with the x.com.plgd.dps.conf resource will reply. +end note +obt -> dpc: Own device +return +note over dpc + The onboarding tool becomes the owner of the device + or already owns it. +end note +obt -> dpc: Set DPS address +return +note over dpc + When the device is disowned or factory reset, the DPS address is set to empty. +end note +end + +note over dpc, dps + The device is authenticated by the manufacturer certificate. + Expired certificate can be used if enabled. +end note + +dpc -> dps: Connect and authenticate using Manufacturer Certificate +dps -> dps: Validate certificate +note right + Validation against Mfg CA + registered in the Enrollment Group and + against the revocation list. +end note +alt Valid + dps -> dpc: Connection established +else Invalid + dps ->x dpc: Close connection +end + +== Check available content == +note over dpc, dps + Content available for the DPS library which is derived from the Enrollment Group configuration is used to drive the workflow of the provisioning. +end note + +dpc -> dps: Get available resources +dps -> dpc: Available provisioning flow resources +note left of dps +{ + "if": [oic.if.r, oic.if.b] + "links": [ + { + "href": "/api/v1/provisioning/acls" + }, + { + "href": "/api/v1/provisioning/cloud-configuration" + }, + { + "href": "/api/v1/provisioning/credentials" + }, + { + "href": "/api/v1/provisioning/customResource1" + } + { + "href": "/api/v1/provisioning/ownership" + }, + { + "href": "/api/v1/provisioning/ownership" + }, + { + "href": "/x.plgd.dev/time" + }, + ... + ] +} +end note + +== Device time synchronization == + +dpc -> dps: Get current time\n(GET x.plgd.dev/time) +dps -> dpc: [[https://github.com/iotivity/iotivity-lite/blob/master/api/plgd/x.plgd.dev.time.yaml#L65 Current time]] +dpc -> dpc: Apply time if out of sync + +== Device Ownership == + +dpc -> dps: Get device ownerhip\n(GET api/v1/provisioning/ownership) +dps -> dps: Set owner from the Enrollment Group for the device +dps -> dpc: [[https://github.com/openconnectivityfoundation/security/blob/master/swagger2.0/oic.sec.doxm.swagger.json#L160 Device Owner Transfer Method]] +dpc -> dpc: Apply Ownership + +== Device Identity Credentials == + +dpc -> dps : Sign Identity CSR and retrieve device credentials\n(POST api/v1/provisioning/credentials) +alt JWT token not cached + dps -> os:Get token using client credential flow\nwith the owner from the dps configuration + os -> dps: JWT Token +end +dps -> ca: Sign device's Identity CSR +ca -> dps: Identity Certificate and plgd hub CA +dps -> dpc: [[https://github.com/openconnectivityfoundation/security/blob/master/swagger2.0/oic.sec.cred.swagger.json#L439 Identity Certificate and plgd hub CA]] +dpc -> dpc: Apply Identity Credentials + +== Get Device ACLs == + +dpc -> dps: Get device ACLs\n(GET api/v1/provisioning/acls) +dps -> dps: Retrieve custom entries, generate required \nfor plgd hub instance and owner from the Enrollment Group +dps -> dpc: [[https://github.com/openconnectivityfoundation/security/blob/master/swagger2.0/oic.sec.acl2.swagger.json#L362 Device ACLs]] +dpc -> dpc: Apply ACLs + + +== Resource pre-configuration == + +dpc -> dps: Get pre-configuration specified in the Enrollment Group\n(GET api/v1/provisioning/res?if=oic.if.b) +dps -> dpc: Resource content +note left of dps +{ + "if": [oic.if.r, oic.if.b] + "links": [ + { + "href": "/api/v1/provisioning/custom/resource/1" + "etag": "0", + "rep": {...} + }, + { + "href": "/api/v1/provisioning/custom/resource/2" + "etag": "1", + "rep": {...} + }, + ... + ] +} +end note + +== Get plgd hub configuration == +dpc -> dps : Get plgd hub connection data\n(GET api/v1/provisioning/cloud-configuration) +dps -> os: Get token using client credential flow\nwith owner from the dps configuration +dps -> dps: Get connection data of plgd hub\nregistered in the Enrollment Group +dps -> dpc: [[https://github.com/openconnectivityfoundation/cloud-services/blob/master/swagger2.0/oic.r.coapcloudconf.swagger.json#L215 Token & connection data]] +dpc -> dpc: Provisioning device for plgd hub connection +dpc ->x dps: Close connection +dpc -> dpc: Start Cloud Manager + +== plgd hub connection == +note across: Flow continues in a standard way, as documented [[https://docs.plgd.dev/docs/architecture/component-overview/#hub-registration here]] + + +@enduml diff --git a/pkg/strings/strings.go b/pkg/strings/strings.go new file mode 100644 index 000000000..6d6c206e5 --- /dev/null +++ b/pkg/strings/strings.go @@ -0,0 +1,14 @@ +package strings + +func UniqueStable(s []string) []string { + m := make(map[string]struct{}, len(s)) + ret := make([]string, 0, len(s)) + for _, v := range s { + if _, ok := m[v]; ok { + continue + } + m[v] = struct{}{} + ret = append(ret, v) + } + return ret +} diff --git a/pkg/strings/strings_test.go b/pkg/strings/strings_test.go new file mode 100644 index 000000000..89601733c --- /dev/null +++ b/pkg/strings/strings_test.go @@ -0,0 +1,44 @@ +package strings_test + +import ( + "testing" + + "github.com/plgd-dev/hub/v2/pkg/strings" + "github.com/stretchr/testify/require" +) + +func TestUniqueStable(t *testing.T) { + tests := []struct { + name string + s []string + want []string + }{ + { + name: "No duplicates", + s: []string{"a", "b", "c"}, + want: []string{"a", "b", "c"}, + }, + { + name: "With duplicates", + s: []string{"a", "b", "a", "c", "b"}, + want: []string{"a", "b", "c"}, + }, + { + name: "Empty slice", + s: []string{}, + want: []string{}, + }, + { + name: "All duplicates", + s: []string{"a", "a", "a"}, + want: []string{"a"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := strings.UniqueStable(tt.s) + require.Equal(t, tt.want, got) + }) + } +}