diff --git a/interfaces/installation/interface.codefly.yaml b/interfaces/installation/interface.codefly.yaml index 78ddd757..ea5a47c6 100644 --- a/interfaces/installation/interface.codefly.yaml +++ b/interfaces/installation/interface.codefly.yaml @@ -8,4 +8,4 @@ protobuf: package: host.installation.v1 services: - name: InstallationService - methods: [Install, GrantRole, RevokeRole, ListRoleGrants, List] + methods: [Install, Review, GrantRole, RevokeRole, ListRoleGrants, List] diff --git a/libraries/authority-client/contract/catalog.codefly.json b/libraries/authority-client/contract/catalog.codefly.json index 18643b7f..9ff1b5cf 100644 --- a/libraries/authority-client/contract/catalog.codefly.json +++ b/libraries/authority-client/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "api", "path": "contract.binpb", - "digest": "sha256:4d92544f5e186880be4d17079ad6dc056a6f9a84a0e1ebb298a2ff9b72863a46", + "digest": "sha256:f70c838a51dc2bfe1d95bfec4a1ab0f67d7bfd48ce60ccce58de30d342dce6c0", "services": [ { "name": "LocalService", diff --git a/libraries/authority-client/contract/contract.binpb b/libraries/authority-client/contract/contract.binpb index c8f0054e..fd1a4b66 100644 Binary files a/libraries/authority-client/contract/contract.binpb and b/libraries/authority-client/contract/contract.binpb differ diff --git a/libraries/authority-client/go/gen/host/installation/v1/installation.pb.go b/libraries/authority-client/go/gen/host/installation/v1/installation.pb.go index 26529359..602b3fef 100644 --- a/libraries/authority-client/go/gen/host/installation/v1/installation.pb.go +++ b/libraries/authority-client/go/gen/host/installation/v1/installation.pb.go @@ -82,7 +82,7 @@ func (x Installation_Status) Number() protoreflect.EnumNumber { // Deprecated: Use Installation_Status.Descriptor instead. func (Installation_Status) EnumDescriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{8, 0} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{10, 0} } // InstallRequest asks to install one target at one organisation. @@ -96,6 +96,8 @@ type InstallRequest struct { Role string `protobuf:"bytes,3,opt,name=role,proto3" json:"role,omitempty"` // The accountable person; an administrator of the organisation. OwnerPersonId string `protobuf:"bytes,4,opt,name=owner_person_id,json=ownerPersonId,proto3" json:"owner_person_id,omitempty"` + // Required: the exact offer reviewed by the administrator; unchanged on retry. + Approval *ConsentApproval `protobuf:"bytes,5,opt,name=approval,proto3" json:"approval,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -158,6 +160,146 @@ func (x *InstallRequest) GetOwnerPersonId() string { return "" } +func (x *InstallRequest) GetApproval() *ConsentApproval { + if x != nil { + return x.Approval + } + return nil +} + +// ReviewRequest names the offer an administrator intends to review. +type ReviewRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrganizationId string `protobuf:"bytes,1,opt,name=organization_id,json=organizationId,proto3" json:"organization_id,omitempty"` + Target string `protobuf:"bytes,2,opt,name=target,proto3" json:"target,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReviewRequest) Reset() { + *x = ReviewRequest{} + mi := &file_host_installation_v1_installation_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReviewRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReviewRequest) ProtoMessage() {} + +func (x *ReviewRequest) ProtoReflect() protoreflect.Message { + mi := &file_host_installation_v1_installation_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReviewRequest.ProtoReflect.Descriptor instead. +func (*ReviewRequest) Descriptor() ([]byte, []int) { + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{1} +} + +func (x *ReviewRequest) GetOrganizationId() string { + if x != nil { + return x.OrganizationId + } + return "" +} + +func (x *ReviewRequest) GetTarget() string { + if x != nil { + return x.Target + } + return "" +} + +// ConsentApproval binds consent to an immutable target, its reviewed delivery and +// the installation revision (zero if absent). Review leaves operation_id empty; +// the caller supplies a fresh UUID and preserves the entire request on retry. +type ConsentApproval struct { + state protoimpl.MessageState `protogen:"open.v1"` + TargetId string `protobuf:"bytes,1,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` + Generation uint64 `protobuf:"varint,2,opt,name=generation,proto3" json:"generation,omitempty"` + ExpectedRevision uint64 `protobuf:"varint,3,opt,name=expected_revision,json=expectedRevision,proto3" json:"expected_revision,omitempty"` + OperationId string `protobuf:"bytes,4,opt,name=operation_id,json=operationId,proto3" json:"operation_id,omitempty"` + // sha256 digest of the reviewed authority offer. + Digest string `protobuf:"bytes,5,opt,name=digest,proto3" json:"digest,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConsentApproval) Reset() { + *x = ConsentApproval{} + mi := &file_host_installation_v1_installation_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConsentApproval) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConsentApproval) ProtoMessage() {} + +func (x *ConsentApproval) ProtoReflect() protoreflect.Message { + mi := &file_host_installation_v1_installation_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConsentApproval.ProtoReflect.Descriptor instead. +func (*ConsentApproval) Descriptor() ([]byte, []int) { + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{2} +} + +func (x *ConsentApproval) GetTargetId() string { + if x != nil { + return x.TargetId + } + return "" +} + +func (x *ConsentApproval) GetGeneration() uint64 { + if x != nil { + return x.Generation + } + return 0 +} + +func (x *ConsentApproval) GetExpectedRevision() uint64 { + if x != nil { + return x.ExpectedRevision + } + return 0 +} + +func (x *ConsentApproval) GetOperationId() string { + if x != nil { + return x.OperationId + } + return "" +} + +func (x *ConsentApproval) GetDigest() string { + if x != nil { + return x.Digest + } + return "" +} + // GrantRoleRequest grants one role to exactly one person or team. type GrantRoleRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -180,7 +322,7 @@ type GrantRoleRequest struct { func (x *GrantRoleRequest) Reset() { *x = GrantRoleRequest{} - mi := &file_host_installation_v1_installation_proto_msgTypes[1] + mi := &file_host_installation_v1_installation_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -192,7 +334,7 @@ func (x *GrantRoleRequest) String() string { func (*GrantRoleRequest) ProtoMessage() {} func (x *GrantRoleRequest) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[1] + mi := &file_host_installation_v1_installation_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -205,7 +347,7 @@ func (x *GrantRoleRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GrantRoleRequest.ProtoReflect.Descriptor instead. func (*GrantRoleRequest) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{1} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{3} } func (x *GrantRoleRequest) GetOrganizationId() string { @@ -262,7 +404,7 @@ type RevokeRoleRequest struct { func (x *RevokeRoleRequest) Reset() { *x = RevokeRoleRequest{} - mi := &file_host_installation_v1_installation_proto_msgTypes[2] + mi := &file_host_installation_v1_installation_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -274,7 +416,7 @@ func (x *RevokeRoleRequest) String() string { func (*RevokeRoleRequest) ProtoMessage() {} func (x *RevokeRoleRequest) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[2] + mi := &file_host_installation_v1_installation_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -287,7 +429,7 @@ func (x *RevokeRoleRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeRoleRequest.ProtoReflect.Descriptor instead. func (*RevokeRoleRequest) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{2} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{4} } func (x *RevokeRoleRequest) GetOrganizationId() string { @@ -338,7 +480,7 @@ type ListRoleGrantsRequest struct { func (x *ListRoleGrantsRequest) Reset() { *x = ListRoleGrantsRequest{} - mi := &file_host_installation_v1_installation_proto_msgTypes[3] + mi := &file_host_installation_v1_installation_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -350,7 +492,7 @@ func (x *ListRoleGrantsRequest) String() string { func (*ListRoleGrantsRequest) ProtoMessage() {} func (x *ListRoleGrantsRequest) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[3] + mi := &file_host_installation_v1_installation_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -363,7 +505,7 @@ func (x *ListRoleGrantsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoleGrantsRequest.ProtoReflect.Descriptor instead. func (*ListRoleGrantsRequest) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{3} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{5} } func (x *ListRoleGrantsRequest) GetOrganizationId() string { @@ -391,7 +533,7 @@ type ListRoleGrantsResponse struct { func (x *ListRoleGrantsResponse) Reset() { *x = ListRoleGrantsResponse{} - mi := &file_host_installation_v1_installation_proto_msgTypes[4] + mi := &file_host_installation_v1_installation_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -403,7 +545,7 @@ func (x *ListRoleGrantsResponse) String() string { func (*ListRoleGrantsResponse) ProtoMessage() {} func (x *ListRoleGrantsResponse) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[4] + mi := &file_host_installation_v1_installation_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -416,7 +558,7 @@ func (x *ListRoleGrantsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoleGrantsResponse.ProtoReflect.Descriptor instead. func (*ListRoleGrantsResponse) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{4} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{6} } func (x *ListRoleGrantsResponse) GetGrants() []*RoleGrant { @@ -441,7 +583,7 @@ type RoleGrant struct { func (x *RoleGrant) Reset() { *x = RoleGrant{} - mi := &file_host_installation_v1_installation_proto_msgTypes[5] + mi := &file_host_installation_v1_installation_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -453,7 +595,7 @@ func (x *RoleGrant) String() string { func (*RoleGrant) ProtoMessage() {} func (x *RoleGrant) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[5] + mi := &file_host_installation_v1_installation_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -466,7 +608,7 @@ func (x *RoleGrant) ProtoReflect() protoreflect.Message { // Deprecated: Use RoleGrant.ProtoReflect.Descriptor instead. func (*RoleGrant) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{5} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{7} } func (x *RoleGrant) GetRole() string { @@ -501,7 +643,7 @@ type ListRequest struct { func (x *ListRequest) Reset() { *x = ListRequest{} - mi := &file_host_installation_v1_installation_proto_msgTypes[6] + mi := &file_host_installation_v1_installation_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -513,7 +655,7 @@ func (x *ListRequest) String() string { func (*ListRequest) ProtoMessage() {} func (x *ListRequest) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[6] + mi := &file_host_installation_v1_installation_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -526,7 +668,7 @@ func (x *ListRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRequest.ProtoReflect.Descriptor instead. func (*ListRequest) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{6} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{8} } func (x *ListRequest) GetOrganizationId() string { @@ -547,7 +689,7 @@ type ListResponse struct { func (x *ListResponse) Reset() { *x = ListResponse{} - mi := &file_host_installation_v1_installation_proto_msgTypes[7] + mi := &file_host_installation_v1_installation_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -559,7 +701,7 @@ func (x *ListResponse) String() string { func (*ListResponse) ProtoMessage() {} func (x *ListResponse) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[7] + mi := &file_host_installation_v1_installation_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -572,7 +714,7 @@ func (x *ListResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListResponse.ProtoReflect.Descriptor instead. func (*ListResponse) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{7} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{9} } func (x *ListResponse) GetInstallations() []*Installation { @@ -615,7 +757,7 @@ type Installation struct { func (x *Installation) Reset() { *x = Installation{} - mi := &file_host_installation_v1_installation_proto_msgTypes[8] + mi := &file_host_installation_v1_installation_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -627,7 +769,7 @@ func (x *Installation) String() string { func (*Installation) ProtoMessage() {} func (x *Installation) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[8] + mi := &file_host_installation_v1_installation_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -640,7 +782,7 @@ func (x *Installation) ProtoReflect() protoreflect.Message { // Deprecated: Use Installation.ProtoReflect.Descriptor instead. func (*Installation) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{8} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{10} } func (x *Installation) GetId() string { @@ -717,7 +859,7 @@ type InstallationNotice struct { func (x *InstallationNotice) Reset() { *x = InstallationNotice{} - mi := &file_host_installation_v1_installation_proto_msgTypes[9] + mi := &file_host_installation_v1_installation_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -729,7 +871,7 @@ func (x *InstallationNotice) String() string { func (*InstallationNotice) ProtoMessage() {} func (x *InstallationNotice) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[9] + mi := &file_host_installation_v1_installation_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -742,7 +884,7 @@ func (x *InstallationNotice) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallationNotice.ProtoReflect.Descriptor instead. func (*InstallationNotice) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{9} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{11} } func (x *InstallationNotice) GetInstallation() *Installation { @@ -761,7 +903,7 @@ type InstallationAck struct { func (x *InstallationAck) Reset() { *x = InstallationAck{} - mi := &file_host_installation_v1_installation_proto_msgTypes[10] + mi := &file_host_installation_v1_installation_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -773,7 +915,7 @@ func (x *InstallationAck) String() string { func (*InstallationAck) ProtoMessage() {} func (x *InstallationAck) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[10] + mi := &file_host_installation_v1_installation_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -786,19 +928,31 @@ func (x *InstallationAck) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallationAck.ProtoReflect.Descriptor instead. func (*InstallationAck) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{10} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{12} } var File_host_installation_v1_installation_proto protoreflect.FileDescriptor const file_host_installation_v1_installation_proto_rawDesc = "" + "\n" + - "'host/installation/v1/installation.proto\x12\x14host.installation.v1\"\x8d\x01\n" + + "'host/installation/v1/installation.proto\x12\x14host.installation.v1\"\xd0\x01\n" + "\x0eInstallRequest\x12'\n" + "\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\x12\x16\n" + "\x06target\x18\x02 \x01(\tR\x06target\x12\x12\n" + "\x04role\x18\x03 \x01(\tR\x04role\x12&\n" + - "\x0fowner_person_id\x18\x04 \x01(\tR\rownerPersonId\"\xae\x01\n" + + "\x0fowner_person_id\x18\x04 \x01(\tR\rownerPersonId\x12A\n" + + "\bapproval\x18\x05 \x01(\v2%.host.installation.v1.ConsentApprovalR\bapproval\"P\n" + + "\rReviewRequest\x12'\n" + + "\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\x12\x16\n" + + "\x06target\x18\x02 \x01(\tR\x06target\"\xb6\x01\n" + + "\x0fConsentApproval\x12\x1b\n" + + "\ttarget_id\x18\x01 \x01(\tR\btargetId\x12\x1e\n" + + "\n" + + "generation\x18\x02 \x01(\x04R\n" + + "generation\x12+\n" + + "\x11expected_revision\x18\x03 \x01(\x04R\x10expectedRevision\x12!\n" + + "\foperation_id\x18\x04 \x01(\tR\voperationId\x12\x16\n" + + "\x06digest\x18\x05 \x01(\tR\x06digest\"\xae\x01\n" + "\x10GrantRoleRequest\x12'\n" + "\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\x12'\n" + "\x0finstallation_id\x18\x02 \x01(\tR\x0einstallationId\x12\x12\n" + @@ -842,9 +996,10 @@ const file_host_installation_v1_installation_proto_rawDesc = "" + "\x0eSTATUS_REVOKED\x10\x03J\x04\b\a\x10\bJ\x04\b\b\x10\tR\bteam_idsR\x12agent_principal_id\"\\\n" + "\x12InstallationNotice\x12F\n" + "\finstallation\x18\x01 \x01(\v2\".host.installation.v1.InstallationR\finstallation\"\x11\n" + - "\x0fInstallationAck2\xda\x03\n" + + "\x0fInstallationAck2\xb0\x04\n" + "\x13InstallationService\x12S\n" + - "\aInstall\x12$.host.installation.v1.InstallRequest\x1a\".host.installation.v1.Installation\x12W\n" + + "\aInstall\x12$.host.installation.v1.InstallRequest\x1a\".host.installation.v1.Installation\x12T\n" + + "\x06Review\x12#.host.installation.v1.ReviewRequest\x1a%.host.installation.v1.ConsentApproval\x12W\n" + "\tGrantRole\x12&.host.installation.v1.GrantRoleRequest\x1a\".host.installation.v1.Installation\x12Y\n" + "\n" + "RevokeRole\x12'.host.installation.v1.RevokeRoleRequest\x1a\".host.installation.v1.Installation\x12k\n" + @@ -867,43 +1022,48 @@ func file_host_installation_v1_installation_proto_rawDescGZIP() []byte { } var file_host_installation_v1_installation_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_host_installation_v1_installation_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_host_installation_v1_installation_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_host_installation_v1_installation_proto_goTypes = []any{ (Installation_Status)(0), // 0: host.installation.v1.Installation.Status (*InstallRequest)(nil), // 1: host.installation.v1.InstallRequest - (*GrantRoleRequest)(nil), // 2: host.installation.v1.GrantRoleRequest - (*RevokeRoleRequest)(nil), // 3: host.installation.v1.RevokeRoleRequest - (*ListRoleGrantsRequest)(nil), // 4: host.installation.v1.ListRoleGrantsRequest - (*ListRoleGrantsResponse)(nil), // 5: host.installation.v1.ListRoleGrantsResponse - (*RoleGrant)(nil), // 6: host.installation.v1.RoleGrant - (*ListRequest)(nil), // 7: host.installation.v1.ListRequest - (*ListResponse)(nil), // 8: host.installation.v1.ListResponse - (*Installation)(nil), // 9: host.installation.v1.Installation - (*InstallationNotice)(nil), // 10: host.installation.v1.InstallationNotice - (*InstallationAck)(nil), // 11: host.installation.v1.InstallationAck + (*ReviewRequest)(nil), // 2: host.installation.v1.ReviewRequest + (*ConsentApproval)(nil), // 3: host.installation.v1.ConsentApproval + (*GrantRoleRequest)(nil), // 4: host.installation.v1.GrantRoleRequest + (*RevokeRoleRequest)(nil), // 5: host.installation.v1.RevokeRoleRequest + (*ListRoleGrantsRequest)(nil), // 6: host.installation.v1.ListRoleGrantsRequest + (*ListRoleGrantsResponse)(nil), // 7: host.installation.v1.ListRoleGrantsResponse + (*RoleGrant)(nil), // 8: host.installation.v1.RoleGrant + (*ListRequest)(nil), // 9: host.installation.v1.ListRequest + (*ListResponse)(nil), // 10: host.installation.v1.ListResponse + (*Installation)(nil), // 11: host.installation.v1.Installation + (*InstallationNotice)(nil), // 12: host.installation.v1.InstallationNotice + (*InstallationAck)(nil), // 13: host.installation.v1.InstallationAck } var file_host_installation_v1_installation_proto_depIdxs = []int32{ - 6, // 0: host.installation.v1.ListRoleGrantsResponse.grants:type_name -> host.installation.v1.RoleGrant - 9, // 1: host.installation.v1.ListResponse.installations:type_name -> host.installation.v1.Installation - 0, // 2: host.installation.v1.Installation.status:type_name -> host.installation.v1.Installation.Status - 9, // 3: host.installation.v1.InstallationNotice.installation:type_name -> host.installation.v1.Installation - 1, // 4: host.installation.v1.InstallationService.Install:input_type -> host.installation.v1.InstallRequest - 2, // 5: host.installation.v1.InstallationService.GrantRole:input_type -> host.installation.v1.GrantRoleRequest - 3, // 6: host.installation.v1.InstallationService.RevokeRole:input_type -> host.installation.v1.RevokeRoleRequest - 4, // 7: host.installation.v1.InstallationService.ListRoleGrants:input_type -> host.installation.v1.ListRoleGrantsRequest - 7, // 8: host.installation.v1.InstallationService.List:input_type -> host.installation.v1.ListRequest - 10, // 9: host.installation.v1.InstallableService.Install:input_type -> host.installation.v1.InstallationNotice - 9, // 10: host.installation.v1.InstallationService.Install:output_type -> host.installation.v1.Installation - 9, // 11: host.installation.v1.InstallationService.GrantRole:output_type -> host.installation.v1.Installation - 9, // 12: host.installation.v1.InstallationService.RevokeRole:output_type -> host.installation.v1.Installation - 5, // 13: host.installation.v1.InstallationService.ListRoleGrants:output_type -> host.installation.v1.ListRoleGrantsResponse - 8, // 14: host.installation.v1.InstallationService.List:output_type -> host.installation.v1.ListResponse - 11, // 15: host.installation.v1.InstallableService.Install:output_type -> host.installation.v1.InstallationAck - 10, // [10:16] is the sub-list for method output_type - 4, // [4:10] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 3, // 0: host.installation.v1.InstallRequest.approval:type_name -> host.installation.v1.ConsentApproval + 8, // 1: host.installation.v1.ListRoleGrantsResponse.grants:type_name -> host.installation.v1.RoleGrant + 11, // 2: host.installation.v1.ListResponse.installations:type_name -> host.installation.v1.Installation + 0, // 3: host.installation.v1.Installation.status:type_name -> host.installation.v1.Installation.Status + 11, // 4: host.installation.v1.InstallationNotice.installation:type_name -> host.installation.v1.Installation + 1, // 5: host.installation.v1.InstallationService.Install:input_type -> host.installation.v1.InstallRequest + 2, // 6: host.installation.v1.InstallationService.Review:input_type -> host.installation.v1.ReviewRequest + 4, // 7: host.installation.v1.InstallationService.GrantRole:input_type -> host.installation.v1.GrantRoleRequest + 5, // 8: host.installation.v1.InstallationService.RevokeRole:input_type -> host.installation.v1.RevokeRoleRequest + 6, // 9: host.installation.v1.InstallationService.ListRoleGrants:input_type -> host.installation.v1.ListRoleGrantsRequest + 9, // 10: host.installation.v1.InstallationService.List:input_type -> host.installation.v1.ListRequest + 12, // 11: host.installation.v1.InstallableService.Install:input_type -> host.installation.v1.InstallationNotice + 11, // 12: host.installation.v1.InstallationService.Install:output_type -> host.installation.v1.Installation + 3, // 13: host.installation.v1.InstallationService.Review:output_type -> host.installation.v1.ConsentApproval + 11, // 14: host.installation.v1.InstallationService.GrantRole:output_type -> host.installation.v1.Installation + 11, // 15: host.installation.v1.InstallationService.RevokeRole:output_type -> host.installation.v1.Installation + 7, // 16: host.installation.v1.InstallationService.ListRoleGrants:output_type -> host.installation.v1.ListRoleGrantsResponse + 10, // 17: host.installation.v1.InstallationService.List:output_type -> host.installation.v1.ListResponse + 13, // 18: host.installation.v1.InstallableService.Install:output_type -> host.installation.v1.InstallationAck + 12, // [12:19] is the sub-list for method output_type + 5, // [5:12] 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_host_installation_v1_installation_proto_init() } @@ -917,7 +1077,7 @@ func file_host_installation_v1_installation_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_host_installation_v1_installation_proto_rawDesc), len(file_host_installation_v1_installation_proto_rawDesc)), NumEnums: 1, - NumMessages: 11, + NumMessages: 13, NumExtensions: 0, NumServices: 2, }, diff --git a/libraries/authority-client/go/gen/host/installation/v1/installation_grpc.pb.go b/libraries/authority-client/go/gen/host/installation/v1/installation_grpc.pb.go index d6a5f8f9..8e7d8bce 100644 --- a/libraries/authority-client/go/gen/host/installation/v1/installation_grpc.pb.go +++ b/libraries/authority-client/go/gen/host/installation/v1/installation_grpc.pb.go @@ -27,6 +27,7 @@ const _ = grpc.SupportPackageIsVersion9 const ( InstallationService_Install_FullMethodName = "/host.installation.v1.InstallationService/Install" + InstallationService_Review_FullMethodName = "/host.installation.v1.InstallationService/Review" InstallationService_GrantRole_FullMethodName = "/host.installation.v1.InstallationService/GrantRole" InstallationService_RevokeRole_FullMethodName = "/host.installation.v1.InstallationService/RevokeRole" InstallationService_ListRoleGrants_FullMethodName = "/host.installation.v1.InstallationService/ListRoleGrants" @@ -42,6 +43,8 @@ type InstallationServiceClient interface { // Install composes an installation: its agent principal, its root scope and // the row, pending until the target acknowledges its install step. Install(ctx context.Context, in *InstallRequest, opts ...grpc.CallOption) (*Installation, error) + // Review reads the current offer and installation revision without granting consent. + Review(ctx context.Context, in *ReviewRequest, opts ...grpc.CallOption) (*ConsentApproval, error) // GrantRole grants a person or a team one of the solution's roles on the // installation: the access. Idempotent; it leaves existing capabilities // valid. An administrator of the installation's organization, or a holder @@ -77,6 +80,16 @@ func (c *installationServiceClient) Install(ctx context.Context, in *InstallRequ return out, nil } +func (c *installationServiceClient) Review(ctx context.Context, in *ReviewRequest, opts ...grpc.CallOption) (*ConsentApproval, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ConsentApproval) + err := c.cc.Invoke(ctx, InstallationService_Review_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *installationServiceClient) GrantRole(ctx context.Context, in *GrantRoleRequest, opts ...grpc.CallOption) (*Installation, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Installation) @@ -126,6 +139,8 @@ type InstallationServiceServer interface { // Install composes an installation: its agent principal, its root scope and // the row, pending until the target acknowledges its install step. Install(context.Context, *InstallRequest) (*Installation, error) + // Review reads the current offer and installation revision without granting consent. + Review(context.Context, *ReviewRequest) (*ConsentApproval, error) // GrantRole grants a person or a team one of the solution's roles on the // installation: the access. Idempotent; it leaves existing capabilities // valid. An administrator of the installation's organization, or a holder @@ -154,6 +169,9 @@ type UnimplementedInstallationServiceServer struct{} func (UnimplementedInstallationServiceServer) Install(context.Context, *InstallRequest) (*Installation, error) { return nil, status.Error(codes.Unimplemented, "method Install not implemented") } +func (UnimplementedInstallationServiceServer) Review(context.Context, *ReviewRequest) (*ConsentApproval, error) { + return nil, status.Error(codes.Unimplemented, "method Review not implemented") +} func (UnimplementedInstallationServiceServer) GrantRole(context.Context, *GrantRoleRequest) (*Installation, error) { return nil, status.Error(codes.Unimplemented, "method GrantRole not implemented") } @@ -205,6 +223,24 @@ func _InstallationService_Install_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _InstallationService_Review_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReviewRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(InstallationServiceServer).Review(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: InstallationService_Review_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(InstallationServiceServer).Review(ctx, req.(*ReviewRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _InstallationService_GrantRole_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GrantRoleRequest) if err := dec(in); err != nil { @@ -288,6 +324,10 @@ var InstallationService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Install", Handler: _InstallationService_Install_Handler, }, + { + MethodName: "Review", + Handler: _InstallationService_Review_Handler, + }, { MethodName: "GrantRole", Handler: _InstallationService_GrantRole_Handler, diff --git a/libraries/authority-client/library.codefly.yaml b/libraries/authority-client/library.codefly.yaml index 62737a8f..d5cef845 100644 --- a/libraries/authority-client/library.codefly.yaml +++ b/libraries/authority-client/library.codefly.yaml @@ -20,7 +20,7 @@ sources: module: authority service: local endpoint: grpc - contract-digest: sha256:4d92544f5e186880be4d17079ad6dc056a6f9a84a0e1ebb298a2ff9b72863a46 + contract-digest: sha256:f70c838a51dc2bfe1d95bfec4a1ab0f67d7bfd48ce60ccce58de30d342dce6c0 services: - LocalService generated-by: diff --git a/libraries/authority-client/python/authority_client/_gen/host/installation/v1/installation_pb2.py b/libraries/authority-client/python/authority_client/_gen/host/installation/v1/installation_pb2.py index b834e4a3..1577a31d 100644 --- a/libraries/authority-client/python/authority_client/_gen/host/installation/v1/installation_pb2.py +++ b/libraries/authority-client/python/authority_client/_gen/host/installation/v1/installation_pb2.py @@ -24,7 +24,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\'host/installation/v1/installation.proto\x12\x14host.installation.v1\"\x8d\x01\n\x0eInstallRequest\x12\'\n\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\x12\x16\n\x06target\x18\x02 \x01(\tR\x06target\x12\x12\n\x04role\x18\x03 \x01(\tR\x04role\x12&\n\x0fowner_person_id\x18\x04 \x01(\tR\rownerPersonId\"\xae\x01\n\x10GrantRoleRequest\x12\'\n\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\x12\'\n\x0finstallation_id\x18\x02 \x01(\tR\x0einstallationId\x12\x12\n\x04role\x18\x03 \x01(\tR\x04role\x12\x1b\n\tperson_id\x18\x04 \x01(\tR\x08personId\x12\x17\n\x07team_id\x18\x05 \x01(\tR\x06teamId\"\xaf\x01\n\x11RevokeRoleRequest\x12\'\n\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\x12\'\n\x0finstallation_id\x18\x02 \x01(\tR\x0einstallationId\x12\x12\n\x04role\x18\x03 \x01(\tR\x04role\x12\x1b\n\tperson_id\x18\x04 \x01(\tR\x08personId\x12\x17\n\x07team_id\x18\x05 \x01(\tR\x06teamId\"i\n\x15ListRoleGrantsRequest\x12\'\n\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\x12\'\n\x0finstallation_id\x18\x02 \x01(\tR\x0einstallationId\"Q\n\x16ListRoleGrantsResponse\x12\x37\n\x06grants\x18\x01 \x03(\x0b\x32\x1f.host.installation.v1.RoleGrantR\x06grants\"U\n\tRoleGrant\x12\x12\n\x04role\x18\x01 \x01(\tR\x04role\x12\x1b\n\tperson_id\x18\x02 \x01(\tR\x08personId\x12\x17\n\x07team_id\x18\x03 \x01(\tR\x06teamId\"6\n\x0bListRequest\x12\'\n\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\"X\n\x0cListResponse\x12H\n\rinstallations\x18\x01 \x03(\x0b\x32\".host.installation.v1.InstallationR\rinstallations\"\xb0\x03\n\x0cInstallation\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\'\n\x0forganization_id\x18\x02 \x01(\tR\x0eorganizationId\x12\x16\n\x06target\x18\x03 \x01(\tR\x06target\x12\x41\n\x06status\x18\x04 \x01(\x0e\x32).host.installation.v1.Installation.StatusR\x06status\x12\x1a\n\x08revision\x18\x05 \x01(\x04R\x08revision\x12\x12\n\x04role\x18\x06 \x01(\tR\x04role\x12\x1c\n\tnamespace\x18\t \x01(\tR\tnamespace\x12!\n\x0cprincipal_id\x18\n \x01(\tR\x0bprincipalId\x12\x14\n\x05roles\x18\x0b \x03(\tR\x05roles\"[\n\x06Status\x12\x16\n\x12STATUS_UNSPECIFIED\x10\x00\x12\x12\n\x0eSTATUS_PENDING\x10\x01\x12\x11\n\rSTATUS_ACTIVE\x10\x02\x12\x12\n\x0eSTATUS_REVOKED\x10\x03J\x04\x08\x07\x10\x08J\x04\x08\x08\x10\tR\x08team_idsR\x12\x61gent_principal_id\"\\\n\x12InstallationNotice\x12\x46\n\x0cinstallation\x18\x01 \x01(\x0b\x32\".host.installation.v1.InstallationR\x0cinstallation\"\x11\n\x0fInstallationAck2\xda\x03\n\x13InstallationService\x12S\n\x07Install\x12$.host.installation.v1.InstallRequest\x1a\".host.installation.v1.Installation\x12W\n\tGrantRole\x12&.host.installation.v1.GrantRoleRequest\x1a\".host.installation.v1.Installation\x12Y\n\nRevokeRole\x12\'.host.installation.v1.RevokeRoleRequest\x1a\".host.installation.v1.Installation\x12k\n\x0eListRoleGrants\x12+.host.installation.v1.ListRoleGrantsRequest\x1a,.host.installation.v1.ListRoleGrantsResponse\x12M\n\x04List\x12!.host.installation.v1.ListRequest\x1a\".host.installation.v1.ListResponse2p\n\x12InstallableService\x12Z\n\x07Install\x12(.host.installation.v1.InstallationNotice\x1a%.host.installation.v1.InstallationAckB5Z3gateway/pkg/gen/host/installation/v1;installationv1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\'host/installation/v1/installation.proto\x12\x14host.installation.v1\"\xd0\x01\n\x0eInstallRequest\x12\'\n\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\x12\x16\n\x06target\x18\x02 \x01(\tR\x06target\x12\x12\n\x04role\x18\x03 \x01(\tR\x04role\x12&\n\x0fowner_person_id\x18\x04 \x01(\tR\rownerPersonId\x12\x41\n\x08\x61pproval\x18\x05 \x01(\x0b\x32%.host.installation.v1.ConsentApprovalR\x08\x61pproval\"P\n\rReviewRequest\x12\'\n\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\x12\x16\n\x06target\x18\x02 \x01(\tR\x06target\"\xb6\x01\n\x0f\x43onsentApproval\x12\x1b\n\ttarget_id\x18\x01 \x01(\tR\x08targetId\x12\x1e\n\ngeneration\x18\x02 \x01(\x04R\ngeneration\x12+\n\x11\x65xpected_revision\x18\x03 \x01(\x04R\x10\x65xpectedRevision\x12!\n\x0coperation_id\x18\x04 \x01(\tR\x0boperationId\x12\x16\n\x06\x64igest\x18\x05 \x01(\tR\x06\x64igest\"\xae\x01\n\x10GrantRoleRequest\x12\'\n\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\x12\'\n\x0finstallation_id\x18\x02 \x01(\tR\x0einstallationId\x12\x12\n\x04role\x18\x03 \x01(\tR\x04role\x12\x1b\n\tperson_id\x18\x04 \x01(\tR\x08personId\x12\x17\n\x07team_id\x18\x05 \x01(\tR\x06teamId\"\xaf\x01\n\x11RevokeRoleRequest\x12\'\n\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\x12\'\n\x0finstallation_id\x18\x02 \x01(\tR\x0einstallationId\x12\x12\n\x04role\x18\x03 \x01(\tR\x04role\x12\x1b\n\tperson_id\x18\x04 \x01(\tR\x08personId\x12\x17\n\x07team_id\x18\x05 \x01(\tR\x06teamId\"i\n\x15ListRoleGrantsRequest\x12\'\n\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\x12\'\n\x0finstallation_id\x18\x02 \x01(\tR\x0einstallationId\"Q\n\x16ListRoleGrantsResponse\x12\x37\n\x06grants\x18\x01 \x03(\x0b\x32\x1f.host.installation.v1.RoleGrantR\x06grants\"U\n\tRoleGrant\x12\x12\n\x04role\x18\x01 \x01(\tR\x04role\x12\x1b\n\tperson_id\x18\x02 \x01(\tR\x08personId\x12\x17\n\x07team_id\x18\x03 \x01(\tR\x06teamId\"6\n\x0bListRequest\x12\'\n\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\"X\n\x0cListResponse\x12H\n\rinstallations\x18\x01 \x03(\x0b\x32\".host.installation.v1.InstallationR\rinstallations\"\xb0\x03\n\x0cInstallation\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\'\n\x0forganization_id\x18\x02 \x01(\tR\x0eorganizationId\x12\x16\n\x06target\x18\x03 \x01(\tR\x06target\x12\x41\n\x06status\x18\x04 \x01(\x0e\x32).host.installation.v1.Installation.StatusR\x06status\x12\x1a\n\x08revision\x18\x05 \x01(\x04R\x08revision\x12\x12\n\x04role\x18\x06 \x01(\tR\x04role\x12\x1c\n\tnamespace\x18\t \x01(\tR\tnamespace\x12!\n\x0cprincipal_id\x18\n \x01(\tR\x0bprincipalId\x12\x14\n\x05roles\x18\x0b \x03(\tR\x05roles\"[\n\x06Status\x12\x16\n\x12STATUS_UNSPECIFIED\x10\x00\x12\x12\n\x0eSTATUS_PENDING\x10\x01\x12\x11\n\rSTATUS_ACTIVE\x10\x02\x12\x12\n\x0eSTATUS_REVOKED\x10\x03J\x04\x08\x07\x10\x08J\x04\x08\x08\x10\tR\x08team_idsR\x12\x61gent_principal_id\"\\\n\x12InstallationNotice\x12\x46\n\x0cinstallation\x18\x01 \x01(\x0b\x32\".host.installation.v1.InstallationR\x0cinstallation\"\x11\n\x0fInstallationAck2\xb0\x04\n\x13InstallationService\x12S\n\x07Install\x12$.host.installation.v1.InstallRequest\x1a\".host.installation.v1.Installation\x12T\n\x06Review\x12#.host.installation.v1.ReviewRequest\x1a%.host.installation.v1.ConsentApproval\x12W\n\tGrantRole\x12&.host.installation.v1.GrantRoleRequest\x1a\".host.installation.v1.Installation\x12Y\n\nRevokeRole\x12\'.host.installation.v1.RevokeRoleRequest\x1a\".host.installation.v1.Installation\x12k\n\x0eListRoleGrants\x12+.host.installation.v1.ListRoleGrantsRequest\x1a,.host.installation.v1.ListRoleGrantsResponse\x12M\n\x04List\x12!.host.installation.v1.ListRequest\x1a\".host.installation.v1.ListResponse2p\n\x12InstallableService\x12Z\n\x07Install\x12(.host.installation.v1.InstallationNotice\x1a%.host.installation.v1.InstallationAckB5Z3gateway/pkg/gen/host/installation/v1;installationv1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -33,31 +33,35 @@ _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'Z3gateway/pkg/gen/host/installation/v1;installationv1' _globals['_INSTALLREQUEST']._serialized_start=66 - _globals['_INSTALLREQUEST']._serialized_end=207 - _globals['_GRANTROLEREQUEST']._serialized_start=210 - _globals['_GRANTROLEREQUEST']._serialized_end=384 - _globals['_REVOKEROLEREQUEST']._serialized_start=387 - _globals['_REVOKEROLEREQUEST']._serialized_end=562 - _globals['_LISTROLEGRANTSREQUEST']._serialized_start=564 - _globals['_LISTROLEGRANTSREQUEST']._serialized_end=669 - _globals['_LISTROLEGRANTSRESPONSE']._serialized_start=671 - _globals['_LISTROLEGRANTSRESPONSE']._serialized_end=752 - _globals['_ROLEGRANT']._serialized_start=754 - _globals['_ROLEGRANT']._serialized_end=839 - _globals['_LISTREQUEST']._serialized_start=841 - _globals['_LISTREQUEST']._serialized_end=895 - _globals['_LISTRESPONSE']._serialized_start=897 - _globals['_LISTRESPONSE']._serialized_end=985 - _globals['_INSTALLATION']._serialized_start=988 - _globals['_INSTALLATION']._serialized_end=1420 - _globals['_INSTALLATION_STATUS']._serialized_start=1287 - _globals['_INSTALLATION_STATUS']._serialized_end=1378 - _globals['_INSTALLATIONNOTICE']._serialized_start=1422 - _globals['_INSTALLATIONNOTICE']._serialized_end=1514 - _globals['_INSTALLATIONACK']._serialized_start=1516 - _globals['_INSTALLATIONACK']._serialized_end=1533 - _globals['_INSTALLATIONSERVICE']._serialized_start=1536 - _globals['_INSTALLATIONSERVICE']._serialized_end=2010 - _globals['_INSTALLABLESERVICE']._serialized_start=2012 - _globals['_INSTALLABLESERVICE']._serialized_end=2124 + _globals['_INSTALLREQUEST']._serialized_end=274 + _globals['_REVIEWREQUEST']._serialized_start=276 + _globals['_REVIEWREQUEST']._serialized_end=356 + _globals['_CONSENTAPPROVAL']._serialized_start=359 + _globals['_CONSENTAPPROVAL']._serialized_end=541 + _globals['_GRANTROLEREQUEST']._serialized_start=544 + _globals['_GRANTROLEREQUEST']._serialized_end=718 + _globals['_REVOKEROLEREQUEST']._serialized_start=721 + _globals['_REVOKEROLEREQUEST']._serialized_end=896 + _globals['_LISTROLEGRANTSREQUEST']._serialized_start=898 + _globals['_LISTROLEGRANTSREQUEST']._serialized_end=1003 + _globals['_LISTROLEGRANTSRESPONSE']._serialized_start=1005 + _globals['_LISTROLEGRANTSRESPONSE']._serialized_end=1086 + _globals['_ROLEGRANT']._serialized_start=1088 + _globals['_ROLEGRANT']._serialized_end=1173 + _globals['_LISTREQUEST']._serialized_start=1175 + _globals['_LISTREQUEST']._serialized_end=1229 + _globals['_LISTRESPONSE']._serialized_start=1231 + _globals['_LISTRESPONSE']._serialized_end=1319 + _globals['_INSTALLATION']._serialized_start=1322 + _globals['_INSTALLATION']._serialized_end=1754 + _globals['_INSTALLATION_STATUS']._serialized_start=1621 + _globals['_INSTALLATION_STATUS']._serialized_end=1712 + _globals['_INSTALLATIONNOTICE']._serialized_start=1756 + _globals['_INSTALLATIONNOTICE']._serialized_end=1848 + _globals['_INSTALLATIONACK']._serialized_start=1850 + _globals['_INSTALLATIONACK']._serialized_end=1867 + _globals['_INSTALLATIONSERVICE']._serialized_start=1870 + _globals['_INSTALLATIONSERVICE']._serialized_end=2430 + _globals['_INSTALLABLESERVICE']._serialized_start=2432 + _globals['_INSTALLABLESERVICE']._serialized_end=2544 # @@protoc_insertion_point(module_scope) diff --git a/libraries/authority-client/python/authority_client/_gen/host/installation/v1/installation_pb2_grpc.py b/libraries/authority-client/python/authority_client/_gen/host/installation/v1/installation_pb2_grpc.py index 0c98693f..9e7a3df9 100644 --- a/libraries/authority-client/python/authority_client/_gen/host/installation/v1/installation_pb2_grpc.py +++ b/libraries/authority-client/python/authority_client/_gen/host/installation/v1/installation_pb2_grpc.py @@ -40,6 +40,11 @@ def __init__(self, channel): request_serializer=host_dot_installation_dot_v1_dot_installation__pb2.InstallRequest.SerializeToString, response_deserializer=host_dot_installation_dot_v1_dot_installation__pb2.Installation.FromString, _registered_method=True) + self.Review = channel.unary_unary( + '/host.installation.v1.InstallationService/Review', + request_serializer=host_dot_installation_dot_v1_dot_installation__pb2.ReviewRequest.SerializeToString, + response_deserializer=host_dot_installation_dot_v1_dot_installation__pb2.ConsentApproval.FromString, + _registered_method=True) self.GrantRole = channel.unary_unary( '/host.installation.v1.InstallationService/GrantRole', request_serializer=host_dot_installation_dot_v1_dot_installation__pb2.GrantRoleRequest.SerializeToString, @@ -74,6 +79,13 @@ def Install(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def Review(self, request, context): + """Review reads the current offer and installation revision without granting consent. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def GrantRole(self, request, context): """GrantRole grants a person or a team one of the solution's roles on the installation: the access. Idempotent; it leaves existing capabilities @@ -117,6 +129,11 @@ def add_InstallationServiceServicer_to_server(servicer, server): request_deserializer=host_dot_installation_dot_v1_dot_installation__pb2.InstallRequest.FromString, response_serializer=host_dot_installation_dot_v1_dot_installation__pb2.Installation.SerializeToString, ), + 'Review': grpc.unary_unary_rpc_method_handler( + servicer.Review, + request_deserializer=host_dot_installation_dot_v1_dot_installation__pb2.ReviewRequest.FromString, + response_serializer=host_dot_installation_dot_v1_dot_installation__pb2.ConsentApproval.SerializeToString, + ), 'GrantRole': grpc.unary_unary_rpc_method_handler( servicer.GrantRole, request_deserializer=host_dot_installation_dot_v1_dot_installation__pb2.GrantRoleRequest.FromString, @@ -176,6 +193,33 @@ def Install(request, metadata, _registered_method=True) + @staticmethod + def Review(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/host.installation.v1.InstallationService/Review', + host_dot_installation_dot_v1_dot_installation__pb2.ReviewRequest.SerializeToString, + host_dot_installation_dot_v1_dot_installation__pb2.ConsentApproval.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + @staticmethod def GrantRole(request, target, diff --git a/libraries/authority-client/typescript/src/gen/host/installation/v1/installation_pb.ts b/libraries/authority-client/typescript/src/gen/host/installation/v1/installation_pb.ts index 92cc19ae..0e1507ff 100644 --- a/libraries/authority-client/typescript/src/gen/host/installation/v1/installation_pb.ts +++ b/libraries/authority-client/typescript/src/gen/host/installation/v1/installation_pb.ts @@ -16,7 +16,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file host/installation/v1/installation.proto. */ export const file_host_installation_v1_installation: GenFile = /*@__PURE__*/ - fileDesc("Cidob3N0L2luc3RhbGxhdGlvbi92MS9pbnN0YWxsYXRpb24ucHJvdG8SFGhvc3QuaW5zdGFsbGF0aW9uLnYxImAKDkluc3RhbGxSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIOCgZ0YXJnZXQYAiABKAkSDAoEcm9sZRgDIAEoCRIXCg9vd25lcl9wZXJzb25faWQYBCABKAkidgoQR3JhbnRSb2xlUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEgwKBHJvbGUYAyABKAkSEQoJcGVyc29uX2lkGAQgASgJEg8KB3RlYW1faWQYBSABKAkidwoRUmV2b2tlUm9sZVJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRIMCgRyb2xlGAMgASgJEhEKCXBlcnNvbl9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJIkkKFUxpc3RSb2xlR3JhbnRzUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJIkkKFkxpc3RSb2xlR3JhbnRzUmVzcG9uc2USLwoGZ3JhbnRzGAEgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuUm9sZUdyYW50Ij0KCVJvbGVHcmFudBIMCgRyb2xlGAEgASgJEhEKCXBlcnNvbl9pZBgCIAEoCRIPCgd0ZWFtX2lkGAMgASgJIiYKC0xpc3RSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCSJJCgxMaXN0UmVzcG9uc2USOQoNaW5zdGFsbGF0aW9ucxgBIAMoCzIiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbiLdAgoMSW5zdGFsbGF0aW9uEgoKAmlkGAEgASgJEhcKD29yZ2FuaXphdGlvbl9pZBgCIAEoCRIOCgZ0YXJnZXQYAyABKAkSOQoGc3RhdHVzGAQgASgOMikuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uLlN0YXR1cxIQCghyZXZpc2lvbhgFIAEoBBIMCgRyb2xlGAYgASgJEhEKCW5hbWVzcGFjZRgJIAEoCRIUCgxwcmluY2lwYWxfaWQYCiABKAkSDQoFcm9sZXMYCyADKAkiWwoGU3RhdHVzEhYKElNUQVRVU19VTlNQRUNJRklFRBAAEhIKDlNUQVRVU19QRU5ESU5HEAESEQoNU1RBVFVTX0FDVElWRRACEhIKDlNUQVRVU19SRVZPS0VEEANKBAgHEAhKBAgIEAlSCHRlYW1faWRzUhJhZ2VudF9wcmluY2lwYWxfaWQiTgoSSW5zdGFsbGF0aW9uTm90aWNlEjgKDGluc3RhbGxhdGlvbhgBIAEoCzIiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbiIRCg9JbnN0YWxsYXRpb25BY2sy2gMKE0luc3RhbGxhdGlvblNlcnZpY2USUwoHSW5zdGFsbBIkLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uElcKCUdyYW50Um9sZRImLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkdyYW50Um9sZVJlcXVlc3QaIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24SWQoKUmV2b2tlUm9sZRInLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJldm9rZVJvbGVSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uEmsKDkxpc3RSb2xlR3JhbnRzEisuaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdFJvbGVHcmFudHNSZXF1ZXN0GiwuaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdFJvbGVHcmFudHNSZXNwb25zZRJNCgRMaXN0EiEuaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdFJlcXVlc3QaIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0UmVzcG9uc2UycAoSSW5zdGFsbGFibGVTZXJ2aWNlEloKB0luc3RhbGwSKC5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb25Ob3RpY2UaJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb25BY2tCNVozZ2F0ZXdheS9wa2cvZ2VuL2hvc3QvaW5zdGFsbGF0aW9uL3YxO2luc3RhbGxhdGlvbnYxYgZwcm90bzM"); + fileDesc("Cidob3N0L2luc3RhbGxhdGlvbi92MS9pbnN0YWxsYXRpb24ucHJvdG8SFGhvc3QuaW5zdGFsbGF0aW9uLnYxIpkBCg5JbnN0YWxsUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSDgoGdGFyZ2V0GAIgASgJEgwKBHJvbGUYAyABKAkSFwoPb3duZXJfcGVyc29uX2lkGAQgASgJEjcKCGFwcHJvdmFsGAUgASgLMiUuaG9zdC5pbnN0YWxsYXRpb24udjEuQ29uc2VudEFwcHJvdmFsIjgKDVJldmlld1JlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEg4KBnRhcmdldBgCIAEoCSJ5Cg9Db25zZW50QXBwcm92YWwSEQoJdGFyZ2V0X2lkGAEgASgJEhIKCmdlbmVyYXRpb24YAiABKAQSGQoRZXhwZWN0ZWRfcmV2aXNpb24YAyABKAQSFAoMb3BlcmF0aW9uX2lkGAQgASgJEg4KBmRpZ2VzdBgFIAEoCSJ2ChBHcmFudFJvbGVSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYAiABKAkSDAoEcm9sZRgDIAEoCRIRCglwZXJzb25faWQYBCABKAkSDwoHdGVhbV9pZBgFIAEoCSJ3ChFSZXZva2VSb2xlUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEgwKBHJvbGUYAyABKAkSEQoJcGVyc29uX2lkGAQgASgJEg8KB3RlYW1faWQYBSABKAkiSQoVTGlzdFJvbGVHcmFudHNSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYAiABKAkiSQoWTGlzdFJvbGVHcmFudHNSZXNwb25zZRIvCgZncmFudHMYASADKAsyHy5ob3N0Lmluc3RhbGxhdGlvbi52MS5Sb2xlR3JhbnQiPQoJUm9sZUdyYW50EgwKBHJvbGUYASABKAkSEQoJcGVyc29uX2lkGAIgASgJEg8KB3RlYW1faWQYAyABKAkiJgoLTGlzdFJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJIkkKDExpc3RSZXNwb25zZRI5Cg1pbnN0YWxsYXRpb25zGAEgAygLMiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uIt0CCgxJbnN0YWxsYXRpb24SCgoCaWQYASABKAkSFwoPb3JnYW5pemF0aW9uX2lkGAIgASgJEg4KBnRhcmdldBgDIAEoCRI5CgZzdGF0dXMYBCABKA4yKS5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24uU3RhdHVzEhAKCHJldmlzaW9uGAUgASgEEgwKBHJvbGUYBiABKAkSEQoJbmFtZXNwYWNlGAkgASgJEhQKDHByaW5jaXBhbF9pZBgKIAEoCRINCgVyb2xlcxgLIAMoCSJbCgZTdGF0dXMSFgoSU1RBVFVTX1VOU1BFQ0lGSUVEEAASEgoOU1RBVFVTX1BFTkRJTkcQARIRCg1TVEFUVVNfQUNUSVZFEAISEgoOU1RBVFVTX1JFVk9LRUQQA0oECAcQCEoECAgQCVIIdGVhbV9pZHNSEmFnZW50X3ByaW5jaXBhbF9pZCJOChJJbnN0YWxsYXRpb25Ob3RpY2USOAoMaW5zdGFsbGF0aW9uGAEgASgLMiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uIhEKD0luc3RhbGxhdGlvbkFjazKwBAoTSW5zdGFsbGF0aW9uU2VydmljZRJTCgdJbnN0YWxsEiQuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbFJlcXVlc3QaIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24SVAoGUmV2aWV3EiMuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2aWV3UmVxdWVzdBolLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkNvbnNlbnRBcHByb3ZhbBJXCglHcmFudFJvbGUSJi5ob3N0Lmluc3RhbGxhdGlvbi52MS5HcmFudFJvbGVSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uElkKClJldm9rZVJvbGUSJy5ob3N0Lmluc3RhbGxhdGlvbi52MS5SZXZva2VSb2xlUmVxdWVzdBoiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhJrCg5MaXN0Um9sZUdyYW50cxIrLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSb2xlR3JhbnRzUmVxdWVzdBosLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSb2xlR3JhbnRzUmVzcG9uc2USTQoETGlzdBIhLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdFJlc3BvbnNlMnAKEkluc3RhbGxhYmxlU2VydmljZRJaCgdJbnN0YWxsEiguaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uTm90aWNlGiUuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uQWNrQjVaM2dhdGV3YXkvcGtnL2dlbi9ob3N0L2luc3RhbGxhdGlvbi92MTtpbnN0YWxsYXRpb252MWIGcHJvdG8z"); /** * InstallRequest asks to install one target at one organisation. @@ -51,6 +51,13 @@ export type InstallRequest = Message<"host.installation.v1.InstallRequest"> & { * @generated from field: string owner_person_id = 4; */ ownerPersonId: string; + + /** + * Required: the exact offer reviewed by the administrator; unchanged on retry. + * + * @generated from field: host.installation.v1.ConsentApproval approval = 5; + */ + approval?: ConsentApproval; }; /** @@ -60,6 +67,73 @@ export type InstallRequest = Message<"host.installation.v1.InstallRequest"> & { export const InstallRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_host_installation_v1_installation, 0); +/** + * ReviewRequest names the offer an administrator intends to review. + * + * @generated from message host.installation.v1.ReviewRequest + */ +export type ReviewRequest = Message<"host.installation.v1.ReviewRequest"> & { + /** + * @generated from field: string organization_id = 1; + */ + organizationId: string; + + /** + * @generated from field: string target = 2; + */ + target: string; +}; + +/** + * Describes the message host.installation.v1.ReviewRequest. + * Use `create(ReviewRequestSchema)` to create a new message. + */ +export const ReviewRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 1); + +/** + * ConsentApproval binds consent to an immutable target, its reviewed delivery and + * the installation revision (zero if absent). Review leaves operation_id empty; + * the caller supplies a fresh UUID and preserves the entire request on retry. + * + * @generated from message host.installation.v1.ConsentApproval + */ +export type ConsentApproval = Message<"host.installation.v1.ConsentApproval"> & { + /** + * @generated from field: string target_id = 1; + */ + targetId: string; + + /** + * @generated from field: uint64 generation = 2; + */ + generation: bigint; + + /** + * @generated from field: uint64 expected_revision = 3; + */ + expectedRevision: bigint; + + /** + * @generated from field: string operation_id = 4; + */ + operationId: string; + + /** + * sha256 digest of the reviewed authority offer. + * + * @generated from field: string digest = 5; + */ + digest: string; +}; + +/** + * Describes the message host.installation.v1.ConsentApproval. + * Use `create(ConsentApprovalSchema)` to create a new message. + */ +export const ConsentApprovalSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 2); + /** * GrantRoleRequest grants one role to exactly one person or team. * @@ -110,7 +184,7 @@ export type GrantRoleRequest = Message<"host.installation.v1.GrantRoleRequest"> * Use `create(GrantRoleRequestSchema)` to create a new message. */ export const GrantRoleRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_host_installation_v1_installation, 1); + messageDesc(file_host_installation_v1_installation, 3); /** * RevokeRoleRequest ends one grant, named as GrantRole named it. @@ -159,7 +233,7 @@ export type RevokeRoleRequest = Message<"host.installation.v1.RevokeRoleRequest" * Use `create(RevokeRoleRequestSchema)` to create a new message. */ export const RevokeRoleRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_host_installation_v1_installation, 2); + messageDesc(file_host_installation_v1_installation, 4); /** * ListRoleGrantsRequest names one installation. @@ -187,7 +261,7 @@ export type ListRoleGrantsRequest = Message<"host.installation.v1.ListRoleGrants * Use `create(ListRoleGrantsRequestSchema)` to create a new message. */ export const ListRoleGrantsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_host_installation_v1_installation, 3); + messageDesc(file_host_installation_v1_installation, 5); /** * ListRoleGrantsResponse is every grant on the installation. @@ -208,7 +282,7 @@ export type ListRoleGrantsResponse = Message<"host.installation.v1.ListRoleGrant * Use `create(ListRoleGrantsResponseSchema)` to create a new message. */ export const ListRoleGrantsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_host_installation_v1_installation, 4); + messageDesc(file_host_installation_v1_installation, 6); /** * RoleGrant is one role held by one person or team. @@ -243,7 +317,7 @@ export type RoleGrant = Message<"host.installation.v1.RoleGrant"> & { * Use `create(RoleGrantSchema)` to create a new message. */ export const RoleGrantSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_host_installation_v1_installation, 5); + messageDesc(file_host_installation_v1_installation, 7); /** * ListRequest asks for one organisation's installations. @@ -264,7 +338,7 @@ export type ListRequest = Message<"host.installation.v1.ListRequest"> & { * Use `create(ListRequestSchema)` to create a new message. */ export const ListRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_host_installation_v1_installation, 6); + messageDesc(file_host_installation_v1_installation, 8); /** * ListResponse lists installations. @@ -285,7 +359,7 @@ export type ListResponse = Message<"host.installation.v1.ListResponse"> & { * Use `create(ListResponseSchema)` to create a new message. */ export const ListResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_host_installation_v1_installation, 7); + messageDesc(file_host_installation_v1_installation, 9); /** * Installation is one organisation's installation of one target. @@ -368,7 +442,7 @@ export type Installation = Message<"host.installation.v1.Installation"> & { * Use `create(InstallationSchema)` to create a new message. */ export const InstallationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_host_installation_v1_installation, 8); + messageDesc(file_host_installation_v1_installation, 10); /** * Status is the installation's lifecycle. @@ -409,7 +483,7 @@ export enum Installation_Status { * Describes the enum host.installation.v1.Installation.Status. */ export const Installation_StatusSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_host_installation_v1_installation, 8, 0); + enumDesc(file_host_installation_v1_installation, 10, 0); /** * InstallationNotice is what the host hands the solution's install step. @@ -430,7 +504,7 @@ export type InstallationNotice = Message<"host.installation.v1.InstallationNotic * Use `create(InstallationNoticeSchema)` to create a new message. */ export const InstallationNoticeSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_host_installation_v1_installation, 9); + messageDesc(file_host_installation_v1_installation, 11); /** * InstallationAck acknowledges the install step. @@ -445,7 +519,7 @@ export type InstallationAck = Message<"host.installation.v1.InstallationAck"> & * Use `create(InstallationAckSchema)` to create a new message. */ export const InstallationAckSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_host_installation_v1_installation, 10); + messageDesc(file_host_installation_v1_installation, 12); /** * InstallationService is implemented by the host and by the local authority. @@ -464,6 +538,16 @@ export const InstallationService: GenService<{ input: typeof InstallRequestSchema; output: typeof InstallationSchema; }, + /** + * Review reads the current offer and installation revision without granting consent. + * + * @generated from rpc host.installation.v1.InstallationService.Review + */ + review: { + methodKind: "unary"; + input: typeof ReviewRequestSchema; + output: typeof ConsentApprovalSchema; + }, /** * GrantRole grants a person or a team one of the solution's roles on the * installation: the access. Idempotent; it leaves existing capabilities diff --git a/libraries/authority-core/go/authority/consent.go b/libraries/authority-core/go/authority/consent.go new file mode 100644 index 00000000..a097085e --- /dev/null +++ b/libraries/authority-core/go/authority/consent.go @@ -0,0 +1,106 @@ +package authority + +import ( + "context" + "crypto/sha256" + "encoding/json" + "errors" + "fmt" + + "github.com/codefly-dev/authority/libraries/authority-core/go/decide" + "github.com/codefly-dev/authority/libraries/authority-core/go/domain" + "github.com/google/uuid" +) + +func administrator(tx Tx, caller Person, org string) error { + p, err := tx.Person(caller.ID) + if err != nil && !errors.Is(err, domain.ErrNotFound) { + return err + } + if caller.ID == "" || err != nil || p.Organization != org || !decide.IsAdministrator(p) { + return domain.Refuse("installation administration requires an administrator or owner of organization %q", org) + } + return nil +} + +// offerDigest covers every authority declaration in the reviewed module. JSON +// sorts map keys; the receipt therefore covers roles, ceilings and bindings too. +func offerDigest(m domain.Module) (string, error) { + raw, err := json.Marshal(m) + if err != nil { + return "", err + } + return fmt.Sprintf("sha256:%x", sha256.Sum256(raw)), nil +} + +// Review reads one consistent offer. It never supplies an operation identity or +// approves anything; the caller must retain the returned values when consenting. +func (s *Service) Review(ctx context.Context, caller Person, org, target string) (domain.ConsentApproval, error) { + var out domain.ConsentApproval + err := s.store.Read(ctx, func(tx Tx) error { + if err := administrator(tx, caller, org); err != nil { + return err + } + m, err := tx.ModuleNamed(target) + if err != nil { + return notFound(err) + } + if m.Kind != domain.KindSolution { + return domain.Missing("%q is not a composed solution; a module is installed by composition", target) + } + digest, err := offerDigest(m) + if err != nil { + return err + } + out = domain.ConsentApproval{TargetID: m.Principal, Generation: m.Generation, Digest: digest} + inst, err := tx.Installation(s.cfg.InstallationID(org, target)) + if err == nil { + out.ExpectedRevision = inst.Revision + } else if !errors.Is(err, domain.ErrNotFound) { + return err + } + return nil + }) + return out, err +} + +func consentIntent(caller Person, org, target, role, owner string, a *domain.ConsentApproval) (string, error) { + if a == nil || a.TargetID == "" || a.Generation == 0 || a.Digest == "" || uuid.Validate(a.OperationID) != nil { + return "", domain.NotNow("INSTALLATION_APPROVAL_REQUIRED: review the offer, supply its target, generation, revision and digest with a fresh UUID operation_id, and preserve the request on retry") + } + raw, err := json.Marshal(struct { + Actor, Organization, Target, Role, Owner string + Approval *domain.ConsentApproval + }{caller.ID, org, target, role, owner, a}) + if err != nil { + return "", err + } + return fmt.Sprintf("%x", sha256.Sum256(raw)), nil +} + +func checkConsent(m domain.Module, existing domain.Installation, a *domain.ConsentApproval, intent string) (bool, error) { + if a.TargetID != m.Principal || a.Generation != m.Generation { + return false, domain.NotNow("INSTALLATION_APPROVAL_GENERATION_CHANGED: the reviewed target or generation changed; review and approve again") + } + digest, err := offerDigest(m) + if err != nil { + return false, err + } + if a.Digest != digest { + return false, domain.NotNow("INSTALLATION_APPROVAL_DIGEST_CHANGED: the reviewed authority offer changed; review and approve again") + } + receipt := existing.Consent + if receipt.OperationID == a.OperationID { + if receipt.Intent != intent { + return false, domain.NotNow("INSTALLATION_APPROVAL_OPERATION_CONFLICT: operation_id already names different approval inputs") + } + if existing.Status == domain.Revoked || receipt.Revision != existing.Revision { + return false, domain.NotNow("INSTALLATION_APPROVAL_REVISION_CHANGED: the installation changed after approval; review and approve again") + } + return true, nil + } + if a.ExpectedRevision != existing.Revision { + return false, domain.NotNow("INSTALLATION_APPROVAL_REVISION_CHANGED: the reviewed installation revision changed; review and approve again") + } + return false, nil +} diff --git a/libraries/authority-core/go/authority/consent_test.go b/libraries/authority-core/go/authority/consent_test.go new file mode 100644 index 00000000..dd5cff1b --- /dev/null +++ b/libraries/authority-core/go/authority/consent_test.go @@ -0,0 +1,135 @@ +package authority_test + +import ( + "context" + "testing" + + "github.com/codefly-dev/authority/libraries/authority-core/go/authority" + "github.com/codefly-dev/authority/libraries/authority-core/go/domain" +) + +func TestInstallationConsentBindsReviewedStateAndOperation(t *testing.T) { + h := newHarness(t) + admin := h.admin(globex) + a := h.approval(globex, "b") + steps := 0 + step := func(context.Context, domain.Installation) error { steps++; return nil } + install := func(a *domain.ConsentApproval, role string) (domain.Installation, error) { + return h.svc.Install(h.ctx, admin, globex, "b", role, admin.ID, a, step) + } + _, err := install(nil, "author") + refused(t, err, "INSTALLATION_APPROVAL_REQUIRED") + for _, mutate := range []struct { + reason string + change func(*domain.ConsentApproval) + }{ + {"GENERATION_CHANGED", func(a *domain.ConsentApproval) { a.Generation++ }}, + {"GENERATION_CHANGED", func(a *domain.ConsentApproval) { a.TargetID = "another-target" }}, + {"DIGEST_CHANGED", func(a *domain.ConsentApproval) { a.Digest = "sha256:unreviewed" }}, + {"REVISION_CHANGED", func(a *domain.ConsentApproval) { a.ExpectedRevision++ }}, + } { + bad := *a + mutate.change(&bad) + _, err := install(&bad, "author") + refused(t, err, "INSTALLATION_APPROVAL_"+mutate.reason) + } + if steps != 0 { + t.Fatal("a refused approval ran the install step") + } + first, err := install(a, "author") + if err != nil { + t.Fatal(err) + } + retry, err := install(a, "author") + if err != nil || first != retry || steps != 1 { + t.Fatalf("unchanged retry: %+v, steps=%d, err=%v", retry, steps, err) + } + _, err = install(a, "viewer") + refused(t, err, "INSTALLATION_APPROVAL_OPERATION_CONFLICT") + _, err = h.svc.Install(h.ctx, admin, globex, "b", "author", "", a, step) + refused(t, err, "INSTALLATION_APPROVAL_OPERATION_CONFLICT") + if err := h.svc.Uninstall(h.ctx, admin, globex, first.ID); err != nil { + t.Fatal(err) + } + _, err = install(a, "author") + refused(t, err, "INSTALLATION_APPROVAL_REVISION_CHANGED") + fresh := h.approval(globex, "b") + again, err := install(fresh, "author") + if err != nil || again.PrincipalID != first.PrincipalID || again.Revision <= first.Revision { + t.Fatalf("fresh reinstall: %+v %v", again, err) + } + m := solution("b", false) + m.Generation++ + if _, err := h.svc.ApplyModule(h.ctx, m); err != nil { + t.Fatal(err) + } + _, err = install(fresh, "author") + refused(t, err, "INSTALLATION_APPROVAL_GENERATION_CHANGED") +} + +func TestInstallationConsentIsRecheckedAtAcknowledgement(t *testing.T) { + for _, change := range []string{"delivery", "revocation", "administrator"} { + t.Run(change, func(t *testing.T) { + h := newHarness(t) + admin := h.admin(globex) + a := h.approval(globex, "b") + step := func(ctx context.Context, inst domain.Installation) error { + switch change { + case "delivery": + m := solution("b", false) + m.Generation++ + _, err := h.svc.ApplyModule(ctx, m) + return err + case "revocation": + return h.svc.Uninstall(ctx, admin, globex, inst.ID) + default: + h.store.PutPerson(domain.Person{ID: admin.ID, Organization: globex}) + return nil + } + } + _, err := h.svc.Install(h.ctx, admin, globex, "b", "author", admin.ID, a, step) + if err == nil { + t.Fatal("changed approval activated at acknowledgement") + } + if err := h.store.Read(h.ctx, func(tx authority.Tx) error { + inst, err := tx.Installation(authority.StableInstallationID(globex, "b")) + if err == nil && inst.Status == domain.Active { + t.Fatal("refused acknowledgement left active authority") + } + return err + }); err != nil { + t.Fatal(err) + } + }) + } +} + +// A pending attempt whose solution was redelivered is not stuck: its own +// operation is refused for the changed generation, a fresh review supersedes it, +// and the superseded operation can no longer resume it. +func TestAFreshApprovalSupersedesAPendingAttemptAfterRedelivery(t *testing.T) { + h := newHarness(t) + admin := h.admin(globex) + stale := h.approval(globex, "b") + redeliver := func(ctx context.Context, _ domain.Installation) error { + m := solution("b", false) + m.Generation++ + _, err := h.svc.ApplyModule(ctx, m) + return err + } + if _, err := h.svc.Install(h.ctx, admin, globex, "b", "author", admin.ID, stale, redeliver); err == nil { + t.Fatal("a redelivery during the step activated the stale approval") + } + ok := func(context.Context, domain.Installation) error { return nil } + _, err := h.svc.Install(h.ctx, admin, globex, "b", "author", admin.ID, stale, ok) + refused(t, err, "INSTALLATION_APPROVAL_GENERATION_CHANGED") + fresh := h.approval(globex, "b") + inst, err := h.svc.Install(h.ctx, admin, globex, "b", "author", admin.ID, fresh, ok) + if err != nil || inst.Status != domain.Active || inst.Consent.OperationID != fresh.OperationID { + t.Fatalf("fresh approval after redelivery: %+v %v", inst, err) + } + _, err = h.svc.Install(h.ctx, admin, globex, "b", "author", admin.ID, stale, ok) + if err == nil { + t.Fatal("the superseded operation was accepted") + } +} diff --git a/libraries/authority-core/go/authority/harness_test.go b/libraries/authority-core/go/authority/harness_test.go index f1655791..f40ff8d8 100644 --- a/libraries/authority-core/go/authority/harness_test.go +++ b/libraries/authority-core/go/authority/harness_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "github.com/codefly-dev/core/workcontext" + "github.com/google/uuid" "strings" "testing" "time" @@ -135,7 +136,7 @@ func (h *harness) admin(org string) authority.Person { func (h *harness) install(org, target string) domain.Installation { h.t.Helper() admin := h.admin(org) - inst, err := h.svc.Install(h.ctx, admin, org, target, "author", admin.ID, nil) + inst, err := h.svc.Install(h.ctx, admin, org, target, "author", admin.ID, h.approval(org, target), nil) if err != nil { h.t.Fatal(err) } @@ -247,3 +248,13 @@ func exactly(t *testing.T, i authority.Issued, k, owner string, actions []string } var _ = errors.New + +func (h *harness) approval(org, target string) *domain.ConsentApproval { + h.t.Helper() + a, err := h.svc.Review(h.ctx, h.admin(org), org, target) + if err != nil { + h.t.Fatal(err) + } + a.OperationID = uuid.NewString() + return &a +} diff --git a/libraries/authority-core/go/authority/lifecycle.go b/libraries/authority-core/go/authority/lifecycle.go index 4afed4b7..13ea1076 100644 --- a/libraries/authority-core/go/authority/lifecycle.go +++ b/libraries/authority-core/go/authority/lifecycle.go @@ -180,32 +180,41 @@ type InstallStep func(ctx context.Context, inst domain.Installation) error // while it is pending resumes it. Its principal is stable per organization and // target, so a reinstall reuses it; nobody holds a role until granted one, // the installing person included. The caller must administer the organization. -func (s *Service) Install(ctx context.Context, caller Person, org, target, agentRole, installedBy string, step InstallStep) (domain.Installation, error) { - return s.install(ctx, func(tx Tx) error { - p, err := tx.Person(caller.ID) - if err != nil && !errors.Is(err, domain.ErrNotFound) { - return err - } - if caller.ID == "" || err != nil || p.Organization != org || !decide.IsAdministrator(p) { - return domain.Refuse("installation administration requires an administrator or owner of organization %q", org) - } - return nil - }, org, target, agentRole, installedBy, step) +func (s *Service) Install(ctx context.Context, caller Person, org, target, agentRole, installedBy string, approval *domain.ConsentApproval, step InstallStep) (domain.Installation, error) { + if approval != nil { + copy := *approval + approval = © + } + return s.install(ctx, func(tx Tx) error { return administrator(tx, caller, org) }, caller, org, target, agentRole, installedBy, approval, step) } // InstallAsHost is for trusted host setup, never a request handler. It installs // a solution without person authorization; an absent person cannot select it. func (s *Service) InstallAsHost(ctx context.Context, org, target, agentRole, installedBy string, step InstallStep) (domain.Installation, error) { - return s.install(ctx, func(Tx) error { return nil }, org, target, agentRole, installedBy, step) + return s.install(ctx, func(Tx) error { return nil }, Person{}, org, target, agentRole, installedBy, nil, step) } -func (s *Service) install(ctx context.Context, authorize func(Tx) error, org, target, agentRole, installedBy string, step InstallStep) (domain.Installation, error) { +func (s *Service) install(ctx context.Context, authorize func(Tx) error, caller Person, org, target, agentRole, installedBy string, approval *domain.ConsentApproval, step InstallStep) (domain.Installation, error) { var inst domain.Installation var generation uint64 + var intent string + var replayed bool + requestedOwner := installedBy + if installedBy == "" && caller.ID != "" { + installedBy = caller.ID + } err := s.store.Write(ctx, func(tx Tx) error { + replayed = false if err := authorize(tx); err != nil { return err } + if caller.ID != "" { + var err error + intent, err = consentIntent(caller, org, target, agentRole, requestedOwner, approval) + if err != nil { + return err + } + } if _, err := tx.Organization(org); err != nil { return notFound(err) } @@ -232,6 +241,16 @@ func (s *Service) install(ctx context.Context, authorize func(Tx) error, org, ta if err != nil && !errors.Is(err, domain.ErrNotFound) { return err } + if approval != nil { + same, err := checkConsent(m, existing, approval, intent) + if err != nil { + return err + } + if same && existing.Status == domain.Active { + inst, replayed = existing, true + return nil + } + } if err == nil && existing.Status == domain.Pending { if existing.AgentRole != agentRole || existing.InstalledBy != installedBy { return domain.NotNow("%s is pending at %s as %s for %s; only that install resumes it", target, org, existing.AgentRole, existing.InstalledBy) @@ -239,6 +258,19 @@ func (s *Service) install(ctx context.Context, authorize func(Tx) error, org, ta // The same install asked again resumes its pending attempt: its // step converges idempotently, and its revision stays the one the // first attempt took, so a revoke or a reinstall since is told apart. + // A fresh approval supersedes the unfinished attempt instead: checkConsent + // held it to the current generation, digest and the pending revision, so + // it consents to exactly what is pending now. The revision moves with its + // receipt, so the superseded operation can neither resume nor acknowledge. + if approval != nil && existing.Consent.OperationID != approval.OperationID { + existing.Revision++ + existing.Consent = domain.ConsentReceipt{OperationID: approval.OperationID, Intent: intent, Revision: existing.Revision} + if err := tx.DeleteRoleGrants(id); err != nil { + return err + } + inst = existing + return tx.PutInstallation(existing) + } inst = existing return nil } @@ -268,6 +300,9 @@ func (s *Service) install(ctx context.Context, authorize func(Tx) error, org, ta } inst = domain.Installation{ID: id, Organization: org, Target: target, Status: domain.Pending, Revision: existing.Revision + 1, AgentRole: agentRole, PrincipalID: principal, InstalledBy: installedBy} + if approval != nil { + inst.Consent = domain.ConsentReceipt{OperationID: approval.OperationID, Intent: intent, Revision: inst.Revision} + } if err := tx.DeleteRoleGrants(id); err != nil { return err } @@ -276,6 +311,9 @@ func (s *Service) install(ctx context.Context, authorize func(Tx) error, org, ta if err != nil { return inst, err } + if replayed { + return inst, nil + } if step != nil { if err := step(ctx, inst); err != nil { return inst, err @@ -286,10 +324,26 @@ func (s *Service) install(ctx context.Context, authorize func(Tx) error, org, ta if err != nil { return notFound(err) } + if err := authorize(tx); err != nil { + return err + } m, err := tx.ModuleNamed(target) if err != nil && !errors.Is(err, domain.ErrNotFound) { return err } + if err == nil && approval != nil { + same, err := checkConsent(m, current, approval, intent) + if err != nil { + return err + } + if !same { + return domain.NotNow("INSTALLATION_APPROVAL_OPERATION_CONFLICT: approval changed before acknowledgement") + } + if current.Status == domain.Active { + inst = current + return nil + } + } if err != nil || m.Generation != generation || current.Status != domain.Pending || current.Revision != inst.Revision { return domain.NotNow("installation attempt %s changed before acknowledgement", inst.ID) } diff --git a/libraries/authority-core/go/authority/r143_test.go b/libraries/authority-core/go/authority/r143_test.go index 8f19ef7b..4c287824 100644 --- a/libraries/authority-core/go/authority/r143_test.go +++ b/libraries/authority-core/go/authority/r143_test.go @@ -192,12 +192,13 @@ func TestAFailedInstallStepCanBeRetried(t *testing.T) { h := newHarness(t) admin := h.admin(globex) down := func(context.Context, domain.Installation) error { return errors.New("step down") } - if _, err := h.svc.Install(h.ctx, admin, globex, "b", "author", admin.ID, down); err == nil { + approval := h.approval(globex, "b") + if _, err := h.svc.Install(h.ctx, admin, globex, "b", "author", admin.ID, approval, down); err == nil { t.Fatal("a failing step installed") } - _, err := h.svc.Install(h.ctx, admin, globex, "b", "viewer", admin.ID, nil) - refused(t, err, "pending") - inst, err := h.svc.Install(h.ctx, admin, globex, "b", "author", admin.ID, nil) + _, err := h.svc.Install(h.ctx, admin, globex, "b", "viewer", admin.ID, approval, nil) + refused(t, err, "INSTALLATION_APPROVAL_OPERATION_CONFLICT") + inst, err := h.svc.Install(h.ctx, admin, globex, "b", "author", admin.ID, approval, nil) if err != nil || inst.Status != domain.Active { t.Fatalf("the retry: %+v %v", inst, err) } @@ -216,7 +217,7 @@ func TestAnInstallPrincipalIsItsOwn(t *testing.T) { t.Fatal(err) } admin := h.admin(globex) - _, err = svc.Install(h.ctx, admin, globex, "b", "author", admin.ID, nil) + _, err = svc.Install(h.ctx, admin, globex, "b", "author", admin.ID, h.approval(globex, "b"), nil) code(t, err, domain.Denied) }) } diff --git a/libraries/authority-core/go/authority/r148_test.go b/libraries/authority-core/go/authority/r148_test.go index 985578b1..738c0f04 100644 --- a/libraries/authority-core/go/authority/r148_test.go +++ b/libraries/authority-core/go/authority/r148_test.go @@ -70,7 +70,7 @@ func TestATombstonedPrincipalIsNeverClaimed(t *testing.T) { t.Fatal(err) } admin := h.admin(globex) - _, err = svc.Install(h.ctx, admin, globex, "b", "author", admin.ID, nil) + _, err = svc.Install(h.ctx, admin, globex, "b", "author", admin.ID, h.approval(globex, "b"), nil) code(t, err, domain.Denied) } diff --git a/libraries/authority-core/go/authority/r152_test.go b/libraries/authority-core/go/authority/r152_test.go index af111255..41ac08f6 100644 --- a/libraries/authority-core/go/authority/r152_test.go +++ b/libraries/authority-core/go/authority/r152_test.go @@ -45,7 +45,7 @@ func TestMissingCallerCannotSelectHostPower(t *testing.T) { if h.inst(acme, "store") != before { t.Fatal("empty caller retired module") } - _, err := h.svc.Install(h.ctx, authority.Person{}, globex, "b", "author", h.admin(globex).ID, nil) + _, err := h.svc.Install(h.ctx, authority.Person{}, globex, "b", "author", h.admin(globex).ID, h.approval(globex, "b"), nil) code(t, err, domain.Denied) if err := h.store.Read(h.ctx, func(tx authority.Tx) error { _, err := tx.Installation(authority.StableInstallationID(globex, "b")) @@ -118,7 +118,7 @@ func TestLifecycleStoreFailuresAreReturnedWithoutMutation(t *testing.T) { if h.inst(acme, "store") != before { t.Fatal("outage mutated installation") } - _, err = svc.Install(h.ctx, h.admin(globex), globex, "b", "author", h.admin(globex).ID, nil) + _, err = svc.Install(h.ctx, h.admin(globex), globex, "b", "author", h.admin(globex).ID, h.approval(globex, "b"), nil) if !errors.Is(err, outage) { t.Fatalf("install hid outage: %v", err) } diff --git a/libraries/authority-core/go/authority/race_test.go b/libraries/authority-core/go/authority/race_test.go index 2bdb56dd..50b9760f 100644 --- a/libraries/authority-core/go/authority/race_test.go +++ b/libraries/authority-core/go/authority/race_test.go @@ -129,7 +129,7 @@ func TestAnUninstalledAgentsHopStaysDeadAfterReinstall(t *testing.T) { t.Fatal("the uninstalled agent's hop checks") } admin := h.admin(acme) - if _, err := h.svc.Install(h.ctx, admin, acme, "a", "viewer", admin.ID, nil); err != nil { + if _, err := h.svc.Install(h.ctx, admin, acme, "a", "viewer", admin.ID, h.approval(acme, "a"), nil); err != nil { t.Fatal(err) } if h.check(deeper) == nil { diff --git a/libraries/authority-core/go/authority/sources_test.go b/libraries/authority-core/go/authority/sources_test.go index cbbc9668..3a486685 100644 --- a/libraries/authority-core/go/authority/sources_test.go +++ b/libraries/authority-core/go/authority/sources_test.go @@ -29,7 +29,7 @@ func TestAReinstallNeverRepeatsThePrincipalsEpoch(t *testing.T) { t.Fatal("an uninstalled principal has an epoch") } admin := h.admin(acme) - if _, err := h.svc.Install(h.ctx, admin, acme, "a", "viewer", admin.ID, nil); err != nil { + if _, err := h.svc.Install(h.ctx, admin, acme, "a", "viewer", admin.ID, h.approval(acme, "a"), nil); err != nil { t.Fatal(err) } after, err := h.svc.Seals().PrincipalEpoch(h.ctx, u) diff --git a/libraries/authority-core/go/domain/domain.go b/libraries/authority-core/go/domain/domain.go index ebeb265c..6278ba60 100644 --- a/libraries/authority-core/go/domain/domain.go +++ b/libraries/authority-core/go/domain/domain.go @@ -106,6 +106,8 @@ type Installation struct { AgentRole string PrincipalID string InstalledBy string + // Consent is the last reviewed operation; a later revision invalidates its receipt. + Consent ConsentReceipt } // RoleGrant is one role on one installation held by exactly one person or team. @@ -175,3 +177,16 @@ func CodeOf(err error) (Code, bool) { } return 0, false } + +// ConsentApproval is the reviewed authority offer and caller's operation identity. +type ConsentApproval struct { + TargetID string + Generation, ExpectedRevision uint64 + OperationID, Digest string +} + +// ConsentReceipt binds an unchanged retry to its resulting installation revision. +type ConsentReceipt struct { + OperationID, Intent string + Revision uint64 +} diff --git a/libraries/conformance/go/README.md b/libraries/conformance/go/README.md index b60834bc..7275d76e 100644 --- a/libraries/conformance/go/README.md +++ b/libraries/conformance/go/README.md @@ -154,3 +154,21 @@ declares `owner_aware` on the modules solutions consume. The local authority runs `Run` and `RunSolutionPrincipalTarget` with every `go test ./plugins/registry_test` from `modules/authority/services/local/code`; `Run`'s cases assert the same model the target does. + +## Reviewed installation consent + +`InstallationService/Review` returns the host's immutable target identity, +current generation, installation revision and authority-offer digest. The caller +reviews that offer, supplies a fresh UUID `operation_id`, and sends the unchanged +approval with `Install`. Review does not grant authority. An unchanged successful +retry returns the installation without repeating its install step; changed +inputs, a newer delivery, a changed revision or revocation refuse with a named +`INSTALLATION_APPROVAL_*` reason. A refusal requires a fresh review, not a retry +that silently substitutes current state. + +The lifecycle case reads Review through the published contract, checks missing +and mismatched approvals, preserves approval across acknowledgement, and checks +replay after revocation. The local core additionally rechecks the offer and +administrator in the transaction that acknowledges the install step. Its digest +covers the stored authority declarations, including roles, ceilings and bindings; +it is opaque to callers. Host-owned fixture setup stays on `InstallAsHost`. diff --git a/libraries/conformance/go/go.mod b/libraries/conformance/go/go.mod index 64081225..2f1efc49 100644 --- a/libraries/conformance/go/go.mod +++ b/libraries/conformance/go/go.mod @@ -6,6 +6,7 @@ require ( github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261026082258-3acb2c3acc46 github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261023203940-e720ad2851c7 github.com/codefly-dev/core v0.19.7-0.20261026092055-b23dc13543b2 + github.com/google/uuid v1.6.0 google.golang.org/grpc v1.84.0 google.golang.org/protobuf v1.36.12 ) @@ -37,7 +38,6 @@ require ( github.com/gofrs/flock v0.13.1 // indirect github.com/google/go-github/v89 v89.0.0 // indirect github.com/google/go-querystring v1.2.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect diff --git a/libraries/conformance/go/installation_lifecycle.go b/libraries/conformance/go/installation_lifecycle.go index ab95d049..132662e2 100644 --- a/libraries/conformance/go/installation_lifecycle.go +++ b/libraries/conformance/go/installation_lifecycle.go @@ -2,6 +2,7 @@ package conformance import ( "context" + "github.com/google/uuid" "testing" "time" @@ -64,7 +65,28 @@ func (c *client) installationLifecycle(t *testing.T, ctx context.Context, f Fixt resultCh := make(chan result, 1) installCtx, cancel := context.WithTimeout(adminCtx, 10*time.Second) defer cancel() - request := map[string]any{"organization_id": f.Organization, "target": l.Target, "role": "viewer", "owner_person_id": str(admin, "person_id")} + request := map[string]any{"organization_id": f.Organization, "target": l.Target, "role": "viewer", "owner_person_id": str(admin, "person_id"), "approval": c.reviewInstallation(t, adminCtx, f.Organization, l.Target)} + for _, tc := range []struct { + field string + value any + reason string + }{ + {"target_id", "unreviewed-target", "GENERATION_CHANGED"}, + {"generation", uint64(2), "GENERATION_CHANGED"}, + {"expected_revision", uint64(1), "REVISION_CHANGED"}, + {"digest", "sha256:unreviewed", "DIGEST_CHANGED"}, + } { + bad := clone(request) + approval := clone(request["approval"].(map[string]any)) + approval[tc.field] = tc.value + bad["approval"] = approval + _, err := c.call(adminCtx, "host.installation.v1.InstallationService", "Install", bad) + refused(t, err, codes.FailedPrecondition, "INSTALLATION_APPROVAL_"+tc.reason) + } + missing := clone(request) + delete(missing, "approval") + _, err := c.call(adminCtx, "host.installation.v1.InstallationService", "Install", missing) + refused(t, err, codes.FailedPrecondition, "INSTALLATION_APPROVAL_REQUIRED") go func() { inst, err := c.call(installCtx, "host.installation.v1.InstallationService", "Install", request) resultCh <- result{inst, err} @@ -80,7 +102,11 @@ func (c *client) installationLifecycle(t *testing.T, ctx context.Context, f Fixt } } if pending == nil { - time.Sleep(10 * time.Millisecond) + select { + case early := <-resultCh: + t.Fatalf("install finished before a pending row was visible: %v", early.err) + case <-time.After(10 * time.Millisecond): + } } } if pending == nil { @@ -119,11 +145,14 @@ func (c *client) installationLifecycle(t *testing.T, ctx context.Context, f Fixt if grants := listMsgs(listed, "grants"); len(grants) != 1 || str(grants[0], "team_id") != l.Team || str(grants[0], "role") != "viewer" { t.Fatalf("acknowledged installation was not granted to its team exactly once: %v", grants) } - _, err := c.call(adminCtx, "host.installation.v1.InstallationService", "Install", request) - refused(t, err, codes.PermissionDenied, "already installed") + // Role grants do not change the installation consent revision. + retried := c.must(t, adminCtx, "host.installation.v1.InstallationService", "Install", request) + if str(retried, "id") != id { + t.Fatal("unchanged approval retry created another installation") + } foreignGrant := clone(grant) foreignGrant["organization_id"] = "conformance-foreign-org" - _, err = c.call(adminCtx, "host.installation.v1.InstallationService", expose, foreignGrant) + _, err := c.call(adminCtx, "host.installation.v1.InstallationService", expose, foreignGrant) refused(t, err, codes.PermissionDenied, "organization") }) out, err := issue() @@ -135,10 +164,18 @@ func (c *client) installationLifecycle(t *testing.T, ctx context.Context, f Fixt if err := l.Revoke(ctx, id); err != nil { t.Fatal(err) } - _, err := issue() + _, err := c.call(adminCtx, "host.installation.v1.InstallationService", "Install", request) + refused(t, err, codes.FailedPrecondition, "INSTALLATION_APPROVAL_REVISION_CHANGED") + _, err = issue() refused(t, err, codes.FailedPrecondition, "revoked") _, err = c.call(ctx, "host.authority.v1.AuthorityService", "Check", map[string]any{"token": str(out, "token"), "audience": l.Target}) refused(t, err, codes.PermissionDenied, "") }) }) } + +func (c *client) reviewInstallation(t *testing.T, ctx context.Context, org, target string) map[string]any { + t.Helper() + a := c.must(t, ctx, "host.installation.v1.InstallationService", "Review", map[string]any{"organization_id": org, "target": target}) + return map[string]any{"target_id": str(a, "target_id"), "generation": u64(a, "generation"), "expected_revision": u64(a, "expected_revision"), "digest": str(a, "digest"), "operation_id": uuid.NewString()} +} diff --git a/libraries/conformance/go/solution_principal.go b/libraries/conformance/go/solution_principal.go index 6c00a23c..78d7de27 100644 --- a/libraries/conformance/go/solution_principal.go +++ b/libraries/conformance/go/solution_principal.go @@ -695,7 +695,7 @@ func (sp *principalSuite) ensure(t *testing.T, org string, s SolutionUnderTest) } admin := sp.admin(org) out := sp.c.must(t, sp.as(t, admin), "host.installation.v1.InstallationService", "Install", map[string]any{ - "organization_id": org, "target": s.Solution, "role": "author", "owner_person_id": sp.personID(t, admin)}) + "organization_id": org, "target": s.Solution, "role": "author", "owner_person_id": sp.personID(t, admin), "approval": sp.c.reviewInstallation(t, sp.as(t, admin), org, s.Solution)}) if enumName(out, "status") != "STATUS_ACTIVE" { t.Fatalf("%s at %s is %s; the host's install step must converge for the suite's solutions", s.Solution, org, enumName(out, "status")) } diff --git a/modules/authority/services/local/code/pkg/gen/host/installation/v1/installation.pb.go b/modules/authority/services/local/code/pkg/gen/host/installation/v1/installation.pb.go index 63f94348..2905862a 100644 --- a/modules/authority/services/local/code/pkg/gen/host/installation/v1/installation.pb.go +++ b/modules/authority/services/local/code/pkg/gen/host/installation/v1/installation.pb.go @@ -82,7 +82,7 @@ func (x Installation_Status) Number() protoreflect.EnumNumber { // Deprecated: Use Installation_Status.Descriptor instead. func (Installation_Status) EnumDescriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{8, 0} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{10, 0} } // InstallRequest asks to install one target at one organisation. @@ -96,6 +96,8 @@ type InstallRequest struct { Role string `protobuf:"bytes,3,opt,name=role,proto3" json:"role,omitempty"` // The accountable person; an administrator of the organisation. OwnerPersonId string `protobuf:"bytes,4,opt,name=owner_person_id,json=ownerPersonId,proto3" json:"owner_person_id,omitempty"` + // Required: the exact offer reviewed by the administrator; unchanged on retry. + Approval *ConsentApproval `protobuf:"bytes,5,opt,name=approval,proto3" json:"approval,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -158,6 +160,146 @@ func (x *InstallRequest) GetOwnerPersonId() string { return "" } +func (x *InstallRequest) GetApproval() *ConsentApproval { + if x != nil { + return x.Approval + } + return nil +} + +// ReviewRequest names the offer an administrator intends to review. +type ReviewRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrganizationId string `protobuf:"bytes,1,opt,name=organization_id,json=organizationId,proto3" json:"organization_id,omitempty"` + Target string `protobuf:"bytes,2,opt,name=target,proto3" json:"target,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReviewRequest) Reset() { + *x = ReviewRequest{} + mi := &file_host_installation_v1_installation_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReviewRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReviewRequest) ProtoMessage() {} + +func (x *ReviewRequest) ProtoReflect() protoreflect.Message { + mi := &file_host_installation_v1_installation_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReviewRequest.ProtoReflect.Descriptor instead. +func (*ReviewRequest) Descriptor() ([]byte, []int) { + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{1} +} + +func (x *ReviewRequest) GetOrganizationId() string { + if x != nil { + return x.OrganizationId + } + return "" +} + +func (x *ReviewRequest) GetTarget() string { + if x != nil { + return x.Target + } + return "" +} + +// ConsentApproval binds consent to an immutable target, its reviewed delivery and +// the installation revision (zero if absent). Review leaves operation_id empty; +// the caller supplies a fresh UUID and preserves the entire request on retry. +type ConsentApproval struct { + state protoimpl.MessageState `protogen:"open.v1"` + TargetId string `protobuf:"bytes,1,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` + Generation uint64 `protobuf:"varint,2,opt,name=generation,proto3" json:"generation,omitempty"` + ExpectedRevision uint64 `protobuf:"varint,3,opt,name=expected_revision,json=expectedRevision,proto3" json:"expected_revision,omitempty"` + OperationId string `protobuf:"bytes,4,opt,name=operation_id,json=operationId,proto3" json:"operation_id,omitempty"` + // sha256 digest of the reviewed authority offer. + Digest string `protobuf:"bytes,5,opt,name=digest,proto3" json:"digest,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConsentApproval) Reset() { + *x = ConsentApproval{} + mi := &file_host_installation_v1_installation_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConsentApproval) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConsentApproval) ProtoMessage() {} + +func (x *ConsentApproval) ProtoReflect() protoreflect.Message { + mi := &file_host_installation_v1_installation_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConsentApproval.ProtoReflect.Descriptor instead. +func (*ConsentApproval) Descriptor() ([]byte, []int) { + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{2} +} + +func (x *ConsentApproval) GetTargetId() string { + if x != nil { + return x.TargetId + } + return "" +} + +func (x *ConsentApproval) GetGeneration() uint64 { + if x != nil { + return x.Generation + } + return 0 +} + +func (x *ConsentApproval) GetExpectedRevision() uint64 { + if x != nil { + return x.ExpectedRevision + } + return 0 +} + +func (x *ConsentApproval) GetOperationId() string { + if x != nil { + return x.OperationId + } + return "" +} + +func (x *ConsentApproval) GetDigest() string { + if x != nil { + return x.Digest + } + return "" +} + // GrantRoleRequest grants one role to exactly one person or team. type GrantRoleRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -180,7 +322,7 @@ type GrantRoleRequest struct { func (x *GrantRoleRequest) Reset() { *x = GrantRoleRequest{} - mi := &file_host_installation_v1_installation_proto_msgTypes[1] + mi := &file_host_installation_v1_installation_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -192,7 +334,7 @@ func (x *GrantRoleRequest) String() string { func (*GrantRoleRequest) ProtoMessage() {} func (x *GrantRoleRequest) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[1] + mi := &file_host_installation_v1_installation_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -205,7 +347,7 @@ func (x *GrantRoleRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GrantRoleRequest.ProtoReflect.Descriptor instead. func (*GrantRoleRequest) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{1} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{3} } func (x *GrantRoleRequest) GetOrganizationId() string { @@ -262,7 +404,7 @@ type RevokeRoleRequest struct { func (x *RevokeRoleRequest) Reset() { *x = RevokeRoleRequest{} - mi := &file_host_installation_v1_installation_proto_msgTypes[2] + mi := &file_host_installation_v1_installation_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -274,7 +416,7 @@ func (x *RevokeRoleRequest) String() string { func (*RevokeRoleRequest) ProtoMessage() {} func (x *RevokeRoleRequest) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[2] + mi := &file_host_installation_v1_installation_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -287,7 +429,7 @@ func (x *RevokeRoleRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeRoleRequest.ProtoReflect.Descriptor instead. func (*RevokeRoleRequest) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{2} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{4} } func (x *RevokeRoleRequest) GetOrganizationId() string { @@ -338,7 +480,7 @@ type ListRoleGrantsRequest struct { func (x *ListRoleGrantsRequest) Reset() { *x = ListRoleGrantsRequest{} - mi := &file_host_installation_v1_installation_proto_msgTypes[3] + mi := &file_host_installation_v1_installation_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -350,7 +492,7 @@ func (x *ListRoleGrantsRequest) String() string { func (*ListRoleGrantsRequest) ProtoMessage() {} func (x *ListRoleGrantsRequest) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[3] + mi := &file_host_installation_v1_installation_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -363,7 +505,7 @@ func (x *ListRoleGrantsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoleGrantsRequest.ProtoReflect.Descriptor instead. func (*ListRoleGrantsRequest) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{3} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{5} } func (x *ListRoleGrantsRequest) GetOrganizationId() string { @@ -391,7 +533,7 @@ type ListRoleGrantsResponse struct { func (x *ListRoleGrantsResponse) Reset() { *x = ListRoleGrantsResponse{} - mi := &file_host_installation_v1_installation_proto_msgTypes[4] + mi := &file_host_installation_v1_installation_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -403,7 +545,7 @@ func (x *ListRoleGrantsResponse) String() string { func (*ListRoleGrantsResponse) ProtoMessage() {} func (x *ListRoleGrantsResponse) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[4] + mi := &file_host_installation_v1_installation_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -416,7 +558,7 @@ func (x *ListRoleGrantsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoleGrantsResponse.ProtoReflect.Descriptor instead. func (*ListRoleGrantsResponse) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{4} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{6} } func (x *ListRoleGrantsResponse) GetGrants() []*RoleGrant { @@ -441,7 +583,7 @@ type RoleGrant struct { func (x *RoleGrant) Reset() { *x = RoleGrant{} - mi := &file_host_installation_v1_installation_proto_msgTypes[5] + mi := &file_host_installation_v1_installation_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -453,7 +595,7 @@ func (x *RoleGrant) String() string { func (*RoleGrant) ProtoMessage() {} func (x *RoleGrant) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[5] + mi := &file_host_installation_v1_installation_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -466,7 +608,7 @@ func (x *RoleGrant) ProtoReflect() protoreflect.Message { // Deprecated: Use RoleGrant.ProtoReflect.Descriptor instead. func (*RoleGrant) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{5} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{7} } func (x *RoleGrant) GetRole() string { @@ -501,7 +643,7 @@ type ListRequest struct { func (x *ListRequest) Reset() { *x = ListRequest{} - mi := &file_host_installation_v1_installation_proto_msgTypes[6] + mi := &file_host_installation_v1_installation_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -513,7 +655,7 @@ func (x *ListRequest) String() string { func (*ListRequest) ProtoMessage() {} func (x *ListRequest) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[6] + mi := &file_host_installation_v1_installation_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -526,7 +668,7 @@ func (x *ListRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRequest.ProtoReflect.Descriptor instead. func (*ListRequest) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{6} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{8} } func (x *ListRequest) GetOrganizationId() string { @@ -547,7 +689,7 @@ type ListResponse struct { func (x *ListResponse) Reset() { *x = ListResponse{} - mi := &file_host_installation_v1_installation_proto_msgTypes[7] + mi := &file_host_installation_v1_installation_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -559,7 +701,7 @@ func (x *ListResponse) String() string { func (*ListResponse) ProtoMessage() {} func (x *ListResponse) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[7] + mi := &file_host_installation_v1_installation_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -572,7 +714,7 @@ func (x *ListResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListResponse.ProtoReflect.Descriptor instead. func (*ListResponse) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{7} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{9} } func (x *ListResponse) GetInstallations() []*Installation { @@ -615,7 +757,7 @@ type Installation struct { func (x *Installation) Reset() { *x = Installation{} - mi := &file_host_installation_v1_installation_proto_msgTypes[8] + mi := &file_host_installation_v1_installation_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -627,7 +769,7 @@ func (x *Installation) String() string { func (*Installation) ProtoMessage() {} func (x *Installation) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[8] + mi := &file_host_installation_v1_installation_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -640,7 +782,7 @@ func (x *Installation) ProtoReflect() protoreflect.Message { // Deprecated: Use Installation.ProtoReflect.Descriptor instead. func (*Installation) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{8} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{10} } func (x *Installation) GetId() string { @@ -717,7 +859,7 @@ type InstallationNotice struct { func (x *InstallationNotice) Reset() { *x = InstallationNotice{} - mi := &file_host_installation_v1_installation_proto_msgTypes[9] + mi := &file_host_installation_v1_installation_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -729,7 +871,7 @@ func (x *InstallationNotice) String() string { func (*InstallationNotice) ProtoMessage() {} func (x *InstallationNotice) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[9] + mi := &file_host_installation_v1_installation_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -742,7 +884,7 @@ func (x *InstallationNotice) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallationNotice.ProtoReflect.Descriptor instead. func (*InstallationNotice) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{9} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{11} } func (x *InstallationNotice) GetInstallation() *Installation { @@ -761,7 +903,7 @@ type InstallationAck struct { func (x *InstallationAck) Reset() { *x = InstallationAck{} - mi := &file_host_installation_v1_installation_proto_msgTypes[10] + mi := &file_host_installation_v1_installation_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -773,7 +915,7 @@ func (x *InstallationAck) String() string { func (*InstallationAck) ProtoMessage() {} func (x *InstallationAck) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[10] + mi := &file_host_installation_v1_installation_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -786,19 +928,31 @@ func (x *InstallationAck) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallationAck.ProtoReflect.Descriptor instead. func (*InstallationAck) Descriptor() ([]byte, []int) { - return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{10} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{12} } var File_host_installation_v1_installation_proto protoreflect.FileDescriptor const file_host_installation_v1_installation_proto_rawDesc = "" + "\n" + - "'host/installation/v1/installation.proto\x12\x14host.installation.v1\"\x8d\x01\n" + + "'host/installation/v1/installation.proto\x12\x14host.installation.v1\"\xd0\x01\n" + "\x0eInstallRequest\x12'\n" + "\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\x12\x16\n" + "\x06target\x18\x02 \x01(\tR\x06target\x12\x12\n" + "\x04role\x18\x03 \x01(\tR\x04role\x12&\n" + - "\x0fowner_person_id\x18\x04 \x01(\tR\rownerPersonId\"\xae\x01\n" + + "\x0fowner_person_id\x18\x04 \x01(\tR\rownerPersonId\x12A\n" + + "\bapproval\x18\x05 \x01(\v2%.host.installation.v1.ConsentApprovalR\bapproval\"P\n" + + "\rReviewRequest\x12'\n" + + "\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\x12\x16\n" + + "\x06target\x18\x02 \x01(\tR\x06target\"\xb6\x01\n" + + "\x0fConsentApproval\x12\x1b\n" + + "\ttarget_id\x18\x01 \x01(\tR\btargetId\x12\x1e\n" + + "\n" + + "generation\x18\x02 \x01(\x04R\n" + + "generation\x12+\n" + + "\x11expected_revision\x18\x03 \x01(\x04R\x10expectedRevision\x12!\n" + + "\foperation_id\x18\x04 \x01(\tR\voperationId\x12\x16\n" + + "\x06digest\x18\x05 \x01(\tR\x06digest\"\xae\x01\n" + "\x10GrantRoleRequest\x12'\n" + "\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\x12'\n" + "\x0finstallation_id\x18\x02 \x01(\tR\x0einstallationId\x12\x12\n" + @@ -842,9 +996,10 @@ const file_host_installation_v1_installation_proto_rawDesc = "" + "\x0eSTATUS_REVOKED\x10\x03J\x04\b\a\x10\bJ\x04\b\b\x10\tR\bteam_idsR\x12agent_principal_id\"\\\n" + "\x12InstallationNotice\x12F\n" + "\finstallation\x18\x01 \x01(\v2\".host.installation.v1.InstallationR\finstallation\"\x11\n" + - "\x0fInstallationAck2\xda\x03\n" + + "\x0fInstallationAck2\xb0\x04\n" + "\x13InstallationService\x12S\n" + - "\aInstall\x12$.host.installation.v1.InstallRequest\x1a\".host.installation.v1.Installation\x12W\n" + + "\aInstall\x12$.host.installation.v1.InstallRequest\x1a\".host.installation.v1.Installation\x12T\n" + + "\x06Review\x12#.host.installation.v1.ReviewRequest\x1a%.host.installation.v1.ConsentApproval\x12W\n" + "\tGrantRole\x12&.host.installation.v1.GrantRoleRequest\x1a\".host.installation.v1.Installation\x12Y\n" + "\n" + "RevokeRole\x12'.host.installation.v1.RevokeRoleRequest\x1a\".host.installation.v1.Installation\x12k\n" + @@ -866,43 +1021,48 @@ func file_host_installation_v1_installation_proto_rawDescGZIP() []byte { } var file_host_installation_v1_installation_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_host_installation_v1_installation_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_host_installation_v1_installation_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_host_installation_v1_installation_proto_goTypes = []any{ (Installation_Status)(0), // 0: host.installation.v1.Installation.Status (*InstallRequest)(nil), // 1: host.installation.v1.InstallRequest - (*GrantRoleRequest)(nil), // 2: host.installation.v1.GrantRoleRequest - (*RevokeRoleRequest)(nil), // 3: host.installation.v1.RevokeRoleRequest - (*ListRoleGrantsRequest)(nil), // 4: host.installation.v1.ListRoleGrantsRequest - (*ListRoleGrantsResponse)(nil), // 5: host.installation.v1.ListRoleGrantsResponse - (*RoleGrant)(nil), // 6: host.installation.v1.RoleGrant - (*ListRequest)(nil), // 7: host.installation.v1.ListRequest - (*ListResponse)(nil), // 8: host.installation.v1.ListResponse - (*Installation)(nil), // 9: host.installation.v1.Installation - (*InstallationNotice)(nil), // 10: host.installation.v1.InstallationNotice - (*InstallationAck)(nil), // 11: host.installation.v1.InstallationAck + (*ReviewRequest)(nil), // 2: host.installation.v1.ReviewRequest + (*ConsentApproval)(nil), // 3: host.installation.v1.ConsentApproval + (*GrantRoleRequest)(nil), // 4: host.installation.v1.GrantRoleRequest + (*RevokeRoleRequest)(nil), // 5: host.installation.v1.RevokeRoleRequest + (*ListRoleGrantsRequest)(nil), // 6: host.installation.v1.ListRoleGrantsRequest + (*ListRoleGrantsResponse)(nil), // 7: host.installation.v1.ListRoleGrantsResponse + (*RoleGrant)(nil), // 8: host.installation.v1.RoleGrant + (*ListRequest)(nil), // 9: host.installation.v1.ListRequest + (*ListResponse)(nil), // 10: host.installation.v1.ListResponse + (*Installation)(nil), // 11: host.installation.v1.Installation + (*InstallationNotice)(nil), // 12: host.installation.v1.InstallationNotice + (*InstallationAck)(nil), // 13: host.installation.v1.InstallationAck } var file_host_installation_v1_installation_proto_depIdxs = []int32{ - 6, // 0: host.installation.v1.ListRoleGrantsResponse.grants:type_name -> host.installation.v1.RoleGrant - 9, // 1: host.installation.v1.ListResponse.installations:type_name -> host.installation.v1.Installation - 0, // 2: host.installation.v1.Installation.status:type_name -> host.installation.v1.Installation.Status - 9, // 3: host.installation.v1.InstallationNotice.installation:type_name -> host.installation.v1.Installation - 1, // 4: host.installation.v1.InstallationService.Install:input_type -> host.installation.v1.InstallRequest - 2, // 5: host.installation.v1.InstallationService.GrantRole:input_type -> host.installation.v1.GrantRoleRequest - 3, // 6: host.installation.v1.InstallationService.RevokeRole:input_type -> host.installation.v1.RevokeRoleRequest - 4, // 7: host.installation.v1.InstallationService.ListRoleGrants:input_type -> host.installation.v1.ListRoleGrantsRequest - 7, // 8: host.installation.v1.InstallationService.List:input_type -> host.installation.v1.ListRequest - 10, // 9: host.installation.v1.InstallableService.Install:input_type -> host.installation.v1.InstallationNotice - 9, // 10: host.installation.v1.InstallationService.Install:output_type -> host.installation.v1.Installation - 9, // 11: host.installation.v1.InstallationService.GrantRole:output_type -> host.installation.v1.Installation - 9, // 12: host.installation.v1.InstallationService.RevokeRole:output_type -> host.installation.v1.Installation - 5, // 13: host.installation.v1.InstallationService.ListRoleGrants:output_type -> host.installation.v1.ListRoleGrantsResponse - 8, // 14: host.installation.v1.InstallationService.List:output_type -> host.installation.v1.ListResponse - 11, // 15: host.installation.v1.InstallableService.Install:output_type -> host.installation.v1.InstallationAck - 10, // [10:16] is the sub-list for method output_type - 4, // [4:10] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 3, // 0: host.installation.v1.InstallRequest.approval:type_name -> host.installation.v1.ConsentApproval + 8, // 1: host.installation.v1.ListRoleGrantsResponse.grants:type_name -> host.installation.v1.RoleGrant + 11, // 2: host.installation.v1.ListResponse.installations:type_name -> host.installation.v1.Installation + 0, // 3: host.installation.v1.Installation.status:type_name -> host.installation.v1.Installation.Status + 11, // 4: host.installation.v1.InstallationNotice.installation:type_name -> host.installation.v1.Installation + 1, // 5: host.installation.v1.InstallationService.Install:input_type -> host.installation.v1.InstallRequest + 2, // 6: host.installation.v1.InstallationService.Review:input_type -> host.installation.v1.ReviewRequest + 4, // 7: host.installation.v1.InstallationService.GrantRole:input_type -> host.installation.v1.GrantRoleRequest + 5, // 8: host.installation.v1.InstallationService.RevokeRole:input_type -> host.installation.v1.RevokeRoleRequest + 6, // 9: host.installation.v1.InstallationService.ListRoleGrants:input_type -> host.installation.v1.ListRoleGrantsRequest + 9, // 10: host.installation.v1.InstallationService.List:input_type -> host.installation.v1.ListRequest + 12, // 11: host.installation.v1.InstallableService.Install:input_type -> host.installation.v1.InstallationNotice + 11, // 12: host.installation.v1.InstallationService.Install:output_type -> host.installation.v1.Installation + 3, // 13: host.installation.v1.InstallationService.Review:output_type -> host.installation.v1.ConsentApproval + 11, // 14: host.installation.v1.InstallationService.GrantRole:output_type -> host.installation.v1.Installation + 11, // 15: host.installation.v1.InstallationService.RevokeRole:output_type -> host.installation.v1.Installation + 7, // 16: host.installation.v1.InstallationService.ListRoleGrants:output_type -> host.installation.v1.ListRoleGrantsResponse + 10, // 17: host.installation.v1.InstallationService.List:output_type -> host.installation.v1.ListResponse + 13, // 18: host.installation.v1.InstallableService.Install:output_type -> host.installation.v1.InstallationAck + 12, // [12:19] is the sub-list for method output_type + 5, // [5:12] 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_host_installation_v1_installation_proto_init() } @@ -916,7 +1076,7 @@ func file_host_installation_v1_installation_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_host_installation_v1_installation_proto_rawDesc), len(file_host_installation_v1_installation_proto_rawDesc)), NumEnums: 1, - NumMessages: 11, + NumMessages: 13, NumExtensions: 0, NumServices: 2, }, diff --git a/modules/authority/services/local/code/pkg/gen/host/installation/v1/installation_grpc.pb.go b/modules/authority/services/local/code/pkg/gen/host/installation/v1/installation_grpc.pb.go index d6a5f8f9..8e7d8bce 100644 --- a/modules/authority/services/local/code/pkg/gen/host/installation/v1/installation_grpc.pb.go +++ b/modules/authority/services/local/code/pkg/gen/host/installation/v1/installation_grpc.pb.go @@ -27,6 +27,7 @@ const _ = grpc.SupportPackageIsVersion9 const ( InstallationService_Install_FullMethodName = "/host.installation.v1.InstallationService/Install" + InstallationService_Review_FullMethodName = "/host.installation.v1.InstallationService/Review" InstallationService_GrantRole_FullMethodName = "/host.installation.v1.InstallationService/GrantRole" InstallationService_RevokeRole_FullMethodName = "/host.installation.v1.InstallationService/RevokeRole" InstallationService_ListRoleGrants_FullMethodName = "/host.installation.v1.InstallationService/ListRoleGrants" @@ -42,6 +43,8 @@ type InstallationServiceClient interface { // Install composes an installation: its agent principal, its root scope and // the row, pending until the target acknowledges its install step. Install(ctx context.Context, in *InstallRequest, opts ...grpc.CallOption) (*Installation, error) + // Review reads the current offer and installation revision without granting consent. + Review(ctx context.Context, in *ReviewRequest, opts ...grpc.CallOption) (*ConsentApproval, error) // GrantRole grants a person or a team one of the solution's roles on the // installation: the access. Idempotent; it leaves existing capabilities // valid. An administrator of the installation's organization, or a holder @@ -77,6 +80,16 @@ func (c *installationServiceClient) Install(ctx context.Context, in *InstallRequ return out, nil } +func (c *installationServiceClient) Review(ctx context.Context, in *ReviewRequest, opts ...grpc.CallOption) (*ConsentApproval, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ConsentApproval) + err := c.cc.Invoke(ctx, InstallationService_Review_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *installationServiceClient) GrantRole(ctx context.Context, in *GrantRoleRequest, opts ...grpc.CallOption) (*Installation, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Installation) @@ -126,6 +139,8 @@ type InstallationServiceServer interface { // Install composes an installation: its agent principal, its root scope and // the row, pending until the target acknowledges its install step. Install(context.Context, *InstallRequest) (*Installation, error) + // Review reads the current offer and installation revision without granting consent. + Review(context.Context, *ReviewRequest) (*ConsentApproval, error) // GrantRole grants a person or a team one of the solution's roles on the // installation: the access. Idempotent; it leaves existing capabilities // valid. An administrator of the installation's organization, or a holder @@ -154,6 +169,9 @@ type UnimplementedInstallationServiceServer struct{} func (UnimplementedInstallationServiceServer) Install(context.Context, *InstallRequest) (*Installation, error) { return nil, status.Error(codes.Unimplemented, "method Install not implemented") } +func (UnimplementedInstallationServiceServer) Review(context.Context, *ReviewRequest) (*ConsentApproval, error) { + return nil, status.Error(codes.Unimplemented, "method Review not implemented") +} func (UnimplementedInstallationServiceServer) GrantRole(context.Context, *GrantRoleRequest) (*Installation, error) { return nil, status.Error(codes.Unimplemented, "method GrantRole not implemented") } @@ -205,6 +223,24 @@ func _InstallationService_Install_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _InstallationService_Review_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReviewRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(InstallationServiceServer).Review(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: InstallationService_Review_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(InstallationServiceServer).Review(ctx, req.(*ReviewRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _InstallationService_GrantRole_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GrantRoleRequest) if err := dec(in); err != nil { @@ -288,6 +324,10 @@ var InstallationService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Install", Handler: _InstallationService_Install_Handler, }, + { + MethodName: "Review", + Handler: _InstallationService_Review_Handler, + }, { MethodName: "GrantRole", Handler: _InstallationService_GrantRole_Handler, diff --git a/modules/authority/services/local/code/pkg/gen/host/installation/v1/installationv1connect/installation.connect.go b/modules/authority/services/local/code/pkg/gen/host/installation/v1/installationv1connect/installation.connect.go index 71008349..1298c4e1 100644 --- a/modules/authority/services/local/code/pkg/gen/host/installation/v1/installationv1connect/installation.connect.go +++ b/modules/authority/services/local/code/pkg/gen/host/installation/v1/installationv1connect/installation.connect.go @@ -44,6 +44,9 @@ const ( // InstallationServiceInstallProcedure is the fully-qualified name of the InstallationService's // Install RPC. InstallationServiceInstallProcedure = "/host.installation.v1.InstallationService/Install" + // InstallationServiceReviewProcedure is the fully-qualified name of the InstallationService's + // Review RPC. + InstallationServiceReviewProcedure = "/host.installation.v1.InstallationService/Review" // InstallationServiceGrantRoleProcedure is the fully-qualified name of the InstallationService's // GrantRole RPC. InstallationServiceGrantRoleProcedure = "/host.installation.v1.InstallationService/GrantRole" @@ -66,6 +69,8 @@ type InstallationServiceClient interface { // Install composes an installation: its agent principal, its root scope and // the row, pending until the target acknowledges its install step. Install(context.Context, *connect.Request[v1.InstallRequest]) (*connect.Response[v1.Installation], error) + // Review reads the current offer and installation revision without granting consent. + Review(context.Context, *connect.Request[v1.ReviewRequest]) (*connect.Response[v1.ConsentApproval], error) // GrantRole grants a person or a team one of the solution's roles on the // installation: the access. Idempotent; it leaves existing capabilities // valid. An administrator of the installation's organization, or a holder @@ -100,6 +105,12 @@ func NewInstallationServiceClient(httpClient connect.HTTPClient, baseURL string, connect.WithSchema(installationServiceMethods.ByName("Install")), connect.WithClientOptions(opts...), ), + review: connect.NewClient[v1.ReviewRequest, v1.ConsentApproval]( + httpClient, + baseURL+InstallationServiceReviewProcedure, + connect.WithSchema(installationServiceMethods.ByName("Review")), + connect.WithClientOptions(opts...), + ), grantRole: connect.NewClient[v1.GrantRoleRequest, v1.Installation]( httpClient, baseURL+InstallationServiceGrantRoleProcedure, @@ -130,6 +141,7 @@ func NewInstallationServiceClient(httpClient connect.HTTPClient, baseURL string, // installationServiceClient implements InstallationServiceClient. type installationServiceClient struct { install *connect.Client[v1.InstallRequest, v1.Installation] + review *connect.Client[v1.ReviewRequest, v1.ConsentApproval] grantRole *connect.Client[v1.GrantRoleRequest, v1.Installation] revokeRole *connect.Client[v1.RevokeRoleRequest, v1.Installation] listRoleGrants *connect.Client[v1.ListRoleGrantsRequest, v1.ListRoleGrantsResponse] @@ -141,6 +153,11 @@ func (c *installationServiceClient) Install(ctx context.Context, req *connect.Re return c.install.CallUnary(ctx, req) } +// Review calls host.installation.v1.InstallationService.Review. +func (c *installationServiceClient) Review(ctx context.Context, req *connect.Request[v1.ReviewRequest]) (*connect.Response[v1.ConsentApproval], error) { + return c.review.CallUnary(ctx, req) +} + // GrantRole calls host.installation.v1.InstallationService.GrantRole. func (c *installationServiceClient) GrantRole(ctx context.Context, req *connect.Request[v1.GrantRoleRequest]) (*connect.Response[v1.Installation], error) { return c.grantRole.CallUnary(ctx, req) @@ -167,6 +184,8 @@ type InstallationServiceHandler interface { // Install composes an installation: its agent principal, its root scope and // the row, pending until the target acknowledges its install step. Install(context.Context, *connect.Request[v1.InstallRequest]) (*connect.Response[v1.Installation], error) + // Review reads the current offer and installation revision without granting consent. + Review(context.Context, *connect.Request[v1.ReviewRequest]) (*connect.Response[v1.ConsentApproval], error) // GrantRole grants a person or a team one of the solution's roles on the // installation: the access. Idempotent; it leaves existing capabilities // valid. An administrator of the installation's organization, or a holder @@ -197,6 +216,12 @@ func NewInstallationServiceHandler(svc InstallationServiceHandler, opts ...conne connect.WithSchema(installationServiceMethods.ByName("Install")), connect.WithHandlerOptions(opts...), ) + installationServiceReviewHandler := connect.NewUnaryHandler( + InstallationServiceReviewProcedure, + svc.Review, + connect.WithSchema(installationServiceMethods.ByName("Review")), + connect.WithHandlerOptions(opts...), + ) installationServiceGrantRoleHandler := connect.NewUnaryHandler( InstallationServiceGrantRoleProcedure, svc.GrantRole, @@ -225,6 +250,8 @@ func NewInstallationServiceHandler(svc InstallationServiceHandler, opts ...conne switch r.URL.Path { case InstallationServiceInstallProcedure: installationServiceInstallHandler.ServeHTTP(w, r) + case InstallationServiceReviewProcedure: + installationServiceReviewHandler.ServeHTTP(w, r) case InstallationServiceGrantRoleProcedure: installationServiceGrantRoleHandler.ServeHTTP(w, r) case InstallationServiceRevokeRoleProcedure: @@ -246,6 +273,10 @@ func (UnimplementedInstallationServiceHandler) Install(context.Context, *connect return nil, connect.NewError(connect.CodeUnimplemented, errors.New("host.installation.v1.InstallationService.Install is not implemented")) } +func (UnimplementedInstallationServiceHandler) Review(context.Context, *connect.Request[v1.ReviewRequest]) (*connect.Response[v1.ConsentApproval], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("host.installation.v1.InstallationService.Review is not implemented")) +} + func (UnimplementedInstallationServiceHandler) GrantRole(context.Context, *connect.Request[v1.GrantRoleRequest]) (*connect.Response[v1.Installation], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("host.installation.v1.InstallationService.GrantRole is not implemented")) } diff --git a/modules/authority/services/local/code/plugins/installation/plugin.go b/modules/authority/services/local/code/plugins/installation/plugin.go index dbb59532..9bba76da 100644 --- a/modules/authority/services/local/code/plugins/installation/plugin.go +++ b/modules/authority/services/local/code/plugins/installation/plugin.go @@ -11,6 +11,7 @@ import ( "slices" "github.com/codefly-dev/authority/libraries/authority-core/go/authority" + "github.com/codefly-dev/authority/libraries/authority-core/go/domain" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" @@ -51,11 +52,7 @@ func (p *Plugin) Install(ctx context.Context, req *installationv1.InstallRequest if err != nil { return nil, authorityplugin.Status(err) } - installedBy := req.GetOwnerPersonId() - if installedBy == "" { - installedBy = caller.ID - } - inst, err := p.State.Service().Install(ctx, caller, req.GetOrganizationId(), req.GetTarget(), req.GetRole(), installedBy, p.installStep()) + inst, err := p.State.Service().Install(ctx, caller, req.GetOrganizationId(), req.GetTarget(), req.GetRole(), req.GetOwnerPersonId(), approvalFromWire(req.GetApproval()), p.installStep()) if err != nil { return nil, authorityplugin.Status(err) } @@ -152,3 +149,23 @@ func (p *Plugin) wire(i state.Installation) *installationv1.Installation { // New is the registry's constructor: the plugin over what Configure wired. func New() framework.Plugin { return &Plugin{State: wiring.State()} } + +func approvalFromWire(a *installationv1.ConsentApproval) *domain.ConsentApproval { + if a == nil { + return nil + } + return &domain.ConsentApproval{TargetID: a.GetTargetId(), Generation: a.GetGeneration(), ExpectedRevision: a.GetExpectedRevision(), OperationID: a.GetOperationId(), Digest: a.GetDigest()} +} + +// Review reads the exact authority offer without recording consent. +func (p *Plugin) Review(ctx context.Context, req *installationv1.ReviewRequest) (*installationv1.ConsentApproval, error) { + caller, err := p.caller(ctx) + if err != nil { + return nil, authorityplugin.Status(err) + } + a, err := p.State.Service().Review(ctx, caller, req.GetOrganizationId(), req.GetTarget()) + if err != nil { + return nil, authorityplugin.Status(err) + } + return &installationv1.ConsentApproval{TargetId: a.TargetID, Generation: a.Generation, ExpectedRevision: a.ExpectedRevision, Digest: a.Digest}, nil +} diff --git a/modules/authority/services/local/code/plugins/registry_test/unauthenticated_install_test.go b/modules/authority/services/local/code/plugins/registry_test/unauthenticated_install_test.go index cdbe4e54..18ccb4fa 100644 --- a/modules/authority/services/local/code/plugins/registry_test/unauthenticated_install_test.go +++ b/modules/authority/services/local/code/plugins/registry_test/unauthenticated_install_test.go @@ -2,20 +2,19 @@ package registry_test import ( "context" - "net" + "github.com/google/uuid" + "net/http/httptest" "sync/atomic" "testing" + "connectrpc.com/connect" + "connectrpc.com/vanguard/vanguardgrpc" "gateway/pkg/business/state" "gateway/pkg/business/testkit" installationv1 "gateway/pkg/gen/host/installation/v1" + "gateway/pkg/gen/host/installation/v1/installationv1connect" "gateway/plugins/installation" "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - "google.golang.org/grpc/test/bufconn" ) func TestUnauthenticatedRequestCannotReachHostInstallation(t *testing.T) { @@ -27,26 +26,22 @@ func TestUnauthenticatedRequestCannotReachHostInstallation(t *testing.T) { k.State.OnInstall(func(context.Context, state.Installation) error { steps.Add(1); return nil }) server := grpc.NewServer() (&installation.Plugin{State: k.State}).RegisterGRPC(server) - listener := bufconn.Listen(1 << 20) - go func() { _ = server.Serve(listener) }() - t.Cleanup(server.Stop) - conn, err := grpc.NewClient("passthrough:///bufconn", - grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return listener.Dial() }), - grpc.WithTransportCredentials(insecure.NewCredentials())) + handler, err := vanguardgrpc.NewTranscoder(server) if err != nil { t.Fatal(err) } - t.Cleanup(func() { _ = conn.Close() }) - // This contract generates a gRPC client, not a Connect client. - client := installationv1.NewInstallationServiceClient(conn) + web := httptest.NewServer(handler) + t.Cleanup(web.Close) + t.Cleanup(server.Stop) + client := installationv1connect.NewInstallationServiceClient(web.Client(), web.URL) req := &installationv1.InstallRequest{OrganizationId: "acme", Target: "proposals", Role: "viewer", OwnerPersonId: "alex"} for _, bearer := range []string{"", "not-a-session"} { - ctx := t.Context() + request := connect.NewRequest(req) if bearer != "" { - ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("authorization", "Bearer "+bearer)) + request.Header().Set("Authorization", "Bearer "+bearer) } - _, err := client.Install(ctx, req) - if status.Code(err) != codes.Unauthenticated { + _, err := client.Install(t.Context(), request) + if connect.CodeOf(err) != connect.CodeUnauthenticated { t.Fatalf("bearer %q: %v", bearer, err) } if _, exists := k.State.Installation(state.InstallationID("acme", "proposals")); exists { @@ -61,10 +56,31 @@ func TestUnauthenticatedRequestCannotReachHostInstallation(t *testing.T) { if err != nil { t.Fatal(err) } - ctx := metadata.NewOutgoingContext(t.Context(), metadata.Pairs("authorization", "Bearer "+sess.ID)) - if _, err := client.Install(ctx, req); err != nil { + review := connect.NewRequest(&installationv1.ReviewRequest{OrganizationId: req.OrganizationId, Target: req.Target}) + review.Header().Set("Authorization", "Bearer "+sess.ID) + offered, err := client.Review(t.Context(), review) + if err != nil { t.Fatal(err) } + req.Approval = offered.Msg + req.Approval.OperationId = uuid.NewString() + install := func() (*connect.Response[installationv1.Installation], error) { + request := connect.NewRequest(req) + request.Header().Set("Authorization", "Bearer "+sess.ID) + return client.Install(t.Context(), request) + } + first, err := install() + if err != nil { + t.Fatal(err) + } + retry, err := install() + if err != nil || retry.Msg.GetId() != first.Msg.GetId() { + t.Fatalf("unchanged retry: %v", err) + } + req.Role = "installer" + if _, err := install(); connect.CodeOf(err) != connect.CodeFailedPrecondition { + t.Fatalf("changed request: %v", err) + } if steps.Load() != 1 { t.Fatalf("authenticated install steps: %d", steps.Load()) } diff --git a/modules/authority/services/local/proto/host/installation/v1/installation.proto b/modules/authority/services/local/proto/host/installation/v1/installation.proto index 0898c683..909c1e87 100644 --- a/modules/authority/services/local/proto/host/installation/v1/installation.proto +++ b/modules/authority/services/local/proto/host/installation/v1/installation.proto @@ -13,6 +13,8 @@ service InstallationService { // Install composes an installation: its agent principal, its root scope and // the row, pending until the target acknowledges its install step. rpc Install(InstallRequest) returns (Installation); + // Review reads the current offer and installation revision without granting consent. + rpc Review(ReviewRequest) returns (ConsentApproval); // GrantRole grants a person or a team one of the solution's roles on the // installation: the access. Idempotent; it leaves existing capabilities // valid. An administrator of the installation's organization, or a holder @@ -47,6 +49,26 @@ message InstallRequest { string role = 3; // The accountable person; an administrator of the organisation. string owner_person_id = 4; + // Required: the exact offer reviewed by the administrator; unchanged on retry. + ConsentApproval approval = 5; +} + +// ReviewRequest names the offer an administrator intends to review. +message ReviewRequest { + string organization_id = 1; + string target = 2; +} + +// ConsentApproval binds consent to an immutable target, its reviewed delivery and +// the installation revision (zero if absent). Review leaves operation_id empty; +// the caller supplies a fresh UUID and preserves the entire request on retry. +message ConsentApproval { + string target_id = 1; + uint64 generation = 2; + uint64 expected_revision = 3; + string operation_id = 4; + // sha256 digest of the reviewed authority offer. + string digest = 5; } // GrantRoleRequest grants one role to exactly one person or team. diff --git a/tools/noproxygate/generated.json b/tools/noproxygate/generated.json index dbdebe55..455dbdd8 100644 --- a/tools/noproxygate/generated.json +++ b/tools/noproxygate/generated.json @@ -8,8 +8,8 @@ "libraries/authority-client/go/gen/host/authority/v1/work_context.pb.go": "c53b6b47355ee9a502441173931b3b809e510054168aa0cc2984b5152e771bf1", "libraries/authority-client/go/gen/host/composition/v1/composition.pb.go": "0a2eedf2b7cec24d9006835e5d2718be3c139ad6d9f0e8da9d925310613cda1c", "libraries/authority-client/go/gen/host/composition/v1/composition_grpc.pb.go": "acd95033d684741c23c74eb9f05021ebe487b9ab9e9cd81d549d44ab6c6d6162", - "libraries/authority-client/go/gen/host/installation/v1/installation.pb.go": "16d623a917b0f7a2ffc8c6f5c9534f5685ce69e34e62da002eae8718f2e45677", - "libraries/authority-client/go/gen/host/installation/v1/installation_grpc.pb.go": "93a2e576502a08ae05e69bb1723c5259bb37c4c5e71baf4711d7da3dc46b187d", + "libraries/authority-client/go/gen/host/installation/v1/installation.pb.go": "c5cc4de93487e6bd1aa2a92457102e86abecd97b51fa00c427a297180f60b0b9", + "libraries/authority-client/go/gen/host/installation/v1/installation_grpc.pb.go": "b698a9175b07ec08ef22d80386cb4a9f4b1f8fd47d9d144a246cbe528e02394a", "libraries/authority-client/go/gen/host/session/v1/session.pb.go": "509893bfa569a263d9c798672cc47760c4fc2b44d576f834008798048e54b926", "libraries/authority-client/go/gen/host/session/v1/session_grpc.pb.go": "c4177166bf12cacb6e171beb4f739e730b7f005d559669cbab8be51598796468", "libraries/hop/go/gen/authority/hop/v1/policy.pb.go": "24f2a486daac13ba590746ac7569d05839629b92ff9114f10932e1af8532f990", @@ -47,9 +47,9 @@ "modules/authority/services/local/code/pkg/gen/host/composition/v1/composition.pb.go": "42c1a05e5767a0378c3826a15cd2d625b044d0639c1b37f111fa27d9e21e9db5", "modules/authority/services/local/code/pkg/gen/host/composition/v1/composition_grpc.pb.go": "acd95033d684741c23c74eb9f05021ebe487b9ab9e9cd81d549d44ab6c6d6162", "modules/authority/services/local/code/pkg/gen/host/composition/v1/compositionv1connect/composition.connect.go": "d1c06a0ee8ffad586e36271b773e52a43737e49c4473207e1fa03b922f3fe88f", - "modules/authority/services/local/code/pkg/gen/host/installation/v1/installation.pb.go": "3b72ebf9aaf3b043058ed4d5f7a3aebb712035e5c4d5ecdde490c75d8dcf1507", - "modules/authority/services/local/code/pkg/gen/host/installation/v1/installation_grpc.pb.go": "93a2e576502a08ae05e69bb1723c5259bb37c4c5e71baf4711d7da3dc46b187d", - "modules/authority/services/local/code/pkg/gen/host/installation/v1/installationv1connect/installation.connect.go": "dd01a2b12f9ecf5be84bbe57319e6bb4a3d8c1533dcb584aadb5143e36241fbc", + "modules/authority/services/local/code/pkg/gen/host/installation/v1/installation.pb.go": "7aee011a95a4e3a91dfdcfd0c5897f86bab99c97b36121a770bf52d0e177c9a6", + "modules/authority/services/local/code/pkg/gen/host/installation/v1/installation_grpc.pb.go": "b698a9175b07ec08ef22d80386cb4a9f4b1f8fd47d9d144a246cbe528e02394a", + "modules/authority/services/local/code/pkg/gen/host/installation/v1/installationv1connect/installation.connect.go": "10821a6115655e22ce42e00b71aab991e049ae5a63e94fcb976e9b85268b2fbb", "modules/authority/services/local/code/pkg/gen/host/session/v1/session.pb.go": "a82b22fdddeb97f1e17651c5d1b965adf7db038374c913c41eb929792ba49208", "modules/authority/services/local/code/pkg/gen/host/session/v1/session.pb.gw.go": "8238f29baefd4a136fc299d6a5813eb8242f115063aecd99347bfa4786862d79", "modules/authority/services/local/code/pkg/gen/host/session/v1/session_grpc.pb.go": "c4177166bf12cacb6e171beb4f739e730b7f005d559669cbab8be51598796468",