diff --git a/AGENTS.md b/AGENTS.md index a0212ded..9792c899 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,10 +17,12 @@ another clause, it does not belong here. attestation and composition packages, plus this repo's libraries. The local service does not currently import module-saas-starter. -The host-package reuse requirement above is an unresolved design obligation: -the existing local issuer/state resolves fixture grants itself. Do not add a -parallel resolver, infer that local tests prove SaaS parity, or silently erase -that obligation to match the implementation. Reconcile it with the host owner. +The local issuer now adapts requests to `libraries/authority-core/go`; its state +adapter supplies fixture roles and delivery data to that shared core. This does +not establish Accounts adoption or settle the host-package reuse requirement +above. Do not add a parallel resolver, infer that local tests prove SaaS parity, +or silently erase that obligation to match the implementation. Reconcile it with +the host owner. Rules, governed by https://github.com/obin-ai/handbook/blob/main/concepts/boundaries.md and the development contract at diff --git a/README.md b/README.md index 05d94399..c8200e6a 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,11 @@ This repository holds two things, and only these two: **What is reused today.** The local implementation uses Core's Work Context authority/verifier, workload attestation and signed composition machinery. -Its issuer/state resolves fixture grants; it does not currently link Accounts' -sign-in or grant-derivation packages. The original host-package reuse requirement +Its local issuer delegates issuance and grant derivation to this repository's +shared authority core; the state adapter supplies fixture roles and delivery data. +It does not currently link Accounts' sign-in or grant-derivation packages, and +this adapter migration does not establish Accounts adoption of that core. +The original host-package reuse requirement remains an unresolved design obligation with the host owner, not a guarantee established by this checkout. The shared conformance suite must run against both implementations; a local pass or a skipped provider scenario does not prove parity. @@ -173,8 +176,10 @@ talks to. Against that: an endpoint on a cell), installations with the install step, module routes. **What remains is the interface.** Authority: `Issue`, `Check`, `Keys`. -Composition: `ApplyManifest`. Installation: `Install`, `GrantToTeam`, `List`, and -the install step callback. Sign-in at the browser edge only. Routing is the data +Composition: `ApplyManifest`. Installation: `Install`, `GrantRole`, `RevokeRole`, +`ListRoleGrants`, `List`, and the install step callback (installation 0.2.0: an +installed solution is a principal, and a role grant to a person or a team is +the access; `GrantToTeam` is gone). Sign-in at the browser edge only. Routing is the data plane, not a contract. 0.1.0 mirrors today's spellings so the local authority serves the SDKs as they are; the reduced shape is 0.2.0, a host contract change through a handbook proposal, after which the SDKs move once. @@ -355,7 +360,7 @@ role through a granted team, and nothing from the organisation's roles. Another organisation's installation grants nothing. The values of real roles belong to each solution's manifest and arrive through the carrier: core's manifest carries no roles, so they travel in the authority's own document delivered with it -(`AuthorityManifestV1.roles`, as the sketch's `ModuleManifestV1.roles`), bounded +(`AuthorityManifestV1.roles`), bounded by the solution's own scope ceilings and the invoke/lookup scopes in the paired Core manifest. Consumed kinds do not become kinds the solution serves. This repository holds the contract and its evaluation, never a product's roles. @@ -413,10 +418,14 @@ role narrowing/removal and binding narrowing/removal before resolution, during minting and after issuance. These checks do not choose a child-scope policy or qualify the separate accounts implementation. -Installation administration (`List`, `Install`, `GrantToTeam`) requires an open -session whose current person is an administrator or owner of the requested -organization. Anonymous and invalid sessions are `Unauthenticated`; a member or -an administrator of another organization receives `PermissionDenied`. This is +Installation administration (`List`, `Install`) requires an open session whose +current person is an administrator or owner of the requested organization; role +grants (`GrantRole`, `RevokeRole`, `ListRoleGrants`) also admit the holder of the +installer role on that installation. Anonymous and invalid sessions are +`Unauthenticated`; anyone else receives `PermissionDenied`, and a caller of +another organization is answered as if the installation did not exist. Every +decision is the generic authority core's (`libraries/authority-core/go`); the +local authority is a host over it. This is not the navigation API: a member probes each target with an empty-scope `Issue`. That call returns `FailedPrecondition` when the audience has no active installation in the person's organization, and `PermissionDenied` when installed @@ -430,9 +439,8 @@ for exactly that scope and expects the answer. The local authority's fixture car two organisations, a person with no roles or teams and an automatically generated developer role; the test kit's composition document installs a solution under different roles in each. The host supplies its own matrix from its -own fixture. When the fixture signs, the suite composes its solution as a carrier -with the authority's document carrying its roles; the sketch remains for a -fixture that does not sign. +own fixture. The suite requires a signed carrier and authenticated delivery +workload, with the paired authority document carrying the solution's roles. The fixture names only the authority's own vocabulary. A composing workspace adds what its modules and solutions need: grants on the fixture's roles, @@ -867,8 +875,10 @@ results do not qualify later suite additions. keys it pins and reads the manifest from the verified document. The mini local authority pins the kit's release key and attests the carrier by the launcher's anchor; the host pins its release keys and attests the delivery service - account. The sketch (`manifest`) stays through the transition and is ignored - when a carrier is present. Not yet done: the host adapter behind it. + account. Public requests without a signed carrier and authenticated delivery + credential are refused before composition or pending installation work. The + old `manifest` wire field grants no authority. Trusted fixture seeding is a + host-only state API. Not yet done: the host adapter behind it. ## Local service name (2026-10-25) diff --git a/interfaces/authority/interface.codefly.yaml b/interfaces/authority/interface.codefly.yaml index 72c484f2..abae8de9 100644 --- a/interfaces/authority/interface.codefly.yaml +++ b/interfaces/authority/interface.codefly.yaml @@ -2,10 +2,10 @@ kind: interface publisher: codefly.dev name: authority description: What a module or solution calls on its host to be issued, checked and verified authority. Implemented by accounts and by the local authority. -version: 0.2.0 +version: 0.3.0 type: connect protobuf: package: host.authority.v1 services: - name: AuthorityService - methods: [Issue, Check, Keys, ListResources, Delegate, RevokeDelegation, GetDelegation] + methods: [Issue, Check, Keys, ListResources, AuthorizeTransfer, Delegate, RevokeDelegation, GetDelegation] diff --git a/interfaces/composition/interface.codefly.yaml b/interfaces/composition/interface.codefly.yaml index a42df9e5..72797d86 100644 --- a/interfaces/composition/interface.codefly.yaml +++ b/interfaces/composition/interface.codefly.yaml @@ -2,7 +2,7 @@ kind: interface publisher: codefly.dev name: composition description: How the host learns what is composed; the ceiling is the manifest. -version: 0.1.0 +version: 0.2.0 type: connect protobuf: package: host.composition.v1 diff --git a/interfaces/installation/interface.codefly.yaml b/interfaces/installation/interface.codefly.yaml index f55e1055..78ddd757 100644 --- a/interfaces/installation/interface.codefly.yaml +++ b/interfaces/installation/interface.codefly.yaml @@ -2,10 +2,10 @@ kind: interface publisher: codefly.dev name: installation description: What is installed at an organisation and who may use it. -version: 0.1.0 +version: 0.2.0 type: connect protobuf: package: host.installation.v1 services: - name: InstallationService - methods: [Install, GrantToTeam, List] + methods: [Install, GrantRole, RevokeRole, ListRoleGrants, List] diff --git a/libraries/authority-client/contract/catalog.codefly.json b/libraries/authority-client/contract/catalog.codefly.json index 14517e3c..18643b7f 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:05b735abdd6a67ad7365c54fcbd82b8b8412796735ada6323dd43d51bd5f3044", + "digest": "sha256:4d92544f5e186880be4d17079ad6dc056a6f9a84a0e1ebb298a2ff9b72863a46", "services": [ { "name": "LocalService", diff --git a/libraries/authority-client/contract/contract.binpb b/libraries/authority-client/contract/contract.binpb index 7daf49d4..c8f0054e 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/authority/v1/authority.pb.go b/libraries/authority-client/go/gen/host/authority/v1/authority.pb.go index 8a6d6865..cd75c76e 100644 --- a/libraries/authority-client/go/gen/host/authority/v1/authority.pb.go +++ b/libraries/authority-client/go/gen/host/authority/v1/authority.pb.go @@ -1217,7 +1217,155 @@ func (x *ListResourcesResponse) GetGrants() []*ResourceGrant { return nil } -// ResourceGrant is one way of naming covered resources. Exactly one is set. +// AuthorizeTransferRequest names one existing resource and where it moves. +type AuthorizeTransferRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The capability of the person asking, addressed to the caller. + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + // The module serving resource_kind, proving which module it is. + Actor *WorkloadCredential `protobuf:"bytes,2,opt,name=actor,proto3" json:"actor,omitempty"` + // The resource kind, as the caller serves it. + ResourceKind string `protobuf:"bytes,3,opt,name=resource_kind,json=resourceKind,proto3" json:"resource_kind,omitempty"` + // The resource id, as the caller knows it. + ResourceId string `protobuf:"bytes,4,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"` + // The resource's current owner, as the caller records it: the organization + // or a person of it. + FromOwnerId string `protobuf:"bytes,5,opt,name=from_owner_id,json=fromOwnerId,proto3" json:"from_owner_id,omitempty"` + // The solution principal the resource joins. + ToPrincipalId string `protobuf:"bytes,6,opt,name=to_principal_id,json=toPrincipalId,proto3" json:"to_principal_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthorizeTransferRequest) Reset() { + *x = AuthorizeTransferRequest{} + mi := &file_host_authority_v1_authority_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthorizeTransferRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorizeTransferRequest) ProtoMessage() {} + +func (x *AuthorizeTransferRequest) ProtoReflect() protoreflect.Message { + mi := &file_host_authority_v1_authority_proto_msgTypes[18] + 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 AuthorizeTransferRequest.ProtoReflect.Descriptor instead. +func (*AuthorizeTransferRequest) Descriptor() ([]byte, []int) { + return file_host_authority_v1_authority_proto_rawDescGZIP(), []int{18} +} + +func (x *AuthorizeTransferRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *AuthorizeTransferRequest) GetActor() *WorkloadCredential { + if x != nil { + return x.Actor + } + return nil +} + +func (x *AuthorizeTransferRequest) GetResourceKind() string { + if x != nil { + return x.ResourceKind + } + return "" +} + +func (x *AuthorizeTransferRequest) GetResourceId() string { + if x != nil { + return x.ResourceId + } + return "" +} + +func (x *AuthorizeTransferRequest) GetFromOwnerId() string { + if x != nil { + return x.FromOwnerId + } + return "" +} + +func (x *AuthorizeTransferRequest) GetToPrincipalId() string { + if x != nil { + return x.ToPrincipalId + } + return "" +} + +// AuthorizeTransferResponse is the decision the store records. +type AuthorizeTransferResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The principal that now owns the resource. + PrincipalId string `protobuf:"bytes,1,opt,name=principal_id,json=principalId,proto3" json:"principal_id,omitempty"` + // The person who authorized the transfer. + AuthorizedBy string `protobuf:"bytes,2,opt,name=authorized_by,json=authorizedBy,proto3" json:"authorized_by,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthorizeTransferResponse) Reset() { + *x = AuthorizeTransferResponse{} + mi := &file_host_authority_v1_authority_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthorizeTransferResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorizeTransferResponse) ProtoMessage() {} + +func (x *AuthorizeTransferResponse) ProtoReflect() protoreflect.Message { + mi := &file_host_authority_v1_authority_proto_msgTypes[19] + 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 AuthorizeTransferResponse.ProtoReflect.Descriptor instead. +func (*AuthorizeTransferResponse) Descriptor() ([]byte, []int) { + return file_host_authority_v1_authority_proto_rawDescGZIP(), []int{19} +} + +func (x *AuthorizeTransferResponse) GetPrincipalId() string { + if x != nil { + return x.PrincipalId + } + return "" +} + +func (x *AuthorizeTransferResponse) GetAuthorizedBy() string { + if x != nil { + return x.AuthorizedBy + } + return "" +} + +// ResourceGrant is one way of naming covered resources. Exactly one match is +// set; resource_owner_id, when set, qualifies it. type ResourceGrant struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Match: @@ -1226,14 +1374,17 @@ type ResourceGrant struct { // *ResourceGrant_Id // *ResourceGrant_Prefix // *ResourceGrant_Pattern - Match isResourceGrant_Match `protobuf_oneof:"match"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Match isResourceGrant_Match `protobuf_oneof:"match"` + // The owner the covered resources must have: a resource is covered when + // the match covers its id and it is owned by this principal. + ResourceOwnerId string `protobuf:"bytes,5,opt,name=resource_owner_id,json=resourceOwnerId,proto3" json:"resource_owner_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ResourceGrant) Reset() { *x = ResourceGrant{} - mi := &file_host_authority_v1_authority_proto_msgTypes[18] + mi := &file_host_authority_v1_authority_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1245,7 +1396,7 @@ func (x *ResourceGrant) String() string { func (*ResourceGrant) ProtoMessage() {} func (x *ResourceGrant) ProtoReflect() protoreflect.Message { - mi := &file_host_authority_v1_authority_proto_msgTypes[18] + mi := &file_host_authority_v1_authority_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1258,7 +1409,7 @@ func (x *ResourceGrant) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceGrant.ProtoReflect.Descriptor instead. func (*ResourceGrant) Descriptor() ([]byte, []int) { - return file_host_authority_v1_authority_proto_rawDescGZIP(), []int{18} + return file_host_authority_v1_authority_proto_rawDescGZIP(), []int{20} } func (x *ResourceGrant) GetMatch() isResourceGrant_Match { @@ -1304,6 +1455,13 @@ func (x *ResourceGrant) GetPattern() string { return "" } +func (x *ResourceGrant) GetResourceOwnerId() string { + if x != nil { + return x.ResourceOwnerId + } + return "" +} + type isResourceGrant_Match interface { isResourceGrant_Match() } @@ -1420,18 +1578,31 @@ const file_host_authority_v1_authority_proto_rawDesc = "" + "\rresource_kind\x18\x03 \x01(\tR\fresourceKind\x12\x16\n" + "\x06action\x18\x04 \x01(\tR\x06action\"Q\n" + "\x15ListResourcesResponse\x128\n" + - "\x06grants\x18\x01 \x03(\v2 .host.authority.v1.ResourceGrantR\x06grants\"t\n" + + "\x06grants\x18\x01 \x03(\v2 .host.authority.v1.ResourceGrantR\x06grants\"\xff\x01\n" + + "\x18AuthorizeTransferRequest\x12\x14\n" + + "\x05token\x18\x01 \x01(\tR\x05token\x12;\n" + + "\x05actor\x18\x02 \x01(\v2%.host.authority.v1.WorkloadCredentialR\x05actor\x12#\n" + + "\rresource_kind\x18\x03 \x01(\tR\fresourceKind\x12\x1f\n" + + "\vresource_id\x18\x04 \x01(\tR\n" + + "resourceId\x12\"\n" + + "\rfrom_owner_id\x18\x05 \x01(\tR\vfromOwnerId\x12&\n" + + "\x0fto_principal_id\x18\x06 \x01(\tR\rtoPrincipalId\"c\n" + + "\x19AuthorizeTransferResponse\x12!\n" + + "\fprincipal_id\x18\x01 \x01(\tR\vprincipalId\x12#\n" + + "\rauthorized_by\x18\x02 \x01(\tR\fauthorizedBy\"\xa0\x01\n" + "\rResourceGrant\x12\x12\n" + "\x03all\x18\x01 \x01(\bH\x00R\x03all\x12\x10\n" + "\x02id\x18\x02 \x01(\tH\x00R\x02id\x12\x18\n" + "\x06prefix\x18\x03 \x01(\tH\x00R\x06prefix\x12\x1a\n" + - "\apattern\x18\x04 \x01(\tH\x00R\apatternB\a\n" + - "\x05match2\xac\x05\n" + + "\apattern\x18\x04 \x01(\tH\x00R\apattern\x12*\n" + + "\x11resource_owner_id\x18\x05 \x01(\tR\x0fresourceOwnerIdB\a\n" + + "\x05match2\x9c\x06\n" + "\x10AuthorityService\x12j\n" + "\x05Issue\x12\x1f.host.authority.v1.IssueRequest\x1a .host.authority.v1.IssueResponse\"\x1e\x82\xd3\xe4\x93\x02\x18:\x01*\"\x13/v1/authority:issue\x12j\n" + "\x05Check\x12\x1f.host.authority.v1.CheckRequest\x1a .host.authority.v1.CheckResponse\"\x1e\x82\xd3\xe4\x93\x02\x18:\x01*\"\x13/v1/authority:check\x12G\n" + "\x04Keys\x12\x1e.host.authority.v1.KeysRequest\x1a\x1f.host.authority.v1.KeysResponse\x12b\n" + - "\rListResources\x12'.host.authority.v1.ListResourcesRequest\x1a(.host.authority.v1.ListResourcesResponse\x12M\n" + + "\rListResources\x12'.host.authority.v1.ListResourcesRequest\x1a(.host.authority.v1.ListResourcesResponse\x12n\n" + + "\x11AuthorizeTransfer\x12+.host.authority.v1.AuthorizeTransferRequest\x1a,.host.authority.v1.AuthorizeTransferResponse\x12M\n" + "\bDelegate\x12\".host.authority.v1.DelegateRequest\x1a\x1d.host.authority.v1.Delegation\x12k\n" + "\x10RevokeDelegation\x12*.host.authority.v1.RevokeDelegationRequest\x1a+.host.authority.v1.RevokeDelegationResponse\x12W\n" + "\rGetDelegation\x12'.host.authority.v1.GetDelegationRequest\x1a\x1d.host.authority.v1.DelegationB\xdf\x01\n" + @@ -1449,64 +1620,69 @@ func file_host_authority_v1_authority_proto_rawDescGZIP() []byte { return file_host_authority_v1_authority_proto_rawDescData } -var file_host_authority_v1_authority_proto_msgTypes = make([]protoimpl.MessageInfo, 19) +var file_host_authority_v1_authority_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_host_authority_v1_authority_proto_goTypes = []any{ - (*IssueRequest)(nil), // 0: host.authority.v1.IssueRequest - (*DelegationRef)(nil), // 1: host.authority.v1.DelegationRef - (*DelegateRequest)(nil), // 2: host.authority.v1.DelegateRequest - (*Delegation)(nil), // 3: host.authority.v1.Delegation - (*GetDelegationRequest)(nil), // 4: host.authority.v1.GetDelegationRequest - (*RevokeDelegationRequest)(nil), // 5: host.authority.v1.RevokeDelegationRequest - (*RevokeDelegationResponse)(nil), // 6: host.authority.v1.RevokeDelegationResponse - (*PersonSession)(nil), // 7: host.authority.v1.PersonSession - (*WorkloadCredential)(nil), // 8: host.authority.v1.WorkloadCredential - (*InstallationRef)(nil), // 9: host.authority.v1.InstallationRef - (*IssueResponse)(nil), // 10: host.authority.v1.IssueResponse - (*CheckRequest)(nil), // 11: host.authority.v1.CheckRequest - (*CheckResponse)(nil), // 12: host.authority.v1.CheckResponse - (*KeysRequest)(nil), // 13: host.authority.v1.KeysRequest - (*KeysResponse)(nil), // 14: host.authority.v1.KeysResponse - (*PublicKey)(nil), // 15: host.authority.v1.PublicKey - (*ListResourcesRequest)(nil), // 16: host.authority.v1.ListResourcesRequest - (*ListResourcesResponse)(nil), // 17: host.authority.v1.ListResourcesResponse - (*ResourceGrant)(nil), // 18: host.authority.v1.ResourceGrant - (*WorkScopeV1)(nil), // 19: host.authority.v1.WorkScopeV1 - (*WorkContextV1)(nil), // 20: host.authority.v1.WorkContextV1 + (*IssueRequest)(nil), // 0: host.authority.v1.IssueRequest + (*DelegationRef)(nil), // 1: host.authority.v1.DelegationRef + (*DelegateRequest)(nil), // 2: host.authority.v1.DelegateRequest + (*Delegation)(nil), // 3: host.authority.v1.Delegation + (*GetDelegationRequest)(nil), // 4: host.authority.v1.GetDelegationRequest + (*RevokeDelegationRequest)(nil), // 5: host.authority.v1.RevokeDelegationRequest + (*RevokeDelegationResponse)(nil), // 6: host.authority.v1.RevokeDelegationResponse + (*PersonSession)(nil), // 7: host.authority.v1.PersonSession + (*WorkloadCredential)(nil), // 8: host.authority.v1.WorkloadCredential + (*InstallationRef)(nil), // 9: host.authority.v1.InstallationRef + (*IssueResponse)(nil), // 10: host.authority.v1.IssueResponse + (*CheckRequest)(nil), // 11: host.authority.v1.CheckRequest + (*CheckResponse)(nil), // 12: host.authority.v1.CheckResponse + (*KeysRequest)(nil), // 13: host.authority.v1.KeysRequest + (*KeysResponse)(nil), // 14: host.authority.v1.KeysResponse + (*PublicKey)(nil), // 15: host.authority.v1.PublicKey + (*ListResourcesRequest)(nil), // 16: host.authority.v1.ListResourcesRequest + (*ListResourcesResponse)(nil), // 17: host.authority.v1.ListResourcesResponse + (*AuthorizeTransferRequest)(nil), // 18: host.authority.v1.AuthorizeTransferRequest + (*AuthorizeTransferResponse)(nil), // 19: host.authority.v1.AuthorizeTransferResponse + (*ResourceGrant)(nil), // 20: host.authority.v1.ResourceGrant + (*WorkScopeV1)(nil), // 21: host.authority.v1.WorkScopeV1 + (*WorkContextV1)(nil), // 22: host.authority.v1.WorkContextV1 } var file_host_authority_v1_authority_proto_depIdxs = []int32{ 7, // 0: host.authority.v1.IssueRequest.person:type_name -> host.authority.v1.PersonSession 8, // 1: host.authority.v1.IssueRequest.workload:type_name -> host.authority.v1.WorkloadCredential 9, // 2: host.authority.v1.IssueRequest.installation:type_name -> host.authority.v1.InstallationRef 1, // 3: host.authority.v1.IssueRequest.delegation:type_name -> host.authority.v1.DelegationRef - 19, // 4: host.authority.v1.IssueRequest.scopes:type_name -> host.authority.v1.WorkScopeV1 + 21, // 4: host.authority.v1.IssueRequest.scopes:type_name -> host.authority.v1.WorkScopeV1 8, // 5: host.authority.v1.IssueRequest.actor:type_name -> host.authority.v1.WorkloadCredential - 19, // 6: host.authority.v1.DelegateRequest.scopes:type_name -> host.authority.v1.WorkScopeV1 - 19, // 7: host.authority.v1.Delegation.scopes:type_name -> host.authority.v1.WorkScopeV1 + 21, // 6: host.authority.v1.DelegateRequest.scopes:type_name -> host.authority.v1.WorkScopeV1 + 21, // 7: host.authority.v1.Delegation.scopes:type_name -> host.authority.v1.WorkScopeV1 8, // 8: host.authority.v1.GetDelegationRequest.actor:type_name -> host.authority.v1.WorkloadCredential 3, // 9: host.authority.v1.RevokeDelegationResponse.delegation:type_name -> host.authority.v1.Delegation - 20, // 10: host.authority.v1.IssueResponse.context:type_name -> host.authority.v1.WorkContextV1 - 20, // 11: host.authority.v1.CheckResponse.context:type_name -> host.authority.v1.WorkContextV1 + 22, // 10: host.authority.v1.IssueResponse.context:type_name -> host.authority.v1.WorkContextV1 + 22, // 11: host.authority.v1.CheckResponse.context:type_name -> host.authority.v1.WorkContextV1 15, // 12: host.authority.v1.KeysResponse.keys:type_name -> host.authority.v1.PublicKey - 18, // 13: host.authority.v1.ListResourcesResponse.grants:type_name -> host.authority.v1.ResourceGrant - 0, // 14: host.authority.v1.AuthorityService.Issue:input_type -> host.authority.v1.IssueRequest - 11, // 15: host.authority.v1.AuthorityService.Check:input_type -> host.authority.v1.CheckRequest - 13, // 16: host.authority.v1.AuthorityService.Keys:input_type -> host.authority.v1.KeysRequest - 16, // 17: host.authority.v1.AuthorityService.ListResources:input_type -> host.authority.v1.ListResourcesRequest - 2, // 18: host.authority.v1.AuthorityService.Delegate:input_type -> host.authority.v1.DelegateRequest - 5, // 19: host.authority.v1.AuthorityService.RevokeDelegation:input_type -> host.authority.v1.RevokeDelegationRequest - 4, // 20: host.authority.v1.AuthorityService.GetDelegation:input_type -> host.authority.v1.GetDelegationRequest - 10, // 21: host.authority.v1.AuthorityService.Issue:output_type -> host.authority.v1.IssueResponse - 12, // 22: host.authority.v1.AuthorityService.Check:output_type -> host.authority.v1.CheckResponse - 14, // 23: host.authority.v1.AuthorityService.Keys:output_type -> host.authority.v1.KeysResponse - 17, // 24: host.authority.v1.AuthorityService.ListResources:output_type -> host.authority.v1.ListResourcesResponse - 3, // 25: host.authority.v1.AuthorityService.Delegate:output_type -> host.authority.v1.Delegation - 6, // 26: host.authority.v1.AuthorityService.RevokeDelegation:output_type -> host.authority.v1.RevokeDelegationResponse - 3, // 27: host.authority.v1.AuthorityService.GetDelegation:output_type -> host.authority.v1.Delegation - 21, // [21:28] is the sub-list for method output_type - 14, // [14:21] is the sub-list for method input_type - 14, // [14:14] is the sub-list for extension type_name - 14, // [14:14] is the sub-list for extension extendee - 0, // [0:14] is the sub-list for field type_name + 20, // 13: host.authority.v1.ListResourcesResponse.grants:type_name -> host.authority.v1.ResourceGrant + 8, // 14: host.authority.v1.AuthorizeTransferRequest.actor:type_name -> host.authority.v1.WorkloadCredential + 0, // 15: host.authority.v1.AuthorityService.Issue:input_type -> host.authority.v1.IssueRequest + 11, // 16: host.authority.v1.AuthorityService.Check:input_type -> host.authority.v1.CheckRequest + 13, // 17: host.authority.v1.AuthorityService.Keys:input_type -> host.authority.v1.KeysRequest + 16, // 18: host.authority.v1.AuthorityService.ListResources:input_type -> host.authority.v1.ListResourcesRequest + 18, // 19: host.authority.v1.AuthorityService.AuthorizeTransfer:input_type -> host.authority.v1.AuthorizeTransferRequest + 2, // 20: host.authority.v1.AuthorityService.Delegate:input_type -> host.authority.v1.DelegateRequest + 5, // 21: host.authority.v1.AuthorityService.RevokeDelegation:input_type -> host.authority.v1.RevokeDelegationRequest + 4, // 22: host.authority.v1.AuthorityService.GetDelegation:input_type -> host.authority.v1.GetDelegationRequest + 10, // 23: host.authority.v1.AuthorityService.Issue:output_type -> host.authority.v1.IssueResponse + 12, // 24: host.authority.v1.AuthorityService.Check:output_type -> host.authority.v1.CheckResponse + 14, // 25: host.authority.v1.AuthorityService.Keys:output_type -> host.authority.v1.KeysResponse + 17, // 26: host.authority.v1.AuthorityService.ListResources:output_type -> host.authority.v1.ListResourcesResponse + 19, // 27: host.authority.v1.AuthorityService.AuthorizeTransfer:output_type -> host.authority.v1.AuthorizeTransferResponse + 3, // 28: host.authority.v1.AuthorityService.Delegate:output_type -> host.authority.v1.Delegation + 6, // 29: host.authority.v1.AuthorityService.RevokeDelegation:output_type -> host.authority.v1.RevokeDelegationResponse + 3, // 30: host.authority.v1.AuthorityService.GetDelegation:output_type -> host.authority.v1.Delegation + 23, // [23:31] is the sub-list for method output_type + 15, // [15:23] is the sub-list for method input_type + 15, // [15:15] is the sub-list for extension type_name + 15, // [15:15] is the sub-list for extension extendee + 0, // [0:15] is the sub-list for field type_name } func init() { file_host_authority_v1_authority_proto_init() } @@ -1522,7 +1698,7 @@ func file_host_authority_v1_authority_proto_init() { (*IssueRequest_ParentToken)(nil), (*IssueRequest_Delegation)(nil), } - file_host_authority_v1_authority_proto_msgTypes[18].OneofWrappers = []any{ + file_host_authority_v1_authority_proto_msgTypes[20].OneofWrappers = []any{ (*ResourceGrant_All)(nil), (*ResourceGrant_Id)(nil), (*ResourceGrant_Prefix)(nil), @@ -1534,7 +1710,7 @@ func file_host_authority_v1_authority_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_host_authority_v1_authority_proto_rawDesc), len(file_host_authority_v1_authority_proto_rawDesc)), NumEnums: 0, - NumMessages: 19, + NumMessages: 21, NumExtensions: 0, NumServices: 1, }, diff --git a/libraries/authority-client/go/gen/host/authority/v1/authority_grpc.pb.go b/libraries/authority-client/go/gen/host/authority/v1/authority_grpc.pb.go index 51d772ee..e548980e 100644 --- a/libraries/authority-client/go/gen/host/authority/v1/authority_grpc.pb.go +++ b/libraries/authority-client/go/gen/host/authority/v1/authority_grpc.pb.go @@ -26,13 +26,14 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - AuthorityService_Issue_FullMethodName = "/host.authority.v1.AuthorityService/Issue" - AuthorityService_Check_FullMethodName = "/host.authority.v1.AuthorityService/Check" - AuthorityService_Keys_FullMethodName = "/host.authority.v1.AuthorityService/Keys" - AuthorityService_ListResources_FullMethodName = "/host.authority.v1.AuthorityService/ListResources" - AuthorityService_Delegate_FullMethodName = "/host.authority.v1.AuthorityService/Delegate" - AuthorityService_RevokeDelegation_FullMethodName = "/host.authority.v1.AuthorityService/RevokeDelegation" - AuthorityService_GetDelegation_FullMethodName = "/host.authority.v1.AuthorityService/GetDelegation" + AuthorityService_Issue_FullMethodName = "/host.authority.v1.AuthorityService/Issue" + AuthorityService_Check_FullMethodName = "/host.authority.v1.AuthorityService/Check" + AuthorityService_Keys_FullMethodName = "/host.authority.v1.AuthorityService/Keys" + AuthorityService_ListResources_FullMethodName = "/host.authority.v1.AuthorityService/ListResources" + AuthorityService_AuthorizeTransfer_FullMethodName = "/host.authority.v1.AuthorityService/AuthorizeTransfer" + AuthorityService_Delegate_FullMethodName = "/host.authority.v1.AuthorityService/Delegate" + AuthorityService_RevokeDelegation_FullMethodName = "/host.authority.v1.AuthorityService/RevokeDelegation" + AuthorityService_GetDelegation_FullMethodName = "/host.authority.v1.AuthorityService/GetDelegation" ) // AuthorityServiceClient is the client API for AuthorityService service. @@ -57,6 +58,19 @@ type AuthorityServiceClient interface { // rather than carried in the capability. A module maps the answer onto its // own resources; the authority knows them only by id. ListResources(ctx context.Context, in *ListResourcesRequest, opts ...grpc.CallOption) (*ListResourcesResponse, error) + // AuthorizeTransfer decides whether an existing resource may join an + // installed solution's principal. The caller is the kind's one serving + // module, which attests the resource exists and who owns it now + // (from_owner_id), presenting the capability of the person asking: issued + // through the target principal's installation and addressed to the caller. + // It is allowed when the current owner is the organization or a person of + // it (never another solution principal, another organization or an unknown + // owner), the person holds admin on that resource from their own + // organization grants, and the person's current roles on the principal hold + // write on the kind, all read live. An installation acting for itself never + // transfers. The store then records the principal as owner; the original + // creator stays the author. + AuthorizeTransfer(ctx context.Context, in *AuthorizeTransferRequest, opts ...grpc.CallOption) (*AuthorizeTransferResponse, error) // Delegate records a signed-in person's grant to a module (the caller's // bearer is the person); Issue with a delegation acts on it. Delegate(ctx context.Context, in *DelegateRequest, opts ...grpc.CallOption) (*Delegation, error) @@ -117,6 +131,16 @@ func (c *authorityServiceClient) ListResources(ctx context.Context, in *ListReso return out, nil } +func (c *authorityServiceClient) AuthorizeTransfer(ctx context.Context, in *AuthorizeTransferRequest, opts ...grpc.CallOption) (*AuthorizeTransferResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AuthorizeTransferResponse) + err := c.cc.Invoke(ctx, AuthorityService_AuthorizeTransfer_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *authorityServiceClient) Delegate(ctx context.Context, in *DelegateRequest, opts ...grpc.CallOption) (*Delegation, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Delegation) @@ -169,6 +193,19 @@ type AuthorityServiceServer interface { // rather than carried in the capability. A module maps the answer onto its // own resources; the authority knows them only by id. ListResources(context.Context, *ListResourcesRequest) (*ListResourcesResponse, error) + // AuthorizeTransfer decides whether an existing resource may join an + // installed solution's principal. The caller is the kind's one serving + // module, which attests the resource exists and who owns it now + // (from_owner_id), presenting the capability of the person asking: issued + // through the target principal's installation and addressed to the caller. + // It is allowed when the current owner is the organization or a person of + // it (never another solution principal, another organization or an unknown + // owner), the person holds admin on that resource from their own + // organization grants, and the person's current roles on the principal hold + // write on the kind, all read live. An installation acting for itself never + // transfers. The store then records the principal as owner; the original + // creator stays the author. + AuthorizeTransfer(context.Context, *AuthorizeTransferRequest) (*AuthorizeTransferResponse, error) // Delegate records a signed-in person's grant to a module (the caller's // bearer is the person); Issue with a delegation acts on it. Delegate(context.Context, *DelegateRequest) (*Delegation, error) @@ -201,6 +238,9 @@ func (UnimplementedAuthorityServiceServer) Keys(context.Context, *KeysRequest) ( func (UnimplementedAuthorityServiceServer) ListResources(context.Context, *ListResourcesRequest) (*ListResourcesResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListResources not implemented") } +func (UnimplementedAuthorityServiceServer) AuthorizeTransfer(context.Context, *AuthorizeTransferRequest) (*AuthorizeTransferResponse, error) { + return nil, status.Error(codes.Unimplemented, "method AuthorizeTransfer not implemented") +} func (UnimplementedAuthorityServiceServer) Delegate(context.Context, *DelegateRequest) (*Delegation, error) { return nil, status.Error(codes.Unimplemented, "method Delegate not implemented") } @@ -303,6 +343,24 @@ func _AuthorityService_ListResources_Handler(srv interface{}, ctx context.Contex return interceptor(ctx, in, info, handler) } +func _AuthorityService_AuthorizeTransfer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AuthorizeTransferRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthorityServiceServer).AuthorizeTransfer(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthorityService_AuthorizeTransfer_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthorityServiceServer).AuthorizeTransfer(ctx, req.(*AuthorizeTransferRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _AuthorityService_Delegate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DelegateRequest) if err := dec(in); err != nil { @@ -380,6 +438,10 @@ var AuthorityService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListResources", Handler: _AuthorityService_ListResources_Handler, }, + { + MethodName: "AuthorizeTransfer", + Handler: _AuthorityService_AuthorizeTransfer_Handler, + }, { MethodName: "Delegate", Handler: _AuthorityService_Delegate_Handler, diff --git a/libraries/authority-client/go/gen/host/authority/v1/work_context.pb.go b/libraries/authority-client/go/gen/host/authority/v1/work_context.pb.go index 95f91ca4..56d52154 100644 --- a/libraries/authority-client/go/gen/host/authority/v1/work_context.pb.go +++ b/libraries/authority-client/go/gen/host/authority/v1/work_context.pb.go @@ -548,7 +548,11 @@ type WorkScopeV1 struct { // (an installed solution's principal). Empty means the scope selects no // owner. With resource_ids, a resource is covered only when both match. A // reader that cannot compare a resource's owner must not treat an - // owner-selected scope as covering the whole kind. + // owner-selected scope as covering the whole kind. It is at most 512 bytes + // of printable ASCII other than space and "*" (0x21-0x29, 0x2B-0x7E): no + // whitespace, control, non-ASCII, invisible or lookalike character, so two + // owners that print alike are the same bytes and every regex engine answers + // the rule the same way. ResourceOwnerId string `protobuf:"bytes,4,opt,name=resource_owner_id,json=resourceOwnerId,proto3" json:"resource_owner_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1041,13 +1045,13 @@ const file_host_authority_v1_work_context_proto_rawDesc = "" + "binding_id\x18\x01 \x01(\tB\n" + "\xbaH\ar\x05\x10\x01\x18\x80\x04R\tbindingId\x12#\n" + "\brevision\x18\x02 \x01(\x04B\a\xbaH\x042\x02(\x01R\brevision\x12)\n" + - "\vincarnation\x18\x03 \x01(\x04B\a\xbaH\x042\x02(\x01R\vincarnation\"\xc0\x01\n" + + "\vincarnation\x18\x03 \x01(\x04B\a\xbaH\x042\x02(\x01R\vincarnation\"\xbe\x01\n" + "\vWorkScopeV1\x12/\n" + "\rresource_kind\x18\x01 \x01(\tB\n" + "\xbaH\ar\x05\x10\x01\x18\x80\x01R\fresourceKind\x12\x18\n" + "\aactions\x18\x02 \x03(\tR\aactions\x12!\n" + - "\fresource_ids\x18\x03 \x03(\tR\vresourceIds\x12C\n" + - "\x11resource_owner_id\x18\x04 \x01(\tB\x17\xbaH\x14r\x12\x18\x80\x042\r^[^\\s\\x00*]*$R\x0fresourceOwnerId\"\xd0\x03\n" + + "\fresource_ids\x18\x03 \x03(\tR\vresourceIds\x12A\n" + + "\x11resource_owner_id\x18\x04 \x01(\tB\x15\xbaH\x12r\x10(\x80\x042\v^[!-)+-~]*$R\x0fresourceOwnerId\"\xd0\x03\n" + "\vWorkActorV1\x12-\n" + "\fprincipal_id\x18\x01 \x01(\tB\n" + "\xbaH\ar\x05\x10\x01\x18\x80\x04R\vprincipalId\x121\n" + diff --git a/libraries/authority-client/go/gen/host/composition/v1/composition.pb.go b/libraries/authority-client/go/gen/host/composition/v1/composition.pb.go index 606daa51..1ee25c52 100644 --- a/libraries/authority-client/go/gen/host/composition/v1/composition.pb.go +++ b/libraries/authority-client/go/gen/host/composition/v1/composition.pb.go @@ -136,7 +136,12 @@ type AuthorityManifestV1 struct { // one of them installer. Headless-only scopes cannot supply a role grant. // Core's manifest carries no roles, so they travel here until it does. A // module declares none; a module's delivery listing any is refused. - Roles []*RoleV1 `protobuf:"bytes,6,rep,name=roles,proto3" json:"roles,omitempty"` + Roles []*RoleV1 `protobuf:"bytes,6,rep,name=roles,proto3" json:"roles,omitempty"` + // The module checks a resource's owner against an owner-selected scope + // (Claims.AllowsOwned): only an audience declaring it receives one, and a + // delivery withdrawing it refuses the owner-selected capabilities issued + // before. Every solution receiving its roles' scopes declares it. + OwnerAware bool `protobuf:"varint,7,opt,name=owner_aware,json=ownerAware,proto3" json:"owner_aware,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -213,6 +218,13 @@ func (x *AuthorityManifestV1) GetRoles() []*RoleV1 { return nil } +func (x *AuthorityManifestV1) GetOwnerAware() bool { + if x != nil { + return x.OwnerAware + } + return false +} + // MethodPolicyV1 is what a call to one method must hold, as the module's // contract declares it with the authority.hop.v1.policy option. type MethodPolicyV1 struct { @@ -417,7 +429,7 @@ type ModuleManifestV1 struct { Page *PageV1 `protobuf:"bytes,7,opt,name=page,proto3" json:"page,omitempty"` // The roles a solution declares a person may hold on an installation of it, // each within what the solution serves or explicitly consumes through - // invoke/lookup bindings; one is named installer, held by whoever installs it. + // invoke/lookup bindings; one is named installer, held only by whom it is granted to. // Headless-only scopes cannot supply a role grant. A module declares none. Roles []*RoleV1 `protobuf:"bytes,8,rep,name=roles,proto3" json:"roles,omitempty"` unknownFields protoimpl.UnknownFields @@ -769,7 +781,7 @@ const file_host_composition_v1_composition_proto_rawDesc = "" + "\n" + "credential\x18\x03 \x01(\tR\n" + "credential\x12C\n" + - "\tauthority\x18\x04 \x01(\v2%.host.composition.v1.SignedManifestV1R\tauthority\"\xb8\x02\n" + + "\tauthority\x18\x04 \x01(\v2%.host.composition.v1.SignedManifestV1R\tauthority\"\xd9\x02\n" + "\x13AuthorityManifestV1\x12\x16\n" + "\x06module\x18\x01 \x01(\tR\x06module\x12\x1e\n" + "\n" + @@ -778,7 +790,9 @@ const file_host_composition_v1_composition_proto_rawDesc = "" + "\x14core_manifest_digest\x18\x03 \x01(\tR\x12coreManifestDigest\x12=\n" + "\amethods\x18\x04 \x03(\v2#.host.composition.v1.MethodPolicyV1R\amethods\x12E\n" + "\x0escope_ceilings\x18\x05 \x03(\v2\x1e.host.authority.v1.WorkScopeV1R\rscopeCeilings\x121\n" + - "\x05roles\x18\x06 \x03(\v2\x1b.host.composition.v1.RoleV1R\x05roles\"\x92\x01\n" + + "\x05roles\x18\x06 \x03(\v2\x1b.host.composition.v1.RoleV1R\x05roles\x12\x1f\n" + + "\vowner_aware\x18\a \x01(\bR\n" + + "ownerAware\"\x92\x01\n" + "\x0eMethodPolicyV1\x12\x16\n" + "\x06method\x18\x01 \x01(\tR\x06method\x12:\n" + "\brequires\x18\x02 \x03(\v2\x1e.host.authority.v1.WorkScopeV1R\brequires\x12,\n" + 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 5c59032b..26529359 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 @@ -5,8 +5,10 @@ // source: host/installation/v1/installation.proto // The installation interface: what is installed at an organisation and who may -// use it. Three operations on the host, and one the solution implements, the -// install step the host calls once per installation. +// use it. An installed solution is a principal of its organisation; its roles +// are levels on the data that principal owns, and a grant of a role is the +// access. Operations on the host, and one the solution implements, the install +// step the host calls once per installation. package installationv1 @@ -80,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{4, 0} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{8, 0} } // InstallRequest asks to install one target at one organisation. @@ -156,35 +158,40 @@ func (x *InstallRequest) GetOwnerPersonId() string { return "" } -// GrantToTeamRequest exposes an installation to a team. -type GrantToTeamRequest struct { +// GrantRoleRequest grants one role to exactly one person or team. +type GrantRoleRequest struct { state protoimpl.MessageState `protogen:"open.v1"` + // The organization the installation belongs to; a request naming another + // organization's installation is refused naming the organization, before + // the caller's authority is read. + OrganizationId string `protobuf:"bytes,1,opt,name=organization_id,json=organizationId,proto3" json:"organization_id,omitempty"` // The installation. - InstallationId string `protobuf:"bytes,1,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` - // The team granted the installation's role at its root. - TeamId string `protobuf:"bytes,2,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` - // The organisation the installation belongs to; a grant is an organisation - // administrator's act, and the host checks the actor against it before it - // reads the installation. Learned from the second implementation (the host). - OrganizationId string `protobuf:"bytes,3,opt,name=organization_id,json=organizationId,proto3" json:"organization_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + InstallationId string `protobuf:"bytes,2,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` + // One of the roles the solution declares. + Role string `protobuf:"bytes,3,opt,name=role,proto3" json:"role,omitempty"` + // The person granted, a member of the organization; exactly one of + // person_id and team_id is set. + PersonId string `protobuf:"bytes,4,opt,name=person_id,json=personId,proto3" json:"person_id,omitempty"` + // The team granted, a team of the organization. + TeamId string `protobuf:"bytes,5,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *GrantToTeamRequest) Reset() { - *x = GrantToTeamRequest{} +func (x *GrantRoleRequest) Reset() { + *x = GrantRoleRequest{} mi := &file_host_installation_v1_installation_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *GrantToTeamRequest) String() string { +func (x *GrantRoleRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*GrantToTeamRequest) ProtoMessage() {} +func (*GrantRoleRequest) ProtoMessage() {} -func (x *GrantToTeamRequest) ProtoReflect() protoreflect.Message { +func (x *GrantRoleRequest) ProtoReflect() protoreflect.Message { mi := &file_host_installation_v1_installation_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -196,32 +203,293 @@ func (x *GrantToTeamRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use GrantToTeamRequest.ProtoReflect.Descriptor instead. -func (*GrantToTeamRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use GrantRoleRequest.ProtoReflect.Descriptor instead. +func (*GrantRoleRequest) Descriptor() ([]byte, []int) { return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{1} } -func (x *GrantToTeamRequest) GetInstallationId() string { +func (x *GrantRoleRequest) GetOrganizationId() string { + if x != nil { + return x.OrganizationId + } + return "" +} + +func (x *GrantRoleRequest) GetInstallationId() string { + if x != nil { + return x.InstallationId + } + return "" +} + +func (x *GrantRoleRequest) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +func (x *GrantRoleRequest) GetPersonId() string { + if x != nil { + return x.PersonId + } + return "" +} + +func (x *GrantRoleRequest) GetTeamId() string { + if x != nil { + return x.TeamId + } + return "" +} + +// RevokeRoleRequest ends one grant, named as GrantRole named it. +type RevokeRoleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The organization the installation belongs to. + OrganizationId string `protobuf:"bytes,1,opt,name=organization_id,json=organizationId,proto3" json:"organization_id,omitempty"` + // The installation. + InstallationId string `protobuf:"bytes,2,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` + // The role. + Role string `protobuf:"bytes,3,opt,name=role,proto3" json:"role,omitempty"` + // The person; exactly one of person_id and team_id is set. + PersonId string `protobuf:"bytes,4,opt,name=person_id,json=personId,proto3" json:"person_id,omitempty"` + // The team. + TeamId string `protobuf:"bytes,5,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeRoleRequest) Reset() { + *x = RevokeRoleRequest{} + mi := &file_host_installation_v1_installation_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeRoleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeRoleRequest) ProtoMessage() {} + +func (x *RevokeRoleRequest) 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 RevokeRoleRequest.ProtoReflect.Descriptor instead. +func (*RevokeRoleRequest) Descriptor() ([]byte, []int) { + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{2} +} + +func (x *RevokeRoleRequest) GetOrganizationId() string { + if x != nil { + return x.OrganizationId + } + return "" +} + +func (x *RevokeRoleRequest) GetInstallationId() string { if x != nil { return x.InstallationId } return "" } -func (x *GrantToTeamRequest) GetTeamId() string { +func (x *RevokeRoleRequest) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +func (x *RevokeRoleRequest) GetPersonId() string { + if x != nil { + return x.PersonId + } + return "" +} + +func (x *RevokeRoleRequest) GetTeamId() string { if x != nil { return x.TeamId } return "" } -func (x *GrantToTeamRequest) GetOrganizationId() string { +// ListRoleGrantsRequest names one installation. +type ListRoleGrantsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The organization the installation belongs to. + OrganizationId string `protobuf:"bytes,1,opt,name=organization_id,json=organizationId,proto3" json:"organization_id,omitempty"` + // The installation. + InstallationId string `protobuf:"bytes,2,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRoleGrantsRequest) Reset() { + *x = ListRoleGrantsRequest{} + mi := &file_host_installation_v1_installation_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRoleGrantsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRoleGrantsRequest) ProtoMessage() {} + +func (x *ListRoleGrantsRequest) ProtoReflect() protoreflect.Message { + mi := &file_host_installation_v1_installation_proto_msgTypes[3] + 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 ListRoleGrantsRequest.ProtoReflect.Descriptor instead. +func (*ListRoleGrantsRequest) Descriptor() ([]byte, []int) { + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{3} +} + +func (x *ListRoleGrantsRequest) GetOrganizationId() string { if x != nil { return x.OrganizationId } return "" } +func (x *ListRoleGrantsRequest) GetInstallationId() string { + if x != nil { + return x.InstallationId + } + return "" +} + +// ListRoleGrantsResponse is every grant on the installation. +type ListRoleGrantsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The grants. + Grants []*RoleGrant `protobuf:"bytes,1,rep,name=grants,proto3" json:"grants,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRoleGrantsResponse) Reset() { + *x = ListRoleGrantsResponse{} + mi := &file_host_installation_v1_installation_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRoleGrantsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRoleGrantsResponse) ProtoMessage() {} + +func (x *ListRoleGrantsResponse) ProtoReflect() protoreflect.Message { + mi := &file_host_installation_v1_installation_proto_msgTypes[4] + 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 ListRoleGrantsResponse.ProtoReflect.Descriptor instead. +func (*ListRoleGrantsResponse) Descriptor() ([]byte, []int) { + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{4} +} + +func (x *ListRoleGrantsResponse) GetGrants() []*RoleGrant { + if x != nil { + return x.Grants + } + return nil +} + +// RoleGrant is one role held by one person or team. +type RoleGrant struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The role. + Role string `protobuf:"bytes,1,opt,name=role,proto3" json:"role,omitempty"` + // The person, when a person holds it. + PersonId string `protobuf:"bytes,2,opt,name=person_id,json=personId,proto3" json:"person_id,omitempty"` + // The team, when a team holds it. + TeamId string `protobuf:"bytes,3,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RoleGrant) Reset() { + *x = RoleGrant{} + mi := &file_host_installation_v1_installation_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RoleGrant) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoleGrant) ProtoMessage() {} + +func (x *RoleGrant) ProtoReflect() protoreflect.Message { + mi := &file_host_installation_v1_installation_proto_msgTypes[5] + 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 RoleGrant.ProtoReflect.Descriptor instead. +func (*RoleGrant) Descriptor() ([]byte, []int) { + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{5} +} + +func (x *RoleGrant) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +func (x *RoleGrant) GetPersonId() string { + if x != nil { + return x.PersonId + } + return "" +} + +func (x *RoleGrant) GetTeamId() string { + if x != nil { + return x.TeamId + } + return "" +} + // ListRequest asks for one organisation's installations. type ListRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -233,7 +501,7 @@ type ListRequest struct { func (x *ListRequest) Reset() { *x = ListRequest{} - mi := &file_host_installation_v1_installation_proto_msgTypes[2] + mi := &file_host_installation_v1_installation_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -245,7 +513,7 @@ func (x *ListRequest) String() string { func (*ListRequest) ProtoMessage() {} func (x *ListRequest) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[2] + mi := &file_host_installation_v1_installation_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -258,7 +526,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{2} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{6} } func (x *ListRequest) GetOrganizationId() string { @@ -279,7 +547,7 @@ type ListResponse struct { func (x *ListResponse) Reset() { *x = ListResponse{} - mi := &file_host_installation_v1_installation_proto_msgTypes[3] + mi := &file_host_installation_v1_installation_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -291,7 +559,7 @@ func (x *ListResponse) String() string { func (*ListResponse) ProtoMessage() {} func (x *ListResponse) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[3] + mi := &file_host_installation_v1_installation_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -304,7 +572,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{3} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{7} } func (x *ListResponse) GetInstallations() []*Installation { @@ -327,24 +595,27 @@ type Installation struct { Status Installation_Status `protobuf:"varint,4,opt,name=status,proto3,enum=host.installation.v1.Installation_Status" json:"status,omitempty"` // Bumped on every change that narrows or widens the installation. Revision uint64 `protobuf:"varint,5,opt,name=revision,proto3" json:"revision,omitempty"` - // The role the installation holds. + // The role the installation holds when it acts for itself (its agent's + // role); no person or team inherits it. Role string `protobuf:"bytes,6,opt,name=role,proto3" json:"role,omitempty"` - // The teams granted it. - TeamIds []string `protobuf:"bytes,7,rep,name=team_ids,json=teamIds,proto3" json:"team_ids,omitempty"` - // The principal the installation acts as for itself. - AgentPrincipalId string `protobuf:"bytes,8,opt,name=agent_principal_id,json=agentPrincipalId,proto3" json:"agent_principal_id,omitempty"` // The namespace the installation's own authored content belongs to (an // agent's source): the host names it, so a module never chooses which source // an installation may run. A module installed by composition is // ".". - Namespace string `protobuf:"bytes,9,opt,name=namespace,proto3" json:"namespace,omitempty"` + Namespace string `protobuf:"bytes,9,opt,name=namespace,proto3" json:"namespace,omitempty"` + // The solution principal: the identity the installation's data is owned by + // and the installation acts as for itself, a stable UUID per organization + // and target, reused by a reinstall. Empty for a module installation. + PrincipalId string `protobuf:"bytes,10,opt,name=principal_id,json=principalId,proto3" json:"principal_id,omitempty"` + // The roles the solution declares, sorted. + Roles []string `protobuf:"bytes,11,rep,name=roles,proto3" json:"roles,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Installation) Reset() { *x = Installation{} - mi := &file_host_installation_v1_installation_proto_msgTypes[4] + mi := &file_host_installation_v1_installation_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -356,7 +627,7 @@ func (x *Installation) String() string { func (*Installation) ProtoMessage() {} func (x *Installation) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[4] + mi := &file_host_installation_v1_installation_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -369,7 +640,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{4} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{8} } func (x *Installation) GetId() string { @@ -414,25 +685,25 @@ func (x *Installation) GetRole() string { return "" } -func (x *Installation) GetTeamIds() []string { +func (x *Installation) GetNamespace() string { if x != nil { - return x.TeamIds + return x.Namespace } - return nil + return "" } -func (x *Installation) GetAgentPrincipalId() string { +func (x *Installation) GetPrincipalId() string { if x != nil { - return x.AgentPrincipalId + return x.PrincipalId } return "" } -func (x *Installation) GetNamespace() string { +func (x *Installation) GetRoles() []string { if x != nil { - return x.Namespace + return x.Roles } - return "" + return nil } // InstallationNotice is what the host hands the solution's install step. @@ -446,7 +717,7 @@ type InstallationNotice struct { func (x *InstallationNotice) Reset() { *x = InstallationNotice{} - mi := &file_host_installation_v1_installation_proto_msgTypes[5] + mi := &file_host_installation_v1_installation_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -458,7 +729,7 @@ func (x *InstallationNotice) String() string { func (*InstallationNotice) ProtoMessage() {} func (x *InstallationNotice) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[5] + mi := &file_host_installation_v1_installation_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -471,7 +742,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{5} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{9} } func (x *InstallationNotice) GetInstallation() *Installation { @@ -490,7 +761,7 @@ type InstallationAck struct { func (x *InstallationAck) Reset() { *x = InstallationAck{} - mi := &file_host_installation_v1_installation_proto_msgTypes[6] + mi := &file_host_installation_v1_installation_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -502,7 +773,7 @@ func (x *InstallationAck) String() string { func (*InstallationAck) ProtoMessage() {} func (x *InstallationAck) ProtoReflect() protoreflect.Message { - mi := &file_host_installation_v1_installation_proto_msgTypes[6] + mi := &file_host_installation_v1_installation_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -515,7 +786,7 @@ 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{6} + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{10} } var File_host_installation_v1_installation_proto protoreflect.FileDescriptor @@ -527,36 +798,57 @@ const file_host_installation_v1_installation_proto_rawDesc = "" + "\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\"\x7f\n" + - "\x12GrantToTeamRequest\x12'\n" + - "\x0finstallation_id\x18\x01 \x01(\tR\x0einstallationId\x12\x17\n" + - "\ateam_id\x18\x02 \x01(\tR\x06teamId\x12'\n" + - "\x0forganization_id\x18\x03 \x01(\tR\x0eorganizationId\"6\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\bpersonId\x12\x17\n" + + "\ateam_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\bpersonId\x12\x17\n" + + "\ateam_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\x127\n" + + "\x06grants\x18\x01 \x03(\v2\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\bpersonId\x12\x17\n" + + "\ateam_id\x18\x03 \x01(\tR\x06teamId\"6\n" + "\vListRequest\x12'\n" + "\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\"X\n" + "\fListResponse\x12H\n" + - "\rinstallations\x18\x01 \x03(\v2\".host.installation.v1.InstallationR\rinstallations\"\x96\x03\n" + + "\rinstallations\x18\x01 \x03(\v2\".host.installation.v1.InstallationR\rinstallations\"\xb0\x03\n" + "\fInstallation\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\x12A\n" + "\x06status\x18\x04 \x01(\x0e2).host.installation.v1.Installation.StatusR\x06status\x12\x1a\n" + "\brevision\x18\x05 \x01(\x04R\brevision\x12\x12\n" + - "\x04role\x18\x06 \x01(\tR\x04role\x12\x19\n" + - "\bteam_ids\x18\a \x03(\tR\ateamIds\x12,\n" + - "\x12agent_principal_id\x18\b \x01(\tR\x10agentPrincipalId\x12\x1c\n" + - "\tnamespace\x18\t \x01(\tR\tnamespace\"[\n" + + "\x04role\x18\x06 \x01(\tR\x04role\x12\x1c\n" + + "\tnamespace\x18\t \x01(\tR\tnamespace\x12!\n" + + "\fprincipal_id\x18\n" + + " \x01(\tR\vprincipalId\x12\x14\n" + + "\x05roles\x18\v \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\x03\"\\\n" + + "\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\x96\x02\n" + + "\x0fInstallationAck2\xda\x03\n" + "\x13InstallationService\x12S\n" + - "\aInstall\x12$.host.installation.v1.InstallRequest\x1a\".host.installation.v1.Installation\x12[\n" + - "\vGrantToTeam\x12(.host.installation.v1.GrantToTeamRequest\x1a\".host.installation.v1.Installation\x12M\n" + + "\aInstall\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" + + "\n" + + "RevokeRole\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" + "\aInstall\x12(.host.installation.v1.InstallationNotice\x1a%.host.installation.v1.InstallationAckB\xf7\x01\n" + @@ -575,34 +867,43 @@ 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, 7) +var file_host_installation_v1_installation_proto_msgTypes = make([]protoimpl.MessageInfo, 11) 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 - (*GrantToTeamRequest)(nil), // 2: host.installation.v1.GrantToTeamRequest - (*ListRequest)(nil), // 3: host.installation.v1.ListRequest - (*ListResponse)(nil), // 4: host.installation.v1.ListResponse - (*Installation)(nil), // 5: host.installation.v1.Installation - (*InstallationNotice)(nil), // 6: host.installation.v1.InstallationNotice - (*InstallationAck)(nil), // 7: host.installation.v1.InstallationAck + (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 } var file_host_installation_v1_installation_proto_depIdxs = []int32{ - 5, // 0: host.installation.v1.ListResponse.installations:type_name -> host.installation.v1.Installation - 0, // 1: host.installation.v1.Installation.status:type_name -> host.installation.v1.Installation.Status - 5, // 2: host.installation.v1.InstallationNotice.installation:type_name -> host.installation.v1.Installation - 1, // 3: host.installation.v1.InstallationService.Install:input_type -> host.installation.v1.InstallRequest - 2, // 4: host.installation.v1.InstallationService.GrantToTeam:input_type -> host.installation.v1.GrantToTeamRequest - 3, // 5: host.installation.v1.InstallationService.List:input_type -> host.installation.v1.ListRequest - 6, // 6: host.installation.v1.InstallableService.Install:input_type -> host.installation.v1.InstallationNotice - 5, // 7: host.installation.v1.InstallationService.Install:output_type -> host.installation.v1.Installation - 5, // 8: host.installation.v1.InstallationService.GrantToTeam:output_type -> host.installation.v1.Installation - 4, // 9: host.installation.v1.InstallationService.List:output_type -> host.installation.v1.ListResponse - 7, // 10: host.installation.v1.InstallableService.Install:output_type -> host.installation.v1.InstallationAck - 7, // [7:11] is the sub-list for method output_type - 3, // [3:7] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 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 } func init() { file_host_installation_v1_installation_proto_init() } @@ -616,7 +917,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: 7, + NumMessages: 11, 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 8d5c6fea..d6a5f8f9 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 @@ -5,8 +5,10 @@ // source: host/installation/v1/installation.proto // The installation interface: what is installed at an organisation and who may -// use it. Three operations on the host, and one the solution implements, the -// install step the host calls once per installation. +// use it. An installed solution is a principal of its organisation; its roles +// are levels on the data that principal owns, and a grant of a role is the +// access. Operations on the host, and one the solution implements, the install +// step the host calls once per installation. package installationv1 @@ -24,9 +26,11 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - InstallationService_Install_FullMethodName = "/host.installation.v1.InstallationService/Install" - InstallationService_GrantToTeam_FullMethodName = "/host.installation.v1.InstallationService/GrantToTeam" - InstallationService_List_FullMethodName = "/host.installation.v1.InstallationService/List" + InstallationService_Install_FullMethodName = "/host.installation.v1.InstallationService/Install" + InstallationService_GrantRole_FullMethodName = "/host.installation.v1.InstallationService/GrantRole" + InstallationService_RevokeRole_FullMethodName = "/host.installation.v1.InstallationService/RevokeRole" + InstallationService_ListRoleGrants_FullMethodName = "/host.installation.v1.InstallationService/ListRoleGrants" + InstallationService_List_FullMethodName = "/host.installation.v1.InstallationService/List" ) // InstallationServiceClient is the client API for InstallationService service. @@ -38,8 +42,17 @@ 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) - // GrantToTeam exposes an installation to a team under the installation's role. - GrantToTeam(ctx context.Context, in *GrantToTeamRequest, opts ...grpc.CallOption) (*Installation, 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 + // of the installer role on that installation, grants. + GrantRole(ctx context.Context, in *GrantRoleRequest, opts ...grpc.CallOption) (*Installation, error) + // RevokeRole ends one grant; every capability its subject holds from it is + // refused at once. Idempotent; the same callers as GrantRole. + RevokeRole(ctx context.Context, in *RevokeRoleRequest, opts ...grpc.CallOption) (*Installation, error) + // ListRoleGrants answers who holds which role on the installation; the same + // callers as GrantRole. + ListRoleGrants(ctx context.Context, in *ListRoleGrantsRequest, opts ...grpc.CallOption) (*ListRoleGrantsResponse, error) // List answers what one organisation has installed: an administrator's read. A // member's view, which installed solutions they may use, is their navigation and // belongs to the entitlement read, not here. @@ -64,10 +77,30 @@ func (c *installationServiceClient) Install(ctx context.Context, in *InstallRequ return out, nil } -func (c *installationServiceClient) GrantToTeam(ctx context.Context, in *GrantToTeamRequest, opts ...grpc.CallOption) (*Installation, error) { +func (c *installationServiceClient) GrantRole(ctx context.Context, in *GrantRoleRequest, opts ...grpc.CallOption) (*Installation, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Installation) - err := c.cc.Invoke(ctx, InstallationService_GrantToTeam_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, InstallationService_GrantRole_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *installationServiceClient) RevokeRole(ctx context.Context, in *RevokeRoleRequest, opts ...grpc.CallOption) (*Installation, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Installation) + err := c.cc.Invoke(ctx, InstallationService_RevokeRole_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *installationServiceClient) ListRoleGrants(ctx context.Context, in *ListRoleGrantsRequest, opts ...grpc.CallOption) (*ListRoleGrantsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListRoleGrantsResponse) + err := c.cc.Invoke(ctx, InstallationService_ListRoleGrants_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -93,8 +126,17 @@ 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) - // GrantToTeam exposes an installation to a team under the installation's role. - GrantToTeam(context.Context, *GrantToTeamRequest) (*Installation, 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 + // of the installer role on that installation, grants. + GrantRole(context.Context, *GrantRoleRequest) (*Installation, error) + // RevokeRole ends one grant; every capability its subject holds from it is + // refused at once. Idempotent; the same callers as GrantRole. + RevokeRole(context.Context, *RevokeRoleRequest) (*Installation, error) + // ListRoleGrants answers who holds which role on the installation; the same + // callers as GrantRole. + ListRoleGrants(context.Context, *ListRoleGrantsRequest) (*ListRoleGrantsResponse, error) // List answers what one organisation has installed: an administrator's read. A // member's view, which installed solutions they may use, is their navigation and // belongs to the entitlement read, not here. @@ -112,8 +154,14 @@ type UnimplementedInstallationServiceServer struct{} func (UnimplementedInstallationServiceServer) Install(context.Context, *InstallRequest) (*Installation, error) { return nil, status.Error(codes.Unimplemented, "method Install not implemented") } -func (UnimplementedInstallationServiceServer) GrantToTeam(context.Context, *GrantToTeamRequest) (*Installation, error) { - return nil, status.Error(codes.Unimplemented, "method GrantToTeam not implemented") +func (UnimplementedInstallationServiceServer) GrantRole(context.Context, *GrantRoleRequest) (*Installation, error) { + return nil, status.Error(codes.Unimplemented, "method GrantRole not implemented") +} +func (UnimplementedInstallationServiceServer) RevokeRole(context.Context, *RevokeRoleRequest) (*Installation, error) { + return nil, status.Error(codes.Unimplemented, "method RevokeRole not implemented") +} +func (UnimplementedInstallationServiceServer) ListRoleGrants(context.Context, *ListRoleGrantsRequest) (*ListRoleGrantsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListRoleGrants not implemented") } func (UnimplementedInstallationServiceServer) List(context.Context, *ListRequest) (*ListResponse, error) { return nil, status.Error(codes.Unimplemented, "method List not implemented") @@ -157,20 +205,56 @@ func _InstallationService_Install_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } -func _InstallationService_GrantToTeam_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GrantToTeamRequest) +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 { return nil, err } if interceptor == nil { - return srv.(InstallationServiceServer).GrantToTeam(ctx, in) + return srv.(InstallationServiceServer).GrantRole(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: InstallationService_GrantToTeam_FullMethodName, + FullMethod: InstallationService_GrantRole_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(InstallationServiceServer).GrantToTeam(ctx, req.(*GrantToTeamRequest)) + return srv.(InstallationServiceServer).GrantRole(ctx, req.(*GrantRoleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _InstallationService_RevokeRole_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RevokeRoleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(InstallationServiceServer).RevokeRole(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: InstallationService_RevokeRole_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(InstallationServiceServer).RevokeRole(ctx, req.(*RevokeRoleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _InstallationService_ListRoleGrants_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListRoleGrantsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(InstallationServiceServer).ListRoleGrants(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: InstallationService_ListRoleGrants_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(InstallationServiceServer).ListRoleGrants(ctx, req.(*ListRoleGrantsRequest)) } return interceptor(ctx, in, info, handler) } @@ -205,8 +289,16 @@ var InstallationService_ServiceDesc = grpc.ServiceDesc{ Handler: _InstallationService_Install_Handler, }, { - MethodName: "GrantToTeam", - Handler: _InstallationService_GrantToTeam_Handler, + MethodName: "GrantRole", + Handler: _InstallationService_GrantRole_Handler, + }, + { + MethodName: "RevokeRole", + Handler: _InstallationService_RevokeRole_Handler, + }, + { + MethodName: "ListRoleGrants", + Handler: _InstallationService_ListRoleGrants_Handler, }, { MethodName: "List", diff --git a/libraries/authority-client/go/go.mod b/libraries/authority-client/go/go.mod index 66cafb8a..6fa4755b 100644 --- a/libraries/authority-client/go/go.mod +++ b/libraries/authority-client/go/go.mod @@ -4,7 +4,7 @@ go 1.27.3 require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 - github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435 + github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515 github.com/codefly-dev/sdk-go v0.4.0 google.golang.org/genproto/googleapis/api v0.0.0-20261019180522-ce908d29299b google.golang.org/grpc v1.84.0 diff --git a/libraries/authority-client/go/go.sum b/libraries/authority-client/go/go.sum index 5e631158..4b511f53 100644 --- a/libraries/authority-client/go/go.sum +++ b/libraries/authority-client/go/go.sum @@ -28,8 +28,8 @@ github.com/cheggaaa/pb/v3 v3.2.1 h1:aprZbFRG+B7+ug76S8QZ6Y1PW168UHzOmbC3wa+aU6I= github.com/cheggaaa/pb/v3 v3.2.1/go.mod h1:U9hSVxoKqJrZIE3PkFG1xXXNaK/Ilzg+EF/scpXzEdw= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435 h1:3M1c4Ver3YRaHj4rk1+mj1uweqTF5bO2QHZ50rI7sNI= -github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= +github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515 h1:u6dYtxKTnEYeOwlgRx3sfJbxDl6UWI4uhpvUqOR4Ho0= +github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= github.com/codefly-dev/sdk-go v0.4.0 h1:3Jm0bP+UsLI4iZYp3wqVa42bcXBumMIIF/aeweKOAhY= diff --git a/libraries/authority-client/go/workcontext/owner_test.go b/libraries/authority-client/go/workcontext/owner_test.go index 66311bc0..3491ce20 100644 --- a/libraries/authority-client/go/workcontext/owner_test.go +++ b/libraries/authority-client/go/workcontext/owner_test.go @@ -1,6 +1,12 @@ package workcontext_test import ( + "bytes" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "strings" "testing" reader "github.com/codefly-dev/authority/libraries/authority-client/go/workcontext" @@ -92,3 +98,61 @@ func TestAChainThatDropsOrChangesTheOwnerIsRefused(t *testing.T) { }) } } + +type ownerVector struct { + Name string `json:"name"` + Owner string `json:"owner"` + Repeat int `json:"repeat"` +} + +func (v ownerVector) value() string { + if v.Repeat > 0 { + return strings.Repeat(v.Owner, v.Repeat) + } + return v.Owner +} + +// The authority's readers take core's resource_owner_id rule exactly: every +// vector core accepts authenticates, every one it refuses does not. The +// vectors are core's own file, at the pinned core, byte for byte. +func TestResourceOwnerVectorsAreCores(t *testing.T) { + ours, err := os.ReadFile("../../testdata/resource_owner_vectors.json") + if err != nil { + t.Fatal(err) + } + dir, err := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", "github.com/codefly-dev/core").Output() + if err != nil { + t.Fatal(err) + } + cores, err := os.ReadFile(filepath.Join(strings.TrimSpace(string(dir)), "workcontext", "testdata", "resource_owner_vectors.json")) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(ours, cores) { + t.Fatal("testdata/resource_owner_vectors.json differs from the pinned core's; copy it again") + } + var vectors struct{ Accepted, Refused []ownerVector } + if err := json.Unmarshal(ours, &vectors); err != nil { + t.Fatal(err) + } + authenticate := func(owner string) error { + c := body(t, "session") + c.AuthorityScopes = []*basev0.WorkScopeV1{ownedScope(t, "record", "read", owner)} + _, err := reader.Authenticate(stampedToken(tokenFor(t, c)), "test-proof", c.GetAudience(), trustFor(c)) + return err + } + for _, v := range vectors.Accepted { + t.Run("accepts "+v.Name, func(t *testing.T) { + if err := authenticate(v.value()); err != nil { + t.Fatal(err) + } + }) + } + for _, v := range vectors.Refused { + t.Run("refuses "+v.Name, func(t *testing.T) { + if err := authenticate(v.value()); err == nil { + t.Fatal("authenticated") + } + }) + } +} diff --git a/libraries/authority-client/library.codefly.yaml b/libraries/authority-client/library.codefly.yaml index cf9d5b9f..62737a8f 100644 --- a/libraries/authority-client/library.codefly.yaml +++ b/libraries/authority-client/library.codefly.yaml @@ -20,10 +20,10 @@ sources: module: authority service: local endpoint: grpc - contract-digest: sha256:05b735abdd6a67ad7365c54fcbd82b8b8412796735ada6323dd43d51bd5f3044 + contract-digest: sha256:4d92544f5e186880be4d17079ad6dc056a6f9a84a0e1ebb298a2ff9b72863a46 services: - LocalService generated-by: - codefly: 0.1.214 + codefly: development companion: ghcr.io/codefly-dev/proto:0.0.17 facade: false diff --git a/libraries/authority-client/python/authority_client/_gen/host/authority/v1/authority_pb2.py b/libraries/authority-client/python/authority_client/_gen/host/authority/v1/authority_pb2.py index c5c2585e..ee576049 100644 --- a/libraries/authority-client/python/authority_client/_gen/host/authority/v1/authority_pb2.py +++ b/libraries/authority-client/python/authority_client/_gen/host/authority/v1/authority_pb2.py @@ -25,7 +25,7 @@ from authority_client._gen.host.authority.v1 import work_context_pb2 as host_dot_authority_dot_v1_dot_work__context__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!host/authority/v1/authority.proto\x12\x11host.authority.v1\x1a$host/authority/v1/work_context.proto\"\xf3\x04\n\x0cIssueRequest\x12:\n\x06person\x18\x01 \x01(\x0b\x32 .host.authority.v1.PersonSessionH\x00R\x06person\x12\x43\n\x08workload\x18\x02 \x01(\x0b\x32%.host.authority.v1.WorkloadCredentialH\x00R\x08workload\x12H\n\x0cinstallation\x18\x03 \x01(\x0b\x32\".host.authority.v1.InstallationRefH\x00R\x0cinstallation\x12#\n\x0cparent_token\x18\x04 \x01(\tH\x00R\x0bparentToken\x12\x42\n\ndelegation\x18\x0b \x01(\x0b\x32 .host.authority.v1.DelegationRefH\x00R\ndelegation\x12\x1a\n\x08\x61udience\x18\x05 \x01(\tR\x08\x61udience\x12\x36\n\x06scopes\x18\x06 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x06scopes\x12\x1f\n\x0bttl_seconds\x18\x07 \x01(\x03R\nttlSeconds\x12\x30\n\x14operation_binding_id\x18\x08 \x01(\tR\x12operationBindingId\x12\'\n\x0forganization_id\x18\t \x01(\tR\x0eorganizationId\x12;\n\x05\x61\x63tor\x18\n \x01(\x0b\x32%.host.authority.v1.WorkloadCredentialR\x05\x61\x63tor\x12\x1c\n\toperation\x18\x0c \x01(\tR\toperationB\x04\n\x02\x61s\"4\n\rDelegationRef\x12#\n\rdelegation_id\x18\x01 \x01(\tR\x0c\x64\x65legationId\"{\n\x0f\x44\x65legateRequest\x12\x16\n\x06module\x18\x01 \x01(\tR\x06module\x12\x36\n\x06scopes\x18\x02 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x06scopes\x12\x18\n\x07purpose\x18\x03 \x01(\tR\x07purpose\"\xf6\x01\n\nDelegation\x12#\n\rdelegation_id\x18\x01 \x01(\tR\x0c\x64\x65legationId\x12\'\n\x0forganization_id\x18\x02 \x01(\tR\x0eorganizationId\x12\x18\n\x07grantor\x18\x03 \x01(\tR\x07grantor\x12\x16\n\x06module\x18\x04 \x01(\tR\x06module\x12\x36\n\x06scopes\x18\x05 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x06scopes\x12\x18\n\x07purpose\x18\x06 \x01(\tR\x07purpose\x12\x16\n\x06\x61\x63tive\x18\x07 \x01(\x08R\x06\x61\x63tive\"x\n\x14GetDelegationRequest\x12#\n\rdelegation_id\x18\x01 \x01(\tR\x0c\x64\x65legationId\x12;\n\x05\x61\x63tor\x18\x02 \x01(\x0b\x32%.host.authority.v1.WorkloadCredentialR\x05\x61\x63tor\">\n\x17RevokeDelegationRequest\x12#\n\rdelegation_id\x18\x01 \x01(\tR\x0c\x64\x65legationId\"Y\n\x18RevokeDelegationResponse\x12=\n\ndelegation\x18\x01 \x01(\x0b\x32\x1d.host.authority.v1.DelegationR\ndelegation\"!\n\rPersonSessionJ\x04\x08\x01\x10\x02R\nsession_id\"4\n\x12WorkloadCredential\x12\x1e\n\ncredential\x18\x01 \x01(\tR\ncredential\":\n\x0fInstallationRef\x12\'\n\x0finstallation_id\x18\x01 \x01(\tR\x0einstallationId\"\x89\x01\n\rIssueResponse\x12\x14\n\x05token\x18\x01 \x01(\tR\x05token\x12&\n\x0f\x65xpires_at_unix\x18\x02 \x01(\x03R\rexpiresAtUnix\x12:\n\x07\x63ontext\x18\x03 \x01(\x0b\x32 .host.authority.v1.WorkContextV1R\x07\x63ontext\"t\n\x0c\x43heckRequest\x12\x14\n\x05token\x18\x01 \x01(\tR\x05token\x12\x1a\n\x08\x61udience\x18\x02 \x01(\tR\x08\x61udience\x12\x16\n\x06method\x18\x03 \x01(\tR\x06method\x12\x1a\n\x08\x61\x64visory\x18\x04 \x01(\x08R\x08\x61\x64visory\"\x9c\x01\n\rCheckResponse\x12:\n\x07\x63ontext\x18\x01 \x01(\x0b\x32 .host.authority.v1.WorkContextV1R\x07\x63ontext\x12\x35\n\x16\x61uthorization_revision\x18\x02 \x01(\x04R\x15\x61uthorizationRevision\x12\x18\n\x07refusal\x18\x03 \x01(\tR\x07refusal\"\r\n\x0bKeysRequest\"X\n\x0cKeysResponse\x12\x16\n\x06issuer\x18\x01 \x01(\tR\x06issuer\x12\x30\n\x04keys\x18\x02 \x03(\x0b\x32\x1c.host.authority.v1.PublicKeyR\x04keys\"_\n\tPublicKey\x12\x15\n\x06key_id\x18\x01 \x01(\tR\x05keyId\x12\x1c\n\talgorithm\x18\x02 \x01(\tR\talgorithm\x12\x1d\n\npublic_key\x18\x03 \x01(\x0cR\tpublicKey\"\x85\x01\n\x14ListResourcesRequest\x12\x14\n\x05token\x18\x01 \x01(\tR\x05token\x12\x1a\n\x08\x61udience\x18\x02 \x01(\tR\x08\x61udience\x12#\n\rresource_kind\x18\x03 \x01(\tR\x0cresourceKind\x12\x16\n\x06\x61\x63tion\x18\x04 \x01(\tR\x06\x61\x63tion\"Q\n\x15ListResourcesResponse\x12\x38\n\x06grants\x18\x01 \x03(\x0b\x32 .host.authority.v1.ResourceGrantR\x06grants\"t\n\rResourceGrant\x12\x12\n\x03\x61ll\x18\x01 \x01(\x08H\x00R\x03\x61ll\x12\x10\n\x02id\x18\x02 \x01(\tH\x00R\x02id\x12\x18\n\x06prefix\x18\x03 \x01(\tH\x00R\x06prefix\x12\x1a\n\x07pattern\x18\x04 \x01(\tH\x00R\x07patternB\x07\n\x05match2\xec\x04\n\x10\x41uthorityService\x12J\n\x05Issue\x12\x1f.host.authority.v1.IssueRequest\x1a .host.authority.v1.IssueResponse\x12J\n\x05\x43heck\x12\x1f.host.authority.v1.CheckRequest\x1a .host.authority.v1.CheckResponse\x12G\n\x04Keys\x12\x1e.host.authority.v1.KeysRequest\x1a\x1f.host.authority.v1.KeysResponse\x12\x62\n\rListResources\x12\'.host.authority.v1.ListResourcesRequest\x1a(.host.authority.v1.ListResourcesResponse\x12M\n\x08\x44\x65legate\x12\".host.authority.v1.DelegateRequest\x1a\x1d.host.authority.v1.Delegation\x12k\n\x10RevokeDelegation\x12*.host.authority.v1.RevokeDelegationRequest\x1a+.host.authority.v1.RevokeDelegationResponse\x12W\n\rGetDelegation\x12\'.host.authority.v1.GetDelegationRequest\x1a\x1d.host.authority.v1.DelegationB/Z-gateway/pkg/gen/host/authority/v1;authorityv1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!host/authority/v1/authority.proto\x12\x11host.authority.v1\x1a$host/authority/v1/work_context.proto\"\xf3\x04\n\x0cIssueRequest\x12:\n\x06person\x18\x01 \x01(\x0b\x32 .host.authority.v1.PersonSessionH\x00R\x06person\x12\x43\n\x08workload\x18\x02 \x01(\x0b\x32%.host.authority.v1.WorkloadCredentialH\x00R\x08workload\x12H\n\x0cinstallation\x18\x03 \x01(\x0b\x32\".host.authority.v1.InstallationRefH\x00R\x0cinstallation\x12#\n\x0cparent_token\x18\x04 \x01(\tH\x00R\x0bparentToken\x12\x42\n\ndelegation\x18\x0b \x01(\x0b\x32 .host.authority.v1.DelegationRefH\x00R\ndelegation\x12\x1a\n\x08\x61udience\x18\x05 \x01(\tR\x08\x61udience\x12\x36\n\x06scopes\x18\x06 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x06scopes\x12\x1f\n\x0bttl_seconds\x18\x07 \x01(\x03R\nttlSeconds\x12\x30\n\x14operation_binding_id\x18\x08 \x01(\tR\x12operationBindingId\x12\'\n\x0forganization_id\x18\t \x01(\tR\x0eorganizationId\x12;\n\x05\x61\x63tor\x18\n \x01(\x0b\x32%.host.authority.v1.WorkloadCredentialR\x05\x61\x63tor\x12\x1c\n\toperation\x18\x0c \x01(\tR\toperationB\x04\n\x02\x61s\"4\n\rDelegationRef\x12#\n\rdelegation_id\x18\x01 \x01(\tR\x0c\x64\x65legationId\"{\n\x0f\x44\x65legateRequest\x12\x16\n\x06module\x18\x01 \x01(\tR\x06module\x12\x36\n\x06scopes\x18\x02 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x06scopes\x12\x18\n\x07purpose\x18\x03 \x01(\tR\x07purpose\"\xf6\x01\n\nDelegation\x12#\n\rdelegation_id\x18\x01 \x01(\tR\x0c\x64\x65legationId\x12\'\n\x0forganization_id\x18\x02 \x01(\tR\x0eorganizationId\x12\x18\n\x07grantor\x18\x03 \x01(\tR\x07grantor\x12\x16\n\x06module\x18\x04 \x01(\tR\x06module\x12\x36\n\x06scopes\x18\x05 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x06scopes\x12\x18\n\x07purpose\x18\x06 \x01(\tR\x07purpose\x12\x16\n\x06\x61\x63tive\x18\x07 \x01(\x08R\x06\x61\x63tive\"x\n\x14GetDelegationRequest\x12#\n\rdelegation_id\x18\x01 \x01(\tR\x0c\x64\x65legationId\x12;\n\x05\x61\x63tor\x18\x02 \x01(\x0b\x32%.host.authority.v1.WorkloadCredentialR\x05\x61\x63tor\">\n\x17RevokeDelegationRequest\x12#\n\rdelegation_id\x18\x01 \x01(\tR\x0c\x64\x65legationId\"Y\n\x18RevokeDelegationResponse\x12=\n\ndelegation\x18\x01 \x01(\x0b\x32\x1d.host.authority.v1.DelegationR\ndelegation\"!\n\rPersonSessionJ\x04\x08\x01\x10\x02R\nsession_id\"4\n\x12WorkloadCredential\x12\x1e\n\ncredential\x18\x01 \x01(\tR\ncredential\":\n\x0fInstallationRef\x12\'\n\x0finstallation_id\x18\x01 \x01(\tR\x0einstallationId\"\x89\x01\n\rIssueResponse\x12\x14\n\x05token\x18\x01 \x01(\tR\x05token\x12&\n\x0f\x65xpires_at_unix\x18\x02 \x01(\x03R\rexpiresAtUnix\x12:\n\x07\x63ontext\x18\x03 \x01(\x0b\x32 .host.authority.v1.WorkContextV1R\x07\x63ontext\"t\n\x0c\x43heckRequest\x12\x14\n\x05token\x18\x01 \x01(\tR\x05token\x12\x1a\n\x08\x61udience\x18\x02 \x01(\tR\x08\x61udience\x12\x16\n\x06method\x18\x03 \x01(\tR\x06method\x12\x1a\n\x08\x61\x64visory\x18\x04 \x01(\x08R\x08\x61\x64visory\"\x9c\x01\n\rCheckResponse\x12:\n\x07\x63ontext\x18\x01 \x01(\x0b\x32 .host.authority.v1.WorkContextV1R\x07\x63ontext\x12\x35\n\x16\x61uthorization_revision\x18\x02 \x01(\x04R\x15\x61uthorizationRevision\x12\x18\n\x07refusal\x18\x03 \x01(\tR\x07refusal\"\r\n\x0bKeysRequest\"X\n\x0cKeysResponse\x12\x16\n\x06issuer\x18\x01 \x01(\tR\x06issuer\x12\x30\n\x04keys\x18\x02 \x03(\x0b\x32\x1c.host.authority.v1.PublicKeyR\x04keys\"_\n\tPublicKey\x12\x15\n\x06key_id\x18\x01 \x01(\tR\x05keyId\x12\x1c\n\talgorithm\x18\x02 \x01(\tR\talgorithm\x12\x1d\n\npublic_key\x18\x03 \x01(\x0cR\tpublicKey\"\x85\x01\n\x14ListResourcesRequest\x12\x14\n\x05token\x18\x01 \x01(\tR\x05token\x12\x1a\n\x08\x61udience\x18\x02 \x01(\tR\x08\x61udience\x12#\n\rresource_kind\x18\x03 \x01(\tR\x0cresourceKind\x12\x16\n\x06\x61\x63tion\x18\x04 \x01(\tR\x06\x61\x63tion\"Q\n\x15ListResourcesResponse\x12\x38\n\x06grants\x18\x01 \x03(\x0b\x32 .host.authority.v1.ResourceGrantR\x06grants\"\xff\x01\n\x18\x41uthorizeTransferRequest\x12\x14\n\x05token\x18\x01 \x01(\tR\x05token\x12;\n\x05\x61\x63tor\x18\x02 \x01(\x0b\x32%.host.authority.v1.WorkloadCredentialR\x05\x61\x63tor\x12#\n\rresource_kind\x18\x03 \x01(\tR\x0cresourceKind\x12\x1f\n\x0bresource_id\x18\x04 \x01(\tR\nresourceId\x12\"\n\rfrom_owner_id\x18\x05 \x01(\tR\x0b\x66romOwnerId\x12&\n\x0fto_principal_id\x18\x06 \x01(\tR\rtoPrincipalId\"c\n\x19\x41uthorizeTransferResponse\x12!\n\x0cprincipal_id\x18\x01 \x01(\tR\x0bprincipalId\x12#\n\rauthorized_by\x18\x02 \x01(\tR\x0c\x61uthorizedBy\"\xa0\x01\n\rResourceGrant\x12\x12\n\x03\x61ll\x18\x01 \x01(\x08H\x00R\x03\x61ll\x12\x10\n\x02id\x18\x02 \x01(\tH\x00R\x02id\x12\x18\n\x06prefix\x18\x03 \x01(\tH\x00R\x06prefix\x12\x1a\n\x07pattern\x18\x04 \x01(\tH\x00R\x07pattern\x12*\n\x11resource_owner_id\x18\x05 \x01(\tR\x0fresourceOwnerIdB\x07\n\x05match2\xdc\x05\n\x10\x41uthorityService\x12J\n\x05Issue\x12\x1f.host.authority.v1.IssueRequest\x1a .host.authority.v1.IssueResponse\x12J\n\x05\x43heck\x12\x1f.host.authority.v1.CheckRequest\x1a .host.authority.v1.CheckResponse\x12G\n\x04Keys\x12\x1e.host.authority.v1.KeysRequest\x1a\x1f.host.authority.v1.KeysResponse\x12\x62\n\rListResources\x12\'.host.authority.v1.ListResourcesRequest\x1a(.host.authority.v1.ListResourcesResponse\x12n\n\x11\x41uthorizeTransfer\x12+.host.authority.v1.AuthorizeTransferRequest\x1a,.host.authority.v1.AuthorizeTransferResponse\x12M\n\x08\x44\x65legate\x12\".host.authority.v1.DelegateRequest\x1a\x1d.host.authority.v1.Delegation\x12k\n\x10RevokeDelegation\x12*.host.authority.v1.RevokeDelegationRequest\x1a+.host.authority.v1.RevokeDelegationResponse\x12W\n\rGetDelegation\x12\'.host.authority.v1.GetDelegationRequest\x1a\x1d.host.authority.v1.DelegationB/Z-gateway/pkg/gen/host/authority/v1;authorityv1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -69,8 +69,12 @@ _globals['_LISTRESOURCESREQUEST']._serialized_end=2331 _globals['_LISTRESOURCESRESPONSE']._serialized_start=2333 _globals['_LISTRESOURCESRESPONSE']._serialized_end=2414 - _globals['_RESOURCEGRANT']._serialized_start=2416 - _globals['_RESOURCEGRANT']._serialized_end=2532 - _globals['_AUTHORITYSERVICE']._serialized_start=2535 - _globals['_AUTHORITYSERVICE']._serialized_end=3155 + _globals['_AUTHORIZETRANSFERREQUEST']._serialized_start=2417 + _globals['_AUTHORIZETRANSFERREQUEST']._serialized_end=2672 + _globals['_AUTHORIZETRANSFERRESPONSE']._serialized_start=2674 + _globals['_AUTHORIZETRANSFERRESPONSE']._serialized_end=2773 + _globals['_RESOURCEGRANT']._serialized_start=2776 + _globals['_RESOURCEGRANT']._serialized_end=2936 + _globals['_AUTHORITYSERVICE']._serialized_start=2939 + _globals['_AUTHORITYSERVICE']._serialized_end=3671 # @@protoc_insertion_point(module_scope) diff --git a/libraries/authority-client/python/authority_client/_gen/host/authority/v1/authority_pb2_grpc.py b/libraries/authority-client/python/authority_client/_gen/host/authority/v1/authority_pb2_grpc.py index 43cd9d78..349fb362 100644 --- a/libraries/authority-client/python/authority_client/_gen/host/authority/v1/authority_pb2_grpc.py +++ b/libraries/authority-client/python/authority_client/_gen/host/authority/v1/authority_pb2_grpc.py @@ -55,6 +55,11 @@ def __init__(self, channel): request_serializer=host_dot_authority_dot_v1_dot_authority__pb2.ListResourcesRequest.SerializeToString, response_deserializer=host_dot_authority_dot_v1_dot_authority__pb2.ListResourcesResponse.FromString, _registered_method=True) + self.AuthorizeTransfer = channel.unary_unary( + '/host.authority.v1.AuthorityService/AuthorizeTransfer', + request_serializer=host_dot_authority_dot_v1_dot_authority__pb2.AuthorizeTransferRequest.SerializeToString, + response_deserializer=host_dot_authority_dot_v1_dot_authority__pb2.AuthorizeTransferResponse.FromString, + _registered_method=True) self.Delegate = channel.unary_unary( '/host.authority.v1.AuthorityService/Delegate', request_serializer=host_dot_authority_dot_v1_dot_authority__pb2.DelegateRequest.SerializeToString, @@ -112,6 +117,24 @@ def ListResources(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def AuthorizeTransfer(self, request, context): + """AuthorizeTransfer decides whether an existing resource may join an + installed solution's principal. The caller is the kind's one serving + module, which attests the resource exists and who owns it now + (from_owner_id), presenting the capability of the person asking: issued + through the target principal's installation and addressed to the caller. + It is allowed when the current owner is the organization or a person of + it (never another solution principal, another organization or an unknown + owner), the person holds admin on that resource from their own + organization grants, and the person's current roles on the principal hold + write on the kind, all read live. An installation acting for itself never + transfers. The store then records the principal as owner; the original + creator stays the author. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def Delegate(self, request, context): """Delegate records a signed-in person's grant to a module (the caller's bearer is the person); Issue with a delegation acts on it. @@ -160,6 +183,11 @@ def add_AuthorityServiceServicer_to_server(servicer, server): request_deserializer=host_dot_authority_dot_v1_dot_authority__pb2.ListResourcesRequest.FromString, response_serializer=host_dot_authority_dot_v1_dot_authority__pb2.ListResourcesResponse.SerializeToString, ), + 'AuthorizeTransfer': grpc.unary_unary_rpc_method_handler( + servicer.AuthorizeTransfer, + request_deserializer=host_dot_authority_dot_v1_dot_authority__pb2.AuthorizeTransferRequest.FromString, + response_serializer=host_dot_authority_dot_v1_dot_authority__pb2.AuthorizeTransferResponse.SerializeToString, + ), 'Delegate': grpc.unary_unary_rpc_method_handler( servicer.Delegate, request_deserializer=host_dot_authority_dot_v1_dot_authority__pb2.DelegateRequest.FromString, @@ -295,6 +323,33 @@ def ListResources(request, metadata, _registered_method=True) + @staticmethod + def AuthorizeTransfer(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.authority.v1.AuthorityService/AuthorizeTransfer', + host_dot_authority_dot_v1_dot_authority__pb2.AuthorizeTransferRequest.SerializeToString, + host_dot_authority_dot_v1_dot_authority__pb2.AuthorizeTransferResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + @staticmethod def Delegate(request, target, diff --git a/libraries/authority-client/python/authority_client/_gen/host/composition/v1/composition_pb2.py b/libraries/authority-client/python/authority_client/_gen/host/composition/v1/composition_pb2.py index fca4ced2..94c22ac4 100644 --- a/libraries/authority-client/python/authority_client/_gen/host/composition/v1/composition_pb2.py +++ b/libraries/authority-client/python/authority_client/_gen/host/composition/v1/composition_pb2.py @@ -25,7 +25,7 @@ from authority_client._gen.host.authority.v1 import work_context_pb2 as host_dot_authority_dot_v1_dot_work__context__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%host/composition/v1/composition.proto\x12\x13host.composition.v1\x1a$host/authority/v1/work_context.proto\"\xff\x01\n\x14\x41pplyManifestRequest\x12\x41\n\x08manifest\x18\x01 \x01(\x0b\x32%.host.composition.v1.ModuleManifestV1R\x08manifest\x12?\n\x07\x63\x61rrier\x18\x02 \x01(\x0b\x32%.host.composition.v1.SignedManifestV1R\x07\x63\x61rrier\x12\x1e\n\ncredential\x18\x03 \x01(\tR\ncredential\x12\x43\n\tauthority\x18\x04 \x01(\x0b\x32%.host.composition.v1.SignedManifestV1R\tauthority\"\xb8\x02\n\x13\x41uthorityManifestV1\x12\x16\n\x06module\x18\x01 \x01(\tR\x06module\x12\x1e\n\ngeneration\x18\x02 \x01(\x04R\ngeneration\x12\x30\n\x14\x63ore_manifest_digest\x18\x03 \x01(\tR\x12\x63oreManifestDigest\x12=\n\x07methods\x18\x04 \x03(\x0b\x32#.host.composition.v1.MethodPolicyV1R\x07methods\x12\x45\n\x0escope_ceilings\x18\x05 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\rscopeCeilings\x12\x31\n\x05roles\x18\x06 \x03(\x0b\x32\x1b.host.composition.v1.RoleV1R\x05roles\"\x92\x01\n\x0eMethodPolicyV1\x12\x16\n\x06method\x18\x01 \x01(\tR\x06method\x12:\n\x08requires\x18\x02 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x08requires\x12,\n\x12\x61ny_checked_caller\x18\x03 \x01(\x08R\x10\x61nyCheckedCaller\"J\n\x10SignedManifestV1\x12\x1a\n\x08\x64ocument\x18\x01 \x01(\x0cR\x08\x64ocument\x12\x1a\n\x08\x65nvelope\x18\x02 \x01(\x0cR\x08\x65nvelope\"\x80\x01\n\x15\x41pplyManifestResponse\x12\x1c\n\tprincipal\x18\x01 \x01(\tR\tprincipal\x12\x1e\n\ngeneration\x18\x02 \x01(\x04R\ngeneration\x12)\n\x10installation_ids\x18\x03 \x03(\tR\x0finstallationIds\"\xe8\x02\n\x10ModuleManifestV1\x12\x16\n\x06module\x18\x01 \x01(\tR\x06module\x12\x1c\n\tprincipal\x18\x02 \x01(\tR\tprincipal\x12\x12\n\x04kind\x18\x03 \x01(\tR\x04kind\x12\x1e\n\ngeneration\x18\x04 \x01(\x04R\ngeneration\x12:\n\x08\x62indings\x18\x05 \x03(\x0b\x32\x1e.host.composition.v1.BindingV1R\x08\x62indings\x12J\n\x0eresource_kinds\x18\x06 \x03(\x0b\x32#.host.composition.v1.ResourceKindV1R\rresourceKinds\x12/\n\x04page\x18\x07 \x01(\x0b\x32\x1b.host.composition.v1.PageV1R\x04page\x12\x31\n\x05roles\x18\x08 \x03(\x0b\x32\x1b.host.composition.v1.RoleV1R\x05roles\"T\n\x06RoleV1\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x36\n\x06scopes\x18\x02 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x06scopes\"\x8a\x02\n\tBindingV1\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x1a\n\x08\x61udience\x18\x02 \x01(\tR\x08\x61udience\x12\x43\n\rinvoke_scopes\x18\x03 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x0cinvokeScopes\x12G\n\x0fheadless_scopes\x18\x04 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x0eheadlessScopes\x12\x43\n\rlookup_scopes\x18\x05 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x0clookupScopes\">\n\x0eResourceKindV1\x12\x12\n\x04kind\x18\x01 \x01(\tR\x04kind\x12\x18\n\x07\x61\x63tions\x18\x02 \x03(\tR\x07\x61\x63tions\"4\n\x06PageV1\x12\x14\n\x05title\x18\x01 \x01(\tR\x05title\x12\x14\n\x05order\x18\x02 \x01(\x05R\x05order2|\n\x12\x43ompositionService\x12\x66\n\rApplyManifest\x12).host.composition.v1.ApplyManifestRequest\x1a*.host.composition.v1.ApplyManifestResponseB3Z1gateway/pkg/gen/host/composition/v1;compositionv1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%host/composition/v1/composition.proto\x12\x13host.composition.v1\x1a$host/authority/v1/work_context.proto\"\xff\x01\n\x14\x41pplyManifestRequest\x12\x41\n\x08manifest\x18\x01 \x01(\x0b\x32%.host.composition.v1.ModuleManifestV1R\x08manifest\x12?\n\x07\x63\x61rrier\x18\x02 \x01(\x0b\x32%.host.composition.v1.SignedManifestV1R\x07\x63\x61rrier\x12\x1e\n\ncredential\x18\x03 \x01(\tR\ncredential\x12\x43\n\tauthority\x18\x04 \x01(\x0b\x32%.host.composition.v1.SignedManifestV1R\tauthority\"\xd9\x02\n\x13\x41uthorityManifestV1\x12\x16\n\x06module\x18\x01 \x01(\tR\x06module\x12\x1e\n\ngeneration\x18\x02 \x01(\x04R\ngeneration\x12\x30\n\x14\x63ore_manifest_digest\x18\x03 \x01(\tR\x12\x63oreManifestDigest\x12=\n\x07methods\x18\x04 \x03(\x0b\x32#.host.composition.v1.MethodPolicyV1R\x07methods\x12\x45\n\x0escope_ceilings\x18\x05 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\rscopeCeilings\x12\x31\n\x05roles\x18\x06 \x03(\x0b\x32\x1b.host.composition.v1.RoleV1R\x05roles\x12\x1f\n\x0bowner_aware\x18\x07 \x01(\x08R\nownerAware\"\x92\x01\n\x0eMethodPolicyV1\x12\x16\n\x06method\x18\x01 \x01(\tR\x06method\x12:\n\x08requires\x18\x02 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x08requires\x12,\n\x12\x61ny_checked_caller\x18\x03 \x01(\x08R\x10\x61nyCheckedCaller\"J\n\x10SignedManifestV1\x12\x1a\n\x08\x64ocument\x18\x01 \x01(\x0cR\x08\x64ocument\x12\x1a\n\x08\x65nvelope\x18\x02 \x01(\x0cR\x08\x65nvelope\"\x80\x01\n\x15\x41pplyManifestResponse\x12\x1c\n\tprincipal\x18\x01 \x01(\tR\tprincipal\x12\x1e\n\ngeneration\x18\x02 \x01(\x04R\ngeneration\x12)\n\x10installation_ids\x18\x03 \x03(\tR\x0finstallationIds\"\xe8\x02\n\x10ModuleManifestV1\x12\x16\n\x06module\x18\x01 \x01(\tR\x06module\x12\x1c\n\tprincipal\x18\x02 \x01(\tR\tprincipal\x12\x12\n\x04kind\x18\x03 \x01(\tR\x04kind\x12\x1e\n\ngeneration\x18\x04 \x01(\x04R\ngeneration\x12:\n\x08\x62indings\x18\x05 \x03(\x0b\x32\x1e.host.composition.v1.BindingV1R\x08\x62indings\x12J\n\x0eresource_kinds\x18\x06 \x03(\x0b\x32#.host.composition.v1.ResourceKindV1R\rresourceKinds\x12/\n\x04page\x18\x07 \x01(\x0b\x32\x1b.host.composition.v1.PageV1R\x04page\x12\x31\n\x05roles\x18\x08 \x03(\x0b\x32\x1b.host.composition.v1.RoleV1R\x05roles\"T\n\x06RoleV1\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x36\n\x06scopes\x18\x02 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x06scopes\"\x8a\x02\n\tBindingV1\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x1a\n\x08\x61udience\x18\x02 \x01(\tR\x08\x61udience\x12\x43\n\rinvoke_scopes\x18\x03 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x0cinvokeScopes\x12G\n\x0fheadless_scopes\x18\x04 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x0eheadlessScopes\x12\x43\n\rlookup_scopes\x18\x05 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x0clookupScopes\">\n\x0eResourceKindV1\x12\x12\n\x04kind\x18\x01 \x01(\tR\x04kind\x12\x18\n\x07\x61\x63tions\x18\x02 \x03(\tR\x07\x61\x63tions\"4\n\x06PageV1\x12\x14\n\x05title\x18\x01 \x01(\tR\x05title\x12\x14\n\x05order\x18\x02 \x01(\x05R\x05order2|\n\x12\x43ompositionService\x12\x66\n\rApplyManifest\x12).host.composition.v1.ApplyManifestRequest\x1a*.host.composition.v1.ApplyManifestResponseB3Z1gateway/pkg/gen/host/composition/v1;compositionv1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -36,23 +36,23 @@ _globals['_APPLYMANIFESTREQUEST']._serialized_start=101 _globals['_APPLYMANIFESTREQUEST']._serialized_end=356 _globals['_AUTHORITYMANIFESTV1']._serialized_start=359 - _globals['_AUTHORITYMANIFESTV1']._serialized_end=671 - _globals['_METHODPOLICYV1']._serialized_start=674 - _globals['_METHODPOLICYV1']._serialized_end=820 - _globals['_SIGNEDMANIFESTV1']._serialized_start=822 - _globals['_SIGNEDMANIFESTV1']._serialized_end=896 - _globals['_APPLYMANIFESTRESPONSE']._serialized_start=899 - _globals['_APPLYMANIFESTRESPONSE']._serialized_end=1027 - _globals['_MODULEMANIFESTV1']._serialized_start=1030 - _globals['_MODULEMANIFESTV1']._serialized_end=1390 - _globals['_ROLEV1']._serialized_start=1392 - _globals['_ROLEV1']._serialized_end=1476 - _globals['_BINDINGV1']._serialized_start=1479 - _globals['_BINDINGV1']._serialized_end=1745 - _globals['_RESOURCEKINDV1']._serialized_start=1747 - _globals['_RESOURCEKINDV1']._serialized_end=1809 - _globals['_PAGEV1']._serialized_start=1811 - _globals['_PAGEV1']._serialized_end=1863 - _globals['_COMPOSITIONSERVICE']._serialized_start=1865 - _globals['_COMPOSITIONSERVICE']._serialized_end=1989 + _globals['_AUTHORITYMANIFESTV1']._serialized_end=704 + _globals['_METHODPOLICYV1']._serialized_start=707 + _globals['_METHODPOLICYV1']._serialized_end=853 + _globals['_SIGNEDMANIFESTV1']._serialized_start=855 + _globals['_SIGNEDMANIFESTV1']._serialized_end=929 + _globals['_APPLYMANIFESTRESPONSE']._serialized_start=932 + _globals['_APPLYMANIFESTRESPONSE']._serialized_end=1060 + _globals['_MODULEMANIFESTV1']._serialized_start=1063 + _globals['_MODULEMANIFESTV1']._serialized_end=1423 + _globals['_ROLEV1']._serialized_start=1425 + _globals['_ROLEV1']._serialized_end=1509 + _globals['_BINDINGV1']._serialized_start=1512 + _globals['_BINDINGV1']._serialized_end=1778 + _globals['_RESOURCEKINDV1']._serialized_start=1780 + _globals['_RESOURCEKINDV1']._serialized_end=1842 + _globals['_PAGEV1']._serialized_start=1844 + _globals['_PAGEV1']._serialized_end=1896 + _globals['_COMPOSITIONSERVICE']._serialized_start=1898 + _globals['_COMPOSITIONSERVICE']._serialized_end=2022 # @@protoc_insertion_point(module_scope) 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 22efabff..b834e4a3 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\"\x7f\n\x12GrantToTeamRequest\x12\'\n\x0finstallation_id\x18\x01 \x01(\tR\x0einstallationId\x12\x17\n\x07team_id\x18\x02 \x01(\tR\x06teamId\x12\'\n\x0forganization_id\x18\x03 \x01(\tR\x0eorganizationId\"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\"\x96\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\x19\n\x08team_ids\x18\x07 \x03(\tR\x07teamIds\x12,\n\x12\x61gent_principal_id\x18\x08 \x01(\tR\x10\x61gentPrincipalId\x12\x1c\n\tnamespace\x18\t \x01(\tR\tnamespace\"[\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\x03\"\\\n\x12InstallationNotice\x12\x46\n\x0cinstallation\x18\x01 \x01(\x0b\x32\".host.installation.v1.InstallationR\x0cinstallation\"\x11\n\x0fInstallationAck2\x96\x02\n\x13InstallationService\x12S\n\x07Install\x12$.host.installation.v1.InstallRequest\x1a\".host.installation.v1.Installation\x12[\n\x0bGrantToTeam\x12(.host.installation.v1.GrantToTeamRequest\x1a\".host.installation.v1.Installation\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\"\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') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -34,22 +34,30 @@ _globals['DESCRIPTOR']._serialized_options = b'Z3gateway/pkg/gen/host/installation/v1;installationv1' _globals['_INSTALLREQUEST']._serialized_start=66 _globals['_INSTALLREQUEST']._serialized_end=207 - _globals['_GRANTTOTEAMREQUEST']._serialized_start=209 - _globals['_GRANTTOTEAMREQUEST']._serialized_end=336 - _globals['_LISTREQUEST']._serialized_start=338 - _globals['_LISTREQUEST']._serialized_end=392 - _globals['_LISTRESPONSE']._serialized_start=394 - _globals['_LISTRESPONSE']._serialized_end=482 - _globals['_INSTALLATION']._serialized_start=485 - _globals['_INSTALLATION']._serialized_end=891 - _globals['_INSTALLATION_STATUS']._serialized_start=800 - _globals['_INSTALLATION_STATUS']._serialized_end=891 - _globals['_INSTALLATIONNOTICE']._serialized_start=893 - _globals['_INSTALLATIONNOTICE']._serialized_end=985 - _globals['_INSTALLATIONACK']._serialized_start=987 - _globals['_INSTALLATIONACK']._serialized_end=1004 - _globals['_INSTALLATIONSERVICE']._serialized_start=1007 - _globals['_INSTALLATIONSERVICE']._serialized_end=1285 - _globals['_INSTALLABLESERVICE']._serialized_start=1287 - _globals['_INSTALLABLESERVICE']._serialized_end=1399 + _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 # @@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 a00c92f6..0c98693f 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,11 +40,21 @@ 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.GrantToTeam = channel.unary_unary( - '/host.installation.v1.InstallationService/GrantToTeam', - request_serializer=host_dot_installation_dot_v1_dot_installation__pb2.GrantToTeamRequest.SerializeToString, + self.GrantRole = channel.unary_unary( + '/host.installation.v1.InstallationService/GrantRole', + request_serializer=host_dot_installation_dot_v1_dot_installation__pb2.GrantRoleRequest.SerializeToString, response_deserializer=host_dot_installation_dot_v1_dot_installation__pb2.Installation.FromString, _registered_method=True) + self.RevokeRole = channel.unary_unary( + '/host.installation.v1.InstallationService/RevokeRole', + request_serializer=host_dot_installation_dot_v1_dot_installation__pb2.RevokeRoleRequest.SerializeToString, + response_deserializer=host_dot_installation_dot_v1_dot_installation__pb2.Installation.FromString, + _registered_method=True) + self.ListRoleGrants = channel.unary_unary( + '/host.installation.v1.InstallationService/ListRoleGrants', + request_serializer=host_dot_installation_dot_v1_dot_installation__pb2.ListRoleGrantsRequest.SerializeToString, + response_deserializer=host_dot_installation_dot_v1_dot_installation__pb2.ListRoleGrantsResponse.FromString, + _registered_method=True) self.List = channel.unary_unary( '/host.installation.v1.InstallationService/List', request_serializer=host_dot_installation_dot_v1_dot_installation__pb2.ListRequest.SerializeToString, @@ -64,8 +74,27 @@ def Install(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') - def GrantToTeam(self, request, context): - """GrantToTeam exposes an installation to a team under the installation's role. + 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 + valid. An administrator of the installation's organization, or a holder + of the installer role on that installation, grants. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def RevokeRole(self, request, context): + """RevokeRole ends one grant; every capability its subject holds from it is + refused at once. Idempotent; the same callers as GrantRole. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListRoleGrants(self, request, context): + """ListRoleGrants answers who holds which role on the installation; the same + callers as GrantRole. """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') @@ -88,11 +117,21 @@ 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, ), - 'GrantToTeam': grpc.unary_unary_rpc_method_handler( - servicer.GrantToTeam, - request_deserializer=host_dot_installation_dot_v1_dot_installation__pb2.GrantToTeamRequest.FromString, + 'GrantRole': grpc.unary_unary_rpc_method_handler( + servicer.GrantRole, + request_deserializer=host_dot_installation_dot_v1_dot_installation__pb2.GrantRoleRequest.FromString, + response_serializer=host_dot_installation_dot_v1_dot_installation__pb2.Installation.SerializeToString, + ), + 'RevokeRole': grpc.unary_unary_rpc_method_handler( + servicer.RevokeRole, + request_deserializer=host_dot_installation_dot_v1_dot_installation__pb2.RevokeRoleRequest.FromString, response_serializer=host_dot_installation_dot_v1_dot_installation__pb2.Installation.SerializeToString, ), + 'ListRoleGrants': grpc.unary_unary_rpc_method_handler( + servicer.ListRoleGrants, + request_deserializer=host_dot_installation_dot_v1_dot_installation__pb2.ListRoleGrantsRequest.FromString, + response_serializer=host_dot_installation_dot_v1_dot_installation__pb2.ListRoleGrantsResponse.SerializeToString, + ), 'List': grpc.unary_unary_rpc_method_handler( servicer.List, request_deserializer=host_dot_installation_dot_v1_dot_installation__pb2.ListRequest.FromString, @@ -138,7 +177,7 @@ def Install(request, _registered_method=True) @staticmethod - def GrantToTeam(request, + def GrantRole(request, target, options=(), channel_credentials=None, @@ -151,8 +190,8 @@ def GrantToTeam(request, return grpc.experimental.unary_unary( request, target, - '/host.installation.v1.InstallationService/GrantToTeam', - host_dot_installation_dot_v1_dot_installation__pb2.GrantToTeamRequest.SerializeToString, + '/host.installation.v1.InstallationService/GrantRole', + host_dot_installation_dot_v1_dot_installation__pb2.GrantRoleRequest.SerializeToString, host_dot_installation_dot_v1_dot_installation__pb2.Installation.FromString, options, channel_credentials, @@ -164,6 +203,60 @@ def GrantToTeam(request, metadata, _registered_method=True) + @staticmethod + def RevokeRole(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/RevokeRole', + host_dot_installation_dot_v1_dot_installation__pb2.RevokeRoleRequest.SerializeToString, + host_dot_installation_dot_v1_dot_installation__pb2.Installation.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def ListRoleGrants(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/ListRoleGrants', + host_dot_installation_dot_v1_dot_installation__pb2.ListRoleGrantsRequest.SerializeToString, + host_dot_installation_dot_v1_dot_installation__pb2.ListRoleGrantsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + @staticmethod def List(request, target, diff --git a/libraries/authority-client/python/tests/test_hop.py b/libraries/authority-client/python/tests/test_hop.py index 0184c04d..998efd4d 100644 --- a/libraries/authority-client/python/tests/test_hop.py +++ b/libraries/authority-client/python/tests/test_hop.py @@ -87,14 +87,20 @@ def settle(v): def test_keys_are_reread_when_old_and_trusted_no_longer_than_stale(): + import threading + clock = [FIXTURE["now"]] source = Keys(published()) v = verifier(clock=lambda: clock[0], source=source) rotated = KeysResponse(issuer=published().issuer) rotated.keys.add(key_id="rotated", algorithm="Ed25519", public_key=bytes(32)) source.response = rotated + source.hold = threading.Event() clock[0] += 400 # older than KEY_MAX_AGE: a re-read starts; the held key serves this call - v.verify(FIXTURE["headers"]) + try: + v.verify(FIXTURE["headers"]) + finally: + source.hold.set() settle(v) assert source.reads == 2 clock[0] += 11 diff --git a/libraries/authority-client/python/tests/test_owner.py b/libraries/authority-client/python/tests/test_owner.py index 12f599e1..202552bf 100644 --- a/libraries/authority-client/python/tests/test_owner.py +++ b/libraries/authority-client/python/tests/test_owner.py @@ -7,6 +7,9 @@ the hop's identity leaves owner-selected scopes out, and a chain whose hop drops or changes the owner is refused. """ +import json +from pathlib import Path + import pytest from authority_client._gen.host.authority.v1.work_context_pb2 import WorkContextV1 @@ -51,3 +54,29 @@ def test_a_chain_that_drops_or_changes_the_owner_is_refused(last, ok): else: with pytest.raises(Unauthenticated): claims_of(context) + + +VECTORS = json.loads((Path(__file__).parents[2] / "testdata/resource_owner_vectors.json").read_text()) + + +def _owner(vector): + return vector["owner"] * vector.get("repeat", 1) if vector.get("repeat") else vector["owner"] + + +def _authenticates(owner): + context = body("session") + del context.authority_scopes[:] + context.authority_scopes.add(resource_kind="record", actions=["read"], resource_owner_id=owner) + claims_of(context) + + +@pytest.mark.parametrize("vector", VECTORS["accepted"], ids=lambda v: v["name"]) +def test_core_accepted_owners_authenticate(vector): + """Core's resource_owner_id rule, read from core's own vectors.""" + _authenticates(_owner(vector)) + + +@pytest.mark.parametrize("vector", VECTORS["refused"], ids=lambda v: v["name"]) +def test_core_refused_owners_do_not_authenticate(vector): + with pytest.raises(Unauthenticated): + _authenticates(_owner(vector)) diff --git a/libraries/authority-client/testdata/resource_owner_vectors.json b/libraries/authority-client/testdata/resource_owner_vectors.json new file mode 100644 index 00000000..27e8e1c5 --- /dev/null +++ b/libraries/authority-client/testdata/resource_owner_vectors.json @@ -0,0 +1,171 @@ +{ + "_comment": "resource_owner_id vectors: every protovalidate implementation must accept the first list and refuse the second. Read by workcontext/owner_test.go.", + "accepted": [ + { + "name": "512 ASCII bytes", + "owner": "u", + "repeat": 512 + }, + { + "name": "a principal UUID", + "owner": "5f0c8a2e-3b1d-4c7a-9e2f-6a1b0c3d4e5f" + }, + { + "name": "a path-like principal", + "owner": "solutions/org-1/wiki:principal" + }, + { + "name": "every allowed punctuation", + "owner": "!\"#$%&'()+,-./:;<=>?@[\\]^_`{|}~" + } + ], + "refused": [ + { + "name": "513 ASCII bytes", + "owner": "u", + "repeat": 513 + }, + { + "name": "512 bytes of four-byte runes", + "owner": "\ud83d\ude00", + "repeat": 128 + }, + { + "name": "2048 bytes in 512 runes", + "owner": "\ud83d\ude00", + "repeat": 512 + }, + { + "name": "a leading space", + "owner": " principal-u" + }, + { + "name": "an inner space", + "owner": "principal u" + }, + { + "name": "a trailing newline", + "owner": "principal-u\n" + }, + { + "name": "a tab", + "owner": "principal\tu" + }, + { + "name": "a vertical tab", + "owner": "principal\u000bu" + }, + { + "name": "a form feed", + "owner": "principal\fu" + }, + { + "name": "a NUL", + "owner": "principal\u0000u" + }, + { + "name": "a start-of-heading control", + "owner": "principal\u0001u" + }, + { + "name": "DEL", + "owner": "principal\u007fu" + }, + { + "name": "a C1 control", + "owner": "principal\u0085u" + }, + { + "name": "a non-breaking space", + "owner": "principal-u\u00a0" + }, + { + "name": "an ideographic space", + "owner": "principal\u3000u" + }, + { + "name": "a line separator", + "owner": "principal\u2028u" + }, + { + "name": "a zero-width space", + "owner": "principal-u\u200b" + }, + { + "name": "a zero-width joiner", + "owner": "principal\u200du" + }, + { + "name": "a byte-order mark", + "owner": "\ufeffprincipal-u" + }, + { + "name": "a soft hyphen", + "owner": "principal\u00adu" + }, + { + "name": "a right-to-left override", + "owner": "principal\u202eu" + }, + { + "name": "a private-use character", + "owner": "principal\ue000u" + }, + { + "name": "a combining grapheme joiner", + "owner": "principal-u\u034f" + }, + { + "name": "variation selector-16", + "owner": "principal-u\ufe0f" + }, + { + "name": "a Hangul filler", + "owner": "principal\u3164u" + }, + { + "name": "a Hangul choseong filler", + "owner": "principal\u115fu" + }, + { + "name": "a Braille blank", + "owner": "principal\u2800u" + }, + { + "name": "an NFC accent", + "owner": "principal-\u00e9" + }, + { + "name": "an NFD accent", + "owner": "principal-e\u0301" + }, + { + "name": "a Cyrillic lookalike", + "owner": "\u0440rincipal-u" + }, + { + "name": "a fullwidth lookalike", + "owner": "\uff50rincipal-u" + }, + { + "name": "an unassigned code point", + "owner": "a\u0378b" + }, + { + "name": "the noncharacter U+FFFF", + "owner": "a\uffffb" + }, + { + "name": "the noncharacter U+FDD0", + "owner": "a\ufdd0b" + }, + { + "name": "the wildcard", + "owner": "*" + }, + { + "name": "a wildcard inside", + "owner": "principal-*" + } + ] +} diff --git a/libraries/authority-client/typescript/src/gen/host/authority/v1/authority_pb.ts b/libraries/authority-client/typescript/src/gen/host/authority/v1/authority_pb.ts index ec333289..42761d18 100644 --- a/libraries/authority-client/typescript/src/gen/host/authority/v1/authority_pb.ts +++ b/libraries/authority-client/typescript/src/gen/host/authority/v1/authority_pb.ts @@ -19,7 +19,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file host/authority/v1/authority.proto. */ export const file_host_authority_v1_authority: GenFile = /*@__PURE__*/ - fileDesc("CiFob3N0L2F1dGhvcml0eS92MS9hdXRob3JpdHkucHJvdG8SEWhvc3QuYXV0aG9yaXR5LnYxIuYDCgxJc3N1ZVJlcXVlc3QSMgoGcGVyc29uGAEgASgLMiAuaG9zdC5hdXRob3JpdHkudjEuUGVyc29uU2Vzc2lvbkgAEjkKCHdvcmtsb2FkGAIgASgLMiUuaG9zdC5hdXRob3JpdHkudjEuV29ya2xvYWRDcmVkZW50aWFsSAASOgoMaW5zdGFsbGF0aW9uGAMgASgLMiIuaG9zdC5hdXRob3JpdHkudjEuSW5zdGFsbGF0aW9uUmVmSAASFgoMcGFyZW50X3Rva2VuGAQgASgJSAASNgoKZGVsZWdhdGlvbhgLIAEoCzIgLmhvc3QuYXV0aG9yaXR5LnYxLkRlbGVnYXRpb25SZWZIABIQCghhdWRpZW5jZRgFIAEoCRIuCgZzY29wZXMYBiADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMRITCgt0dGxfc2Vjb25kcxgHIAEoAxIcChRvcGVyYXRpb25fYmluZGluZ19pZBgIIAEoCRIXCg9vcmdhbml6YXRpb25faWQYCSABKAkSNAoFYWN0b3IYCiABKAsyJS5ob3N0LmF1dGhvcml0eS52MS5Xb3JrbG9hZENyZWRlbnRpYWwSEQoJb3BlcmF0aW9uGAwgASgJQgQKAmFzIiYKDURlbGVnYXRpb25SZWYSFQoNZGVsZWdhdGlvbl9pZBgBIAEoCSJiCg9EZWxlZ2F0ZVJlcXVlc3QSDgoGbW9kdWxlGAEgASgJEi4KBnNjb3BlcxgCIAMoCzIeLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtTY29wZVYxEg8KB3B1cnBvc2UYAyABKAkirgEKCkRlbGVnYXRpb24SFQoNZGVsZWdhdGlvbl9pZBgBIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAiABKAkSDwoHZ3JhbnRvchgDIAEoCRIOCgZtb2R1bGUYBCABKAkSLgoGc2NvcGVzGAUgAygLMh4uaG9zdC5hdXRob3JpdHkudjEuV29ya1Njb3BlVjESDwoHcHVycG9zZRgGIAEoCRIOCgZhY3RpdmUYByABKAgiYwoUR2V0RGVsZWdhdGlvblJlcXVlc3QSFQoNZGVsZWdhdGlvbl9pZBgBIAEoCRI0CgVhY3RvchgCIAEoCzIlLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtsb2FkQ3JlZGVudGlhbCIwChdSZXZva2VEZWxlZ2F0aW9uUmVxdWVzdBIVCg1kZWxlZ2F0aW9uX2lkGAEgASgJIk0KGFJldm9rZURlbGVnYXRpb25SZXNwb25zZRIxCgpkZWxlZ2F0aW9uGAEgASgLMh0uaG9zdC5hdXRob3JpdHkudjEuRGVsZWdhdGlvbiIhCg1QZXJzb25TZXNzaW9uSgQIARACUgpzZXNzaW9uX2lkIigKEldvcmtsb2FkQ3JlZGVudGlhbBISCgpjcmVkZW50aWFsGAEgASgJIioKD0luc3RhbGxhdGlvblJlZhIXCg9pbnN0YWxsYXRpb25faWQYASABKAkiagoNSXNzdWVSZXNwb25zZRINCgV0b2tlbhgBIAEoCRIXCg9leHBpcmVzX2F0X3VuaXgYAiABKAMSMQoHY29udGV4dBgDIAEoCzIgLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtDb250ZXh0VjEiUQoMQ2hlY2tSZXF1ZXN0Eg0KBXRva2VuGAEgASgJEhAKCGF1ZGllbmNlGAIgASgJEg4KBm1ldGhvZBgDIAEoCRIQCghhZHZpc29yeRgEIAEoCCJzCg1DaGVja1Jlc3BvbnNlEjEKB2NvbnRleHQYASABKAsyIC5ob3N0LmF1dGhvcml0eS52MS5Xb3JrQ29udGV4dFYxEh4KFmF1dGhvcml6YXRpb25fcmV2aXNpb24YAiABKAQSDwoHcmVmdXNhbBgDIAEoCSINCgtLZXlzUmVxdWVzdCJKCgxLZXlzUmVzcG9uc2USDgoGaXNzdWVyGAEgASgJEioKBGtleXMYAiADKAsyHC5ob3N0LmF1dGhvcml0eS52MS5QdWJsaWNLZXkiQgoJUHVibGljS2V5Eg4KBmtleV9pZBgBIAEoCRIRCglhbGdvcml0aG0YAiABKAkSEgoKcHVibGljX2tleRgDIAEoDCJeChRMaXN0UmVzb3VyY2VzUmVxdWVzdBINCgV0b2tlbhgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIVCg1yZXNvdXJjZV9raW5kGAMgASgJEg4KBmFjdGlvbhgEIAEoCSJJChVMaXN0UmVzb3VyY2VzUmVzcG9uc2USMAoGZ3JhbnRzGAEgAygLMiAuaG9zdC5hdXRob3JpdHkudjEuUmVzb3VyY2VHcmFudCJaCg1SZXNvdXJjZUdyYW50Eg0KA2FsbBgBIAEoCEgAEgwKAmlkGAIgASgJSAASEAoGcHJlZml4GAMgASgJSAASEQoHcGF0dGVybhgEIAEoCUgAQgcKBW1hdGNoMqwFChBBdXRob3JpdHlTZXJ2aWNlEmoKBUlzc3VlEh8uaG9zdC5hdXRob3JpdHkudjEuSXNzdWVSZXF1ZXN0GiAuaG9zdC5hdXRob3JpdHkudjEuSXNzdWVSZXNwb25zZSIegtPkkwIYOgEqIhMvdjEvYXV0aG9yaXR5Omlzc3VlEmoKBUNoZWNrEh8uaG9zdC5hdXRob3JpdHkudjEuQ2hlY2tSZXF1ZXN0GiAuaG9zdC5hdXRob3JpdHkudjEuQ2hlY2tSZXNwb25zZSIegtPkkwIYOgEqIhMvdjEvYXV0aG9yaXR5OmNoZWNrEkcKBEtleXMSHi5ob3N0LmF1dGhvcml0eS52MS5LZXlzUmVxdWVzdBofLmhvc3QuYXV0aG9yaXR5LnYxLktleXNSZXNwb25zZRJiCg1MaXN0UmVzb3VyY2VzEicuaG9zdC5hdXRob3JpdHkudjEuTGlzdFJlc291cmNlc1JlcXVlc3QaKC5ob3N0LmF1dGhvcml0eS52MS5MaXN0UmVzb3VyY2VzUmVzcG9uc2USTQoIRGVsZWdhdGUSIi5ob3N0LmF1dGhvcml0eS52MS5EZWxlZ2F0ZVJlcXVlc3QaHS5ob3N0LmF1dGhvcml0eS52MS5EZWxlZ2F0aW9uEmsKEFJldm9rZURlbGVnYXRpb24SKi5ob3N0LmF1dGhvcml0eS52MS5SZXZva2VEZWxlZ2F0aW9uUmVxdWVzdBorLmhvc3QuYXV0aG9yaXR5LnYxLlJldm9rZURlbGVnYXRpb25SZXNwb25zZRJXCg1HZXREZWxlZ2F0aW9uEicuaG9zdC5hdXRob3JpdHkudjEuR2V0RGVsZWdhdGlvblJlcXVlc3QaHS5ob3N0LmF1dGhvcml0eS52MS5EZWxlZ2F0aW9uQi9aLWdhdGV3YXkvcGtnL2dlbi9ob3N0L2F1dGhvcml0eS92MTthdXRob3JpdHl2MWIGcHJvdG8z", [file_google_api_annotations, file_host_authority_v1_work_context]); + fileDesc("CiFob3N0L2F1dGhvcml0eS92MS9hdXRob3JpdHkucHJvdG8SEWhvc3QuYXV0aG9yaXR5LnYxIuYDCgxJc3N1ZVJlcXVlc3QSMgoGcGVyc29uGAEgASgLMiAuaG9zdC5hdXRob3JpdHkudjEuUGVyc29uU2Vzc2lvbkgAEjkKCHdvcmtsb2FkGAIgASgLMiUuaG9zdC5hdXRob3JpdHkudjEuV29ya2xvYWRDcmVkZW50aWFsSAASOgoMaW5zdGFsbGF0aW9uGAMgASgLMiIuaG9zdC5hdXRob3JpdHkudjEuSW5zdGFsbGF0aW9uUmVmSAASFgoMcGFyZW50X3Rva2VuGAQgASgJSAASNgoKZGVsZWdhdGlvbhgLIAEoCzIgLmhvc3QuYXV0aG9yaXR5LnYxLkRlbGVnYXRpb25SZWZIABIQCghhdWRpZW5jZRgFIAEoCRIuCgZzY29wZXMYBiADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMRITCgt0dGxfc2Vjb25kcxgHIAEoAxIcChRvcGVyYXRpb25fYmluZGluZ19pZBgIIAEoCRIXCg9vcmdhbml6YXRpb25faWQYCSABKAkSNAoFYWN0b3IYCiABKAsyJS5ob3N0LmF1dGhvcml0eS52MS5Xb3JrbG9hZENyZWRlbnRpYWwSEQoJb3BlcmF0aW9uGAwgASgJQgQKAmFzIiYKDURlbGVnYXRpb25SZWYSFQoNZGVsZWdhdGlvbl9pZBgBIAEoCSJiCg9EZWxlZ2F0ZVJlcXVlc3QSDgoGbW9kdWxlGAEgASgJEi4KBnNjb3BlcxgCIAMoCzIeLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtTY29wZVYxEg8KB3B1cnBvc2UYAyABKAkirgEKCkRlbGVnYXRpb24SFQoNZGVsZWdhdGlvbl9pZBgBIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAiABKAkSDwoHZ3JhbnRvchgDIAEoCRIOCgZtb2R1bGUYBCABKAkSLgoGc2NvcGVzGAUgAygLMh4uaG9zdC5hdXRob3JpdHkudjEuV29ya1Njb3BlVjESDwoHcHVycG9zZRgGIAEoCRIOCgZhY3RpdmUYByABKAgiYwoUR2V0RGVsZWdhdGlvblJlcXVlc3QSFQoNZGVsZWdhdGlvbl9pZBgBIAEoCRI0CgVhY3RvchgCIAEoCzIlLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtsb2FkQ3JlZGVudGlhbCIwChdSZXZva2VEZWxlZ2F0aW9uUmVxdWVzdBIVCg1kZWxlZ2F0aW9uX2lkGAEgASgJIk0KGFJldm9rZURlbGVnYXRpb25SZXNwb25zZRIxCgpkZWxlZ2F0aW9uGAEgASgLMh0uaG9zdC5hdXRob3JpdHkudjEuRGVsZWdhdGlvbiIhCg1QZXJzb25TZXNzaW9uSgQIARACUgpzZXNzaW9uX2lkIigKEldvcmtsb2FkQ3JlZGVudGlhbBISCgpjcmVkZW50aWFsGAEgASgJIioKD0luc3RhbGxhdGlvblJlZhIXCg9pbnN0YWxsYXRpb25faWQYASABKAkiagoNSXNzdWVSZXNwb25zZRINCgV0b2tlbhgBIAEoCRIXCg9leHBpcmVzX2F0X3VuaXgYAiABKAMSMQoHY29udGV4dBgDIAEoCzIgLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtDb250ZXh0VjEiUQoMQ2hlY2tSZXF1ZXN0Eg0KBXRva2VuGAEgASgJEhAKCGF1ZGllbmNlGAIgASgJEg4KBm1ldGhvZBgDIAEoCRIQCghhZHZpc29yeRgEIAEoCCJzCg1DaGVja1Jlc3BvbnNlEjEKB2NvbnRleHQYASABKAsyIC5ob3N0LmF1dGhvcml0eS52MS5Xb3JrQ29udGV4dFYxEh4KFmF1dGhvcml6YXRpb25fcmV2aXNpb24YAiABKAQSDwoHcmVmdXNhbBgDIAEoCSINCgtLZXlzUmVxdWVzdCJKCgxLZXlzUmVzcG9uc2USDgoGaXNzdWVyGAEgASgJEioKBGtleXMYAiADKAsyHC5ob3N0LmF1dGhvcml0eS52MS5QdWJsaWNLZXkiQgoJUHVibGljS2V5Eg4KBmtleV9pZBgBIAEoCRIRCglhbGdvcml0aG0YAiABKAkSEgoKcHVibGljX2tleRgDIAEoDCJeChRMaXN0UmVzb3VyY2VzUmVxdWVzdBINCgV0b2tlbhgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIVCg1yZXNvdXJjZV9raW5kGAMgASgJEg4KBmFjdGlvbhgEIAEoCSJJChVMaXN0UmVzb3VyY2VzUmVzcG9uc2USMAoGZ3JhbnRzGAEgAygLMiAuaG9zdC5hdXRob3JpdHkudjEuUmVzb3VyY2VHcmFudCK7AQoYQXV0aG9yaXplVHJhbnNmZXJSZXF1ZXN0Eg0KBXRva2VuGAEgASgJEjQKBWFjdG9yGAIgASgLMiUuaG9zdC5hdXRob3JpdHkudjEuV29ya2xvYWRDcmVkZW50aWFsEhUKDXJlc291cmNlX2tpbmQYAyABKAkSEwoLcmVzb3VyY2VfaWQYBCABKAkSFQoNZnJvbV9vd25lcl9pZBgFIAEoCRIXCg90b19wcmluY2lwYWxfaWQYBiABKAkiSAoZQXV0aG9yaXplVHJhbnNmZXJSZXNwb25zZRIUCgxwcmluY2lwYWxfaWQYASABKAkSFQoNYXV0aG9yaXplZF9ieRgCIAEoCSJ1Cg1SZXNvdXJjZUdyYW50Eg0KA2FsbBgBIAEoCEgAEgwKAmlkGAIgASgJSAASEAoGcHJlZml4GAMgASgJSAASEQoHcGF0dGVybhgEIAEoCUgAEhkKEXJlc291cmNlX293bmVyX2lkGAUgASgJQgcKBW1hdGNoMpwGChBBdXRob3JpdHlTZXJ2aWNlEmoKBUlzc3VlEh8uaG9zdC5hdXRob3JpdHkudjEuSXNzdWVSZXF1ZXN0GiAuaG9zdC5hdXRob3JpdHkudjEuSXNzdWVSZXNwb25zZSIegtPkkwIYOgEqIhMvdjEvYXV0aG9yaXR5Omlzc3VlEmoKBUNoZWNrEh8uaG9zdC5hdXRob3JpdHkudjEuQ2hlY2tSZXF1ZXN0GiAuaG9zdC5hdXRob3JpdHkudjEuQ2hlY2tSZXNwb25zZSIegtPkkwIYOgEqIhMvdjEvYXV0aG9yaXR5OmNoZWNrEkcKBEtleXMSHi5ob3N0LmF1dGhvcml0eS52MS5LZXlzUmVxdWVzdBofLmhvc3QuYXV0aG9yaXR5LnYxLktleXNSZXNwb25zZRJiCg1MaXN0UmVzb3VyY2VzEicuaG9zdC5hdXRob3JpdHkudjEuTGlzdFJlc291cmNlc1JlcXVlc3QaKC5ob3N0LmF1dGhvcml0eS52MS5MaXN0UmVzb3VyY2VzUmVzcG9uc2USbgoRQXV0aG9yaXplVHJhbnNmZXISKy5ob3N0LmF1dGhvcml0eS52MS5BdXRob3JpemVUcmFuc2ZlclJlcXVlc3QaLC5ob3N0LmF1dGhvcml0eS52MS5BdXRob3JpemVUcmFuc2ZlclJlc3BvbnNlEk0KCERlbGVnYXRlEiIuaG9zdC5hdXRob3JpdHkudjEuRGVsZWdhdGVSZXF1ZXN0Gh0uaG9zdC5hdXRob3JpdHkudjEuRGVsZWdhdGlvbhJrChBSZXZva2VEZWxlZ2F0aW9uEiouaG9zdC5hdXRob3JpdHkudjEuUmV2b2tlRGVsZWdhdGlvblJlcXVlc3QaKy5ob3N0LmF1dGhvcml0eS52MS5SZXZva2VEZWxlZ2F0aW9uUmVzcG9uc2USVwoNR2V0RGVsZWdhdGlvbhInLmhvc3QuYXV0aG9yaXR5LnYxLkdldERlbGVnYXRpb25SZXF1ZXN0Gh0uaG9zdC5hdXRob3JpdHkudjEuRGVsZWdhdGlvbkIvWi1nYXRld2F5L3BrZy9nZW4vaG9zdC9hdXRob3JpdHkvdjE7YXV0aG9yaXR5djFiBnByb3RvMw", [file_google_api_annotations, file_host_authority_v1_work_context]); /** * IssueRequest names on whose behalf the capability is issued. @@ -648,7 +648,93 @@ export const ListResourcesResponseSchema: GenMessage = /* messageDesc(file_host_authority_v1_authority, 17); /** - * ResourceGrant is one way of naming covered resources. Exactly one is set. + * AuthorizeTransferRequest names one existing resource and where it moves. + * + * @generated from message host.authority.v1.AuthorizeTransferRequest + */ +export type AuthorizeTransferRequest = Message<"host.authority.v1.AuthorizeTransferRequest"> & { + /** + * The capability of the person asking, addressed to the caller. + * + * @generated from field: string token = 1; + */ + token: string; + + /** + * The module serving resource_kind, proving which module it is. + * + * @generated from field: host.authority.v1.WorkloadCredential actor = 2; + */ + actor?: WorkloadCredential; + + /** + * The resource kind, as the caller serves it. + * + * @generated from field: string resource_kind = 3; + */ + resourceKind: string; + + /** + * The resource id, as the caller knows it. + * + * @generated from field: string resource_id = 4; + */ + resourceId: string; + + /** + * The resource's current owner, as the caller records it: the organization + * or a person of it. + * + * @generated from field: string from_owner_id = 5; + */ + fromOwnerId: string; + + /** + * The solution principal the resource joins. + * + * @generated from field: string to_principal_id = 6; + */ + toPrincipalId: string; +}; + +/** + * Describes the message host.authority.v1.AuthorizeTransferRequest. + * Use `create(AuthorizeTransferRequestSchema)` to create a new message. + */ +export const AuthorizeTransferRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 18); + +/** + * AuthorizeTransferResponse is the decision the store records. + * + * @generated from message host.authority.v1.AuthorizeTransferResponse + */ +export type AuthorizeTransferResponse = Message<"host.authority.v1.AuthorizeTransferResponse"> & { + /** + * The principal that now owns the resource. + * + * @generated from field: string principal_id = 1; + */ + principalId: string; + + /** + * The person who authorized the transfer. + * + * @generated from field: string authorized_by = 2; + */ + authorizedBy: string; +}; + +/** + * Describes the message host.authority.v1.AuthorizeTransferResponse. + * Use `create(AuthorizeTransferResponseSchema)` to create a new message. + */ +export const AuthorizeTransferResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 19); + +/** + * ResourceGrant is one way of naming covered resources. Exactly one match is + * set; resource_owner_id, when set, qualifies it. * * @generated from message host.authority.v1.ResourceGrant */ @@ -690,6 +776,14 @@ export type ResourceGrant = Message<"host.authority.v1.ResourceGrant"> & { value: string; case: "pattern"; } | { case: undefined; value?: undefined }; + + /** + * The owner the covered resources must have: a resource is covered when + * the match covers its id and it is owned by this principal. + * + * @generated from field: string resource_owner_id = 5; + */ + resourceOwnerId: string; }; /** @@ -697,7 +791,7 @@ export type ResourceGrant = Message<"host.authority.v1.ResourceGrant"> & { * Use `create(ResourceGrantSchema)` to create a new message. */ export const ResourceGrantSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_host_authority_v1_authority, 18); + messageDesc(file_host_authority_v1_authority, 20); /** * AuthorityService is implemented by the host (accounts) and by the local authority. @@ -753,6 +847,27 @@ export const AuthorityService: GenService<{ input: typeof ListResourcesRequestSchema; output: typeof ListResourcesResponseSchema; }, + /** + * AuthorizeTransfer decides whether an existing resource may join an + * installed solution's principal. The caller is the kind's one serving + * module, which attests the resource exists and who owns it now + * (from_owner_id), presenting the capability of the person asking: issued + * through the target principal's installation and addressed to the caller. + * It is allowed when the current owner is the organization or a person of + * it (never another solution principal, another organization or an unknown + * owner), the person holds admin on that resource from their own + * organization grants, and the person's current roles on the principal hold + * write on the kind, all read live. An installation acting for itself never + * transfers. The store then records the principal as owner; the original + * creator stays the author. + * + * @generated from rpc host.authority.v1.AuthorityService.AuthorizeTransfer + */ + authorizeTransfer: { + methodKind: "unary"; + input: typeof AuthorizeTransferRequestSchema; + output: typeof AuthorizeTransferResponseSchema; + }, /** * Delegate records a signed-in person's grant to a module (the caller's * bearer is the person); Issue with a delegation acts on it. diff --git a/libraries/authority-client/typescript/src/gen/host/authority/v1/work_context_pb.ts b/libraries/authority-client/typescript/src/gen/host/authority/v1/work_context_pb.ts index 0b431d4d..655dd964 100644 --- a/libraries/authority-client/typescript/src/gen/host/authority/v1/work_context_pb.ts +++ b/libraries/authority-client/typescript/src/gen/host/authority/v1/work_context_pb.ts @@ -17,7 +17,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file host/authority/v1/work_context.proto. */ export const file_host_authority_v1_work_context: GenFile = /*@__PURE__*/ - fileDesc("CiRob3N0L2F1dGhvcml0eS92MS93b3JrX2NvbnRleHQucHJvdG8SEWhvc3QuYXV0aG9yaXR5LnYxItAJCg1Xb3JrQ29udGV4dFYxEisKA3R5cBgBIAEoCUIeukgbchkKF2NvZGVmbHkud29yay1jb250ZXh0L3YxEiEKCWFsZ29yaXRobRgCIAEoCUIOukgLcgkKB0VkMjU1MTkSGgoGa2V5X2lkGAMgASgJQgq6SAdyBRABGIABEhoKBmlzc3VlchgEIAEoCUIKukgHcgUQARiABBIcCghhdWRpZW5jZRgFIAEoCUIKukgHcgUQARiABBIXCg9ub3RfYmVmb3JlX3VuaXgYBiABKAMSFgoOaXNzdWVkX2F0X3VuaXgYByABKAMSFwoPZXhwaXJlc19hdF91bml4GAggASgDEhkKBW5vbmNlGAkgASgJQgq6SAdyBRABGIAEEh4KFmF1dGhvcml6YXRpb25fcmV2aXNpb24YCiABKAQSNAoNcmVwbGF5X3BvbGljeRgLIAEoCUIdukgachhSCmlkZW1wb3RlbnRSCnNpbmdsZS11c2USHQoJdGVuYW50X2lkGAwgASgJQgq6SAdyBRABGIAEEiYKEm93bmVyX3ByaW5jaXBhbF9pZBgNIAEoCUIKukgHcgUQARiABBIbCgd0YXNrX2lkGA4gASgJQgq6SAdyBRABGIAEEh4KCnNlc3Npb25faWQYDyABKAlCCrpIB3IFEAEYgAQSKgoRcGFyZW50X3Nlc3Npb25faWQYECABKAlCCrpIB3IFEAEYgARIAIgBARI4ChBhdXRob3JpdHlfc2NvcGVzGBEgAygLMh4uaG9zdC5hdXRob3JpdHkudjEuV29ya1Njb3BlVjESMwoLYWN0b3JfY2hhaW4YEiADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrQWN0b3JWMRIcChRhdHRyaWJ1dGlvbl90ZWFtX2lkcxgTIAMoCRIlCgx3b3Jrc3BhY2VfaWQYFCABKAlCCrpIB3IFEAEYgARIAYgBARIjCgpwcm9qZWN0X2lkGBUgASgJQgq6SAdyBRABGIAESAKIAQESLQoUb3duZXJfcHJpbmNpcGFsX2tpbmQYFiABKAlCCrpIB3IFEAEYgAFIA4gBARInCg5vd25lcl9hZ2VudF9pZBgXIAEoCUIKukgHcgUQARiABEgEiAEBEigKD29yZ2FuaXphdGlvbl9pZBgZIAEoCUIKukgHcgUQARiABEgFiAEBEjkKCWdyYW50X2hvcBgYIAEoCzIhLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtHcmFudEhvcFYxSAaIAQESMwoEc2VhbBgaIAEoCzIdLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtTZWFsVjFCBrpIA8gBARJJChFvcGVyYXRpb25fYmluZGluZxgbIAEoCzIpLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtPcGVyYXRpb25CaW5kaW5nVjFIB4gBAUIUChJfcGFyZW50X3Nlc3Npb25faWRCDwoNX3dvcmtzcGFjZV9pZEINCgtfcHJvamVjdF9pZEIXChVfb3duZXJfcHJpbmNpcGFsX2tpbmRCEQoPX293bmVyX2FnZW50X2lkQhIKEF9vcmdhbml6YXRpb25faWRCDAoKX2dyYW50X2hvcEIUChJfb3BlcmF0aW9uX2JpbmRpbmciqAEKCldvcmtTZWFsVjESIAoPcHJpbmNpcGFsX2Vwb2NoGAEgASgEQge6SAQyAigBEiMKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIKukgHcgUQARiABBImChVpbnN0YWxsYXRpb25fcmV2aXNpb24YAyABKARCB7pIBDICKAFKBAgEEAVKBAgFEAZSEWJ1aWxkX2luY2FybmF0aW9uUgxpbWFnZV9kaWdlc3QicQoWV29ya09wZXJhdGlvbkJpbmRpbmdWMRIeCgpiaW5kaW5nX2lkGAEgASgJQgq6SAdyBRABGIAEEhkKCHJldmlzaW9uGAIgASgEQge6SAQyAigBEhwKC2luY2FybmF0aW9uGAMgASgEQge6SAQyAigBIosBCgtXb3JrU2NvcGVWMRIhCg1yZXNvdXJjZV9raW5kGAEgASgJQgq6SAdyBRABGIABEg8KB2FjdGlvbnMYAiADKAkSFAoMcmVzb3VyY2VfaWRzGAMgAygJEjIKEXJlc291cmNlX293bmVyX2lkGAQgASgJQhe6SBRyEhiABDINXlteXHNceDAwKl0qJCLuAgoLV29ya0FjdG9yVjESIAoMcHJpbmNpcGFsX2lkGAEgASgJQgq6SAdyBRABGIAEEiIKDnByaW5jaXBhbF9raW5kGAIgASgJQgq6SAdyBRABGIABEiEKDWRlbGVnYXRpb25faWQYAyABKAlCCrpIB3IFEAEYgAQSNgoOZ3JhbnRlZF9zY29wZXMYBCADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMRIhCghhZ2VudF9pZBgFIAEoCUIKukgHcgUQARiABEgAiAEBEigKD29yZ2FuaXphdGlvbl9pZBgGIAEoCUIKukgHcgUQARiABEgBiAEBEiMKD3ByaW5jaXBhbF9lcG9jaBgHIAEoBEIKukgHyAEBMgIoAUILCglfYWdlbnRfaWRCEgoQX29yZ2FuaXphdGlvbl9pZEoECAgQCUoECAkQClIMaW1hZ2VfZGlnZXN0UhFidWlsZF9pbmNhcm5hdGlvbiLwAQoOV29ya0dyYW50SG9wVjESHAoIZ3JhbnRfaWQYASABKAlCCrpIB3IFEAEYgAQSQAoJYXBwcm92ZXJzGAIgAygLMiEuaG9zdC5hdXRob3JpdHkudjEuV29ya0FwcHJvdmVyVjFCCrpIB5IBBAgBEEASPQoNZ3JhbnRlZF9zY29wZRgDIAEoCzIeLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtTY29wZVYxQga6SAPIAQESGwoHc3ViamVjdBgEIAEoCUIKukgHcgUQARiABBIiCg5yZXF1ZXN0X2RpZ2VzdBgFIAEoCUIKukgHcgUQARiABCJWCg5Xb3JrQXBwcm92ZXJWMRIgCgxwcmluY2lwYWxfaWQYASABKAlCCrpIB3IFEAEYgAQSIgoOcHJpbmNpcGFsX2tpbmQYAiABKAlCCrpIB3IFEAEYgAEi1AEKEkFwcHJvdmFsUmVxdWlyZWRWMRIeCgpyZXF1ZXN0X2lkGAEgASgJQgq6SAdyBRABGIAEEj8KD3JlcXVlc3RlZF9zY29wZRgCIAEoCzIeLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtTY29wZVYxQga6SAPIAQESGwoHc3ViamVjdBgDIAEoCUIKukgHcgUQARiABBIiCg5yZXF1ZXN0X2RpZ2VzdBgEIAEoCUIKukgHcgUQARiABBIcCghhdWRpZW5jZRgFIAEoCUIKukgHcgUQARiABEIvWi1nYXRld2F5L3BrZy9nZW4vaG9zdC9hdXRob3JpdHkvdjE7YXV0aG9yaXR5djFiBnByb3RvMw", [file_buf_validate_validate]); + fileDesc("CiRob3N0L2F1dGhvcml0eS92MS93b3JrX2NvbnRleHQucHJvdG8SEWhvc3QuYXV0aG9yaXR5LnYxItAJCg1Xb3JrQ29udGV4dFYxEisKA3R5cBgBIAEoCUIeukgbchkKF2NvZGVmbHkud29yay1jb250ZXh0L3YxEiEKCWFsZ29yaXRobRgCIAEoCUIOukgLcgkKB0VkMjU1MTkSGgoGa2V5X2lkGAMgASgJQgq6SAdyBRABGIABEhoKBmlzc3VlchgEIAEoCUIKukgHcgUQARiABBIcCghhdWRpZW5jZRgFIAEoCUIKukgHcgUQARiABBIXCg9ub3RfYmVmb3JlX3VuaXgYBiABKAMSFgoOaXNzdWVkX2F0X3VuaXgYByABKAMSFwoPZXhwaXJlc19hdF91bml4GAggASgDEhkKBW5vbmNlGAkgASgJQgq6SAdyBRABGIAEEh4KFmF1dGhvcml6YXRpb25fcmV2aXNpb24YCiABKAQSNAoNcmVwbGF5X3BvbGljeRgLIAEoCUIdukgachhSCmlkZW1wb3RlbnRSCnNpbmdsZS11c2USHQoJdGVuYW50X2lkGAwgASgJQgq6SAdyBRABGIAEEiYKEm93bmVyX3ByaW5jaXBhbF9pZBgNIAEoCUIKukgHcgUQARiABBIbCgd0YXNrX2lkGA4gASgJQgq6SAdyBRABGIAEEh4KCnNlc3Npb25faWQYDyABKAlCCrpIB3IFEAEYgAQSKgoRcGFyZW50X3Nlc3Npb25faWQYECABKAlCCrpIB3IFEAEYgARIAIgBARI4ChBhdXRob3JpdHlfc2NvcGVzGBEgAygLMh4uaG9zdC5hdXRob3JpdHkudjEuV29ya1Njb3BlVjESMwoLYWN0b3JfY2hhaW4YEiADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrQWN0b3JWMRIcChRhdHRyaWJ1dGlvbl90ZWFtX2lkcxgTIAMoCRIlCgx3b3Jrc3BhY2VfaWQYFCABKAlCCrpIB3IFEAEYgARIAYgBARIjCgpwcm9qZWN0X2lkGBUgASgJQgq6SAdyBRABGIAESAKIAQESLQoUb3duZXJfcHJpbmNpcGFsX2tpbmQYFiABKAlCCrpIB3IFEAEYgAFIA4gBARInCg5vd25lcl9hZ2VudF9pZBgXIAEoCUIKukgHcgUQARiABEgEiAEBEigKD29yZ2FuaXphdGlvbl9pZBgZIAEoCUIKukgHcgUQARiABEgFiAEBEjkKCWdyYW50X2hvcBgYIAEoCzIhLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtHcmFudEhvcFYxSAaIAQESMwoEc2VhbBgaIAEoCzIdLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtTZWFsVjFCBrpIA8gBARJJChFvcGVyYXRpb25fYmluZGluZxgbIAEoCzIpLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtPcGVyYXRpb25CaW5kaW5nVjFIB4gBAUIUChJfcGFyZW50X3Nlc3Npb25faWRCDwoNX3dvcmtzcGFjZV9pZEINCgtfcHJvamVjdF9pZEIXChVfb3duZXJfcHJpbmNpcGFsX2tpbmRCEQoPX293bmVyX2FnZW50X2lkQhIKEF9vcmdhbml6YXRpb25faWRCDAoKX2dyYW50X2hvcEIUChJfb3BlcmF0aW9uX2JpbmRpbmciqAEKCldvcmtTZWFsVjESIAoPcHJpbmNpcGFsX2Vwb2NoGAEgASgEQge6SAQyAigBEiMKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIKukgHcgUQARiABBImChVpbnN0YWxsYXRpb25fcmV2aXNpb24YAyABKARCB7pIBDICKAFKBAgEEAVKBAgFEAZSEWJ1aWxkX2luY2FybmF0aW9uUgxpbWFnZV9kaWdlc3QicQoWV29ya09wZXJhdGlvbkJpbmRpbmdWMRIeCgpiaW5kaW5nX2lkGAEgASgJQgq6SAdyBRABGIAEEhkKCHJldmlzaW9uGAIgASgEQge6SAQyAigBEhwKC2luY2FybmF0aW9uGAMgASgEQge6SAQyAigBIokBCgtXb3JrU2NvcGVWMRIhCg1yZXNvdXJjZV9raW5kGAEgASgJQgq6SAdyBRABGIABEg8KB2FjdGlvbnMYAiADKAkSFAoMcmVzb3VyY2VfaWRzGAMgAygJEjAKEXJlc291cmNlX293bmVyX2lkGAQgASgJQhW6SBJyECiABDILXlshLSkrLX5dKiQi7gIKC1dvcmtBY3RvclYxEiAKDHByaW5jaXBhbF9pZBgBIAEoCUIKukgHcgUQARiABBIiCg5wcmluY2lwYWxfa2luZBgCIAEoCUIKukgHcgUQARiAARIhCg1kZWxlZ2F0aW9uX2lkGAMgASgJQgq6SAdyBRABGIAEEjYKDmdyYW50ZWRfc2NvcGVzGAQgAygLMh4uaG9zdC5hdXRob3JpdHkudjEuV29ya1Njb3BlVjESIQoIYWdlbnRfaWQYBSABKAlCCrpIB3IFEAEYgARIAIgBARIoCg9vcmdhbml6YXRpb25faWQYBiABKAlCCrpIB3IFEAEYgARIAYgBARIjCg9wcmluY2lwYWxfZXBvY2gYByABKARCCrpIB8gBATICKAFCCwoJX2FnZW50X2lkQhIKEF9vcmdhbml6YXRpb25faWRKBAgIEAlKBAgJEApSDGltYWdlX2RpZ2VzdFIRYnVpbGRfaW5jYXJuYXRpb24i8AEKDldvcmtHcmFudEhvcFYxEhwKCGdyYW50X2lkGAEgASgJQgq6SAdyBRABGIAEEkAKCWFwcHJvdmVycxgCIAMoCzIhLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtBcHByb3ZlclYxQgq6SAeSAQQIARBAEj0KDWdyYW50ZWRfc2NvcGUYAyABKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMUIGukgDyAEBEhsKB3N1YmplY3QYBCABKAlCCrpIB3IFEAEYgAQSIgoOcmVxdWVzdF9kaWdlc3QYBSABKAlCCrpIB3IFEAEYgAQiVgoOV29ya0FwcHJvdmVyVjESIAoMcHJpbmNpcGFsX2lkGAEgASgJQgq6SAdyBRABGIAEEiIKDnByaW5jaXBhbF9raW5kGAIgASgJQgq6SAdyBRABGIABItQBChJBcHByb3ZhbFJlcXVpcmVkVjESHgoKcmVxdWVzdF9pZBgBIAEoCUIKukgHcgUQARiABBI/Cg9yZXF1ZXN0ZWRfc2NvcGUYAiABKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMUIGukgDyAEBEhsKB3N1YmplY3QYAyABKAlCCrpIB3IFEAEYgAQSIgoOcmVxdWVzdF9kaWdlc3QYBCABKAlCCrpIB3IFEAEYgAQSHAoIYXVkaWVuY2UYBSABKAlCCrpIB3IFEAEYgARCL1otZ2F0ZXdheS9wa2cvZ2VuL2hvc3QvYXV0aG9yaXR5L3YxO2F1dGhvcml0eXYxYgZwcm90bzM", [file_buf_validate_validate]); /** * WorkContextV1 is the product-neutral identity and authority capability for @@ -422,7 +422,11 @@ export type WorkScopeV1 = Message<"host.authority.v1.WorkScopeV1"> & { * (an installed solution's principal). Empty means the scope selects no * owner. With resource_ids, a resource is covered only when both match. A * reader that cannot compare a resource's owner must not treat an - * owner-selected scope as covering the whole kind. + * owner-selected scope as covering the whole kind. It is at most 512 bytes + * of printable ASCII other than space and "*" (0x21-0x29, 0x2B-0x7E): no + * whitespace, control, non-ASCII, invisible or lookalike character, so two + * owners that print alike are the same bytes and every regex engine answers + * the rule the same way. * * @generated from field: string resource_owner_id = 4; */ diff --git a/libraries/authority-client/typescript/src/gen/host/composition/v1/composition_pb.ts b/libraries/authority-client/typescript/src/gen/host/composition/v1/composition_pb.ts index eab14d07..0eb49499 100644 --- a/libraries/authority-client/typescript/src/gen/host/composition/v1/composition_pb.ts +++ b/libraries/authority-client/typescript/src/gen/host/composition/v1/composition_pb.ts @@ -17,7 +17,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file host/composition/v1/composition.proto. */ export const file_host_composition_v1_composition: GenFile = /*@__PURE__*/ - fileDesc("CiVob3N0L2NvbXBvc2l0aW9uL3YxL2NvbXBvc2l0aW9uLnByb3RvEhNob3N0LmNvbXBvc2l0aW9uLnYxItUBChRBcHBseU1hbmlmZXN0UmVxdWVzdBI3CghtYW5pZmVzdBgBIAEoCzIlLmhvc3QuY29tcG9zaXRpb24udjEuTW9kdWxlTWFuaWZlc3RWMRI2CgdjYXJyaWVyGAIgASgLMiUuaG9zdC5jb21wb3NpdGlvbi52MS5TaWduZWRNYW5pZmVzdFYxEhIKCmNyZWRlbnRpYWwYAyABKAkSOAoJYXV0aG9yaXR5GAQgASgLMiUuaG9zdC5jb21wb3NpdGlvbi52MS5TaWduZWRNYW5pZmVzdFYxIvEBChNBdXRob3JpdHlNYW5pZmVzdFYxEg4KBm1vZHVsZRgBIAEoCRISCgpnZW5lcmF0aW9uGAIgASgEEhwKFGNvcmVfbWFuaWZlc3RfZGlnZXN0GAMgASgJEjQKB21ldGhvZHMYBCADKAsyIy5ob3N0LmNvbXBvc2l0aW9uLnYxLk1ldGhvZFBvbGljeVYxEjYKDnNjb3BlX2NlaWxpbmdzGAUgAygLMh4uaG9zdC5hdXRob3JpdHkudjEuV29ya1Njb3BlVjESKgoFcm9sZXMYBiADKAsyGy5ob3N0LmNvbXBvc2l0aW9uLnYxLlJvbGVWMSJuCg5NZXRob2RQb2xpY3lWMRIOCgZtZXRob2QYASABKAkSMAoIcmVxdWlyZXMYAiADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMRIaChJhbnlfY2hlY2tlZF9jYWxsZXIYAyABKAgiNgoQU2lnbmVkTWFuaWZlc3RWMRIQCghkb2N1bWVudBgBIAEoDBIQCghlbnZlbG9wZRgCIAEoDCJYChVBcHBseU1hbmlmZXN0UmVzcG9uc2USEQoJcHJpbmNpcGFsGAEgASgJEhIKCmdlbmVyYXRpb24YAiABKAQSGAoQaW5zdGFsbGF0aW9uX2lkcxgDIAMoCSKdAgoQTW9kdWxlTWFuaWZlc3RWMRIOCgZtb2R1bGUYASABKAkSEQoJcHJpbmNpcGFsGAIgASgJEgwKBGtpbmQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoBBIwCghiaW5kaW5ncxgFIAMoCzIeLmhvc3QuY29tcG9zaXRpb24udjEuQmluZGluZ1YxEjsKDnJlc291cmNlX2tpbmRzGAYgAygLMiMuaG9zdC5jb21wb3NpdGlvbi52MS5SZXNvdXJjZUtpbmRWMRIpCgRwYWdlGAcgASgLMhsuaG9zdC5jb21wb3NpdGlvbi52MS5QYWdlVjESKgoFcm9sZXMYCCADKAsyGy5ob3N0LmNvbXBvc2l0aW9uLnYxLlJvbGVWMSJGCgZSb2xlVjESDAoEbmFtZRgBIAEoCRIuCgZzY29wZXMYAiADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMSLQAQoJQmluZGluZ1YxEgoKAmlkGAEgASgJEhAKCGF1ZGllbmNlGAIgASgJEjUKDWludm9rZV9zY29wZXMYAyADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMRI3Cg9oZWFkbGVzc19zY29wZXMYBCADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMRI1Cg1sb29rdXBfc2NvcGVzGAUgAygLMh4uaG9zdC5hdXRob3JpdHkudjEuV29ya1Njb3BlVjEiLwoOUmVzb3VyY2VLaW5kVjESDAoEa2luZBgBIAEoCRIPCgdhY3Rpb25zGAIgAygJIiYKBlBhZ2VWMRINCgV0aXRsZRgBIAEoCRINCgVvcmRlchgCIAEoBTJ8ChJDb21wb3NpdGlvblNlcnZpY2USZgoNQXBwbHlNYW5pZmVzdBIpLmhvc3QuY29tcG9zaXRpb24udjEuQXBwbHlNYW5pZmVzdFJlcXVlc3QaKi5ob3N0LmNvbXBvc2l0aW9uLnYxLkFwcGx5TWFuaWZlc3RSZXNwb25zZUIzWjFnYXRld2F5L3BrZy9nZW4vaG9zdC9jb21wb3NpdGlvbi92MTtjb21wb3NpdGlvbnYxYgZwcm90bzM", [file_host_authority_v1_work_context]); + fileDesc("CiVob3N0L2NvbXBvc2l0aW9uL3YxL2NvbXBvc2l0aW9uLnByb3RvEhNob3N0LmNvbXBvc2l0aW9uLnYxItUBChRBcHBseU1hbmlmZXN0UmVxdWVzdBI3CghtYW5pZmVzdBgBIAEoCzIlLmhvc3QuY29tcG9zaXRpb24udjEuTW9kdWxlTWFuaWZlc3RWMRI2CgdjYXJyaWVyGAIgASgLMiUuaG9zdC5jb21wb3NpdGlvbi52MS5TaWduZWRNYW5pZmVzdFYxEhIKCmNyZWRlbnRpYWwYAyABKAkSOAoJYXV0aG9yaXR5GAQgASgLMiUuaG9zdC5jb21wb3NpdGlvbi52MS5TaWduZWRNYW5pZmVzdFYxIoYCChNBdXRob3JpdHlNYW5pZmVzdFYxEg4KBm1vZHVsZRgBIAEoCRISCgpnZW5lcmF0aW9uGAIgASgEEhwKFGNvcmVfbWFuaWZlc3RfZGlnZXN0GAMgASgJEjQKB21ldGhvZHMYBCADKAsyIy5ob3N0LmNvbXBvc2l0aW9uLnYxLk1ldGhvZFBvbGljeVYxEjYKDnNjb3BlX2NlaWxpbmdzGAUgAygLMh4uaG9zdC5hdXRob3JpdHkudjEuV29ya1Njb3BlVjESKgoFcm9sZXMYBiADKAsyGy5ob3N0LmNvbXBvc2l0aW9uLnYxLlJvbGVWMRITCgtvd25lcl9hd2FyZRgHIAEoCCJuCg5NZXRob2RQb2xpY3lWMRIOCgZtZXRob2QYASABKAkSMAoIcmVxdWlyZXMYAiADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMRIaChJhbnlfY2hlY2tlZF9jYWxsZXIYAyABKAgiNgoQU2lnbmVkTWFuaWZlc3RWMRIQCghkb2N1bWVudBgBIAEoDBIQCghlbnZlbG9wZRgCIAEoDCJYChVBcHBseU1hbmlmZXN0UmVzcG9uc2USEQoJcHJpbmNpcGFsGAEgASgJEhIKCmdlbmVyYXRpb24YAiABKAQSGAoQaW5zdGFsbGF0aW9uX2lkcxgDIAMoCSKdAgoQTW9kdWxlTWFuaWZlc3RWMRIOCgZtb2R1bGUYASABKAkSEQoJcHJpbmNpcGFsGAIgASgJEgwKBGtpbmQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoBBIwCghiaW5kaW5ncxgFIAMoCzIeLmhvc3QuY29tcG9zaXRpb24udjEuQmluZGluZ1YxEjsKDnJlc291cmNlX2tpbmRzGAYgAygLMiMuaG9zdC5jb21wb3NpdGlvbi52MS5SZXNvdXJjZUtpbmRWMRIpCgRwYWdlGAcgASgLMhsuaG9zdC5jb21wb3NpdGlvbi52MS5QYWdlVjESKgoFcm9sZXMYCCADKAsyGy5ob3N0LmNvbXBvc2l0aW9uLnYxLlJvbGVWMSJGCgZSb2xlVjESDAoEbmFtZRgBIAEoCRIuCgZzY29wZXMYAiADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMSLQAQoJQmluZGluZ1YxEgoKAmlkGAEgASgJEhAKCGF1ZGllbmNlGAIgASgJEjUKDWludm9rZV9zY29wZXMYAyADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMRI3Cg9oZWFkbGVzc19zY29wZXMYBCADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMRI1Cg1sb29rdXBfc2NvcGVzGAUgAygLMh4uaG9zdC5hdXRob3JpdHkudjEuV29ya1Njb3BlVjEiLwoOUmVzb3VyY2VLaW5kVjESDAoEa2luZBgBIAEoCRIPCgdhY3Rpb25zGAIgAygJIiYKBlBhZ2VWMRINCgV0aXRsZRgBIAEoCRINCgVvcmRlchgCIAEoBTJ8ChJDb21wb3NpdGlvblNlcnZpY2USZgoNQXBwbHlNYW5pZmVzdBIpLmhvc3QuY29tcG9zaXRpb24udjEuQXBwbHlNYW5pZmVzdFJlcXVlc3QaKi5ob3N0LmNvbXBvc2l0aW9uLnYxLkFwcGx5TWFuaWZlc3RSZXNwb25zZUIzWjFnYXRld2F5L3BrZy9nZW4vaG9zdC9jb21wb3NpdGlvbi92MTtjb21wb3NpdGlvbnYxYgZwcm90bzM", [file_host_authority_v1_work_context]); /** * ApplyManifestRequest carries the manifest. @@ -129,6 +129,16 @@ export type AuthorityManifestV1 = Message<"host.composition.v1.AuthorityManifest * @generated from field: repeated host.composition.v1.RoleV1 roles = 6; */ roles: RoleV1[]; + + /** + * The module checks a resource's owner against an owner-selected scope + * (Claims.AllowsOwned): only an audience declaring it receives one, and a + * delivery withdrawing it refuses the owner-selected capabilities issued + * before. Every solution receiving its roles' scopes declares it. + * + * @generated from field: bool owner_aware = 7; + */ + ownerAware: boolean; }; /** @@ -294,7 +304,7 @@ export type ModuleManifestV1 = Message<"host.composition.v1.ModuleManifestV1"> & /** * The roles a solution declares a person may hold on an installation of it, * each within what the solution serves or explicitly consumes through - * invoke/lookup bindings; one is named installer, held by whoever installs it. + * invoke/lookup bindings; one is named installer, held only by whom it is granted to. * Headless-only scopes cannot supply a role grant. A module declares none. * * @generated from field: repeated host.composition.v1.RoleV1 roles = 8; 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 0717f0e5..92cc19ae 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 @@ -3,8 +3,10 @@ /* eslint-disable */ // The installation interface: what is installed at an organisation and who may -// use it. Three operations on the host, and one the solution implements, the -// install step the host calls once per installation. +// use it. An installed solution is a principal of its organisation; its roles +// are levels on the data that principal owns, and a grant of a role is the +// access. Operations on the host, and one the solution implements, the install +// step the host calls once per installation. import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; @@ -14,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("Cidob3N0L2luc3RhbGxhdGlvbi92MS9pbnN0YWxsYXRpb24ucHJvdG8SFGhvc3QuaW5zdGFsbGF0aW9uLnYxImAKDkluc3RhbGxSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIOCgZ0YXJnZXQYAiABKAkSDAoEcm9sZRgDIAEoCRIXCg9vd25lcl9wZXJzb25faWQYBCABKAkiVwoSR3JhbnRUb1RlYW1SZXF1ZXN0EhcKD2luc3RhbGxhdGlvbl9pZBgBIAEoCRIPCgd0ZWFtX2lkGAIgASgJEhcKD29yZ2FuaXphdGlvbl9pZBgDIAEoCSImCgtMaXN0UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkiSQoMTGlzdFJlc3BvbnNlEjkKDWluc3RhbGxhdGlvbnMYASADKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24ivAIKDEluc3RhbGxhdGlvbhIKCgJpZBgBIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAiABKAkSDgoGdGFyZ2V0GAMgASgJEjkKBnN0YXR1cxgEIAEoDjIpLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbi5TdGF0dXMSEAoIcmV2aXNpb24YBSABKAQSDAoEcm9sZRgGIAEoCRIQCgh0ZWFtX2lkcxgHIAMoCRIaChJhZ2VudF9wcmluY2lwYWxfaWQYCCABKAkSEQoJbmFtZXNwYWNlGAkgASgJIlsKBlN0YXR1cxIWChJTVEFUVVNfVU5TUEVDSUZJRUQQABISCg5TVEFUVVNfUEVORElORxABEhEKDVNUQVRVU19BQ1RJVkUQAhISCg5TVEFUVVNfUkVWT0tFRBADIk4KEkluc3RhbGxhdGlvbk5vdGljZRI4CgxpbnN0YWxsYXRpb24YASABKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24iEQoPSW5zdGFsbGF0aW9uQWNrMpYCChNJbnN0YWxsYXRpb25TZXJ2aWNlElMKB0luc3RhbGwSJC5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsUmVxdWVzdBoiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhJbCgtHcmFudFRvVGVhbRIoLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkdyYW50VG9UZWFtUmVxdWVzdBoiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhJNCgRMaXN0EiEuaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdFJlcXVlc3QaIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0UmVzcG9uc2UycAoSSW5zdGFsbGFibGVTZXJ2aWNlEloKB0luc3RhbGwSKC5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb25Ob3RpY2UaJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb25BY2tCNVozZ2F0ZXdheS9wa2cvZ2VuL2hvc3QvaW5zdGFsbGF0aW9uL3YxO2luc3RhbGxhdGlvbnYxYgZwcm90bzM"); + fileDesc("Cidob3N0L2luc3RhbGxhdGlvbi92MS9pbnN0YWxsYXRpb24ucHJvdG8SFGhvc3QuaW5zdGFsbGF0aW9uLnYxImAKDkluc3RhbGxSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIOCgZ0YXJnZXQYAiABKAkSDAoEcm9sZRgDIAEoCRIXCg9vd25lcl9wZXJzb25faWQYBCABKAkidgoQR3JhbnRSb2xlUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEgwKBHJvbGUYAyABKAkSEQoJcGVyc29uX2lkGAQgASgJEg8KB3RlYW1faWQYBSABKAkidwoRUmV2b2tlUm9sZVJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRIMCgRyb2xlGAMgASgJEhEKCXBlcnNvbl9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJIkkKFUxpc3RSb2xlR3JhbnRzUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJIkkKFkxpc3RSb2xlR3JhbnRzUmVzcG9uc2USLwoGZ3JhbnRzGAEgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuUm9sZUdyYW50Ij0KCVJvbGVHcmFudBIMCgRyb2xlGAEgASgJEhEKCXBlcnNvbl9pZBgCIAEoCRIPCgd0ZWFtX2lkGAMgASgJIiYKC0xpc3RSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCSJJCgxMaXN0UmVzcG9uc2USOQoNaW5zdGFsbGF0aW9ucxgBIAMoCzIiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbiLdAgoMSW5zdGFsbGF0aW9uEgoKAmlkGAEgASgJEhcKD29yZ2FuaXphdGlvbl9pZBgCIAEoCRIOCgZ0YXJnZXQYAyABKAkSOQoGc3RhdHVzGAQgASgOMikuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uLlN0YXR1cxIQCghyZXZpc2lvbhgFIAEoBBIMCgRyb2xlGAYgASgJEhEKCW5hbWVzcGFjZRgJIAEoCRIUCgxwcmluY2lwYWxfaWQYCiABKAkSDQoFcm9sZXMYCyADKAkiWwoGU3RhdHVzEhYKElNUQVRVU19VTlNQRUNJRklFRBAAEhIKDlNUQVRVU19QRU5ESU5HEAESEQoNU1RBVFVTX0FDVElWRRACEhIKDlNUQVRVU19SRVZPS0VEEANKBAgHEAhKBAgIEAlSCHRlYW1faWRzUhJhZ2VudF9wcmluY2lwYWxfaWQiTgoSSW5zdGFsbGF0aW9uTm90aWNlEjgKDGluc3RhbGxhdGlvbhgBIAEoCzIiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbiIRCg9JbnN0YWxsYXRpb25BY2sy2gMKE0luc3RhbGxhdGlvblNlcnZpY2USUwoHSW5zdGFsbBIkLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uElcKCUdyYW50Um9sZRImLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkdyYW50Um9sZVJlcXVlc3QaIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24SWQoKUmV2b2tlUm9sZRInLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJldm9rZVJvbGVSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uEmsKDkxpc3RSb2xlR3JhbnRzEisuaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdFJvbGVHcmFudHNSZXF1ZXN0GiwuaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdFJvbGVHcmFudHNSZXNwb25zZRJNCgRMaXN0EiEuaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdFJlcXVlc3QaIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0UmVzcG9uc2UycAoSSW5zdGFsbGFibGVTZXJ2aWNlEloKB0luc3RhbGwSKC5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb25Ob3RpY2UaJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb25BY2tCNVozZ2F0ZXdheS9wa2cvZ2VuL2hvc3QvaW5zdGFsbGF0aW9uL3YxO2luc3RhbGxhdGlvbnYxYgZwcm90bzM"); /** * InstallRequest asks to install one target at one organisation. @@ -59,41 +61,189 @@ export const InstallRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_host_installation_v1_installation, 0); /** - * GrantToTeamRequest exposes an installation to a team. + * GrantRoleRequest grants one role to exactly one person or team. * - * @generated from message host.installation.v1.GrantToTeamRequest + * @generated from message host.installation.v1.GrantRoleRequest */ -export type GrantToTeamRequest = Message<"host.installation.v1.GrantToTeamRequest"> & { +export type GrantRoleRequest = Message<"host.installation.v1.GrantRoleRequest"> & { + /** + * The organization the installation belongs to; a request naming another + * organization's installation is refused naming the organization, before + * the caller's authority is read. + * + * @generated from field: string organization_id = 1; + */ + organizationId: string; + /** * The installation. * - * @generated from field: string installation_id = 1; + * @generated from field: string installation_id = 2; */ installationId: string; /** - * The team granted the installation's role at its root. + * One of the roles the solution declares. * - * @generated from field: string team_id = 2; + * @generated from field: string role = 3; + */ + role: string; + + /** + * The person granted, a member of the organization; exactly one of + * person_id and team_id is set. + * + * @generated from field: string person_id = 4; + */ + personId: string; + + /** + * The team granted, a team of the organization. + * + * @generated from field: string team_id = 5; */ teamId: string; +}; +/** + * Describes the 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); + +/** + * RevokeRoleRequest ends one grant, named as GrantRole named it. + * + * @generated from message host.installation.v1.RevokeRoleRequest + */ +export type RevokeRoleRequest = Message<"host.installation.v1.RevokeRoleRequest"> & { /** - * The organisation the installation belongs to; a grant is an organisation - * administrator's act, and the host checks the actor against it before it - * reads the installation. Learned from the second implementation (the host). + * The organization the installation belongs to. * - * @generated from field: string organization_id = 3; + * @generated from field: string organization_id = 1; */ organizationId: string; + + /** + * The installation. + * + * @generated from field: string installation_id = 2; + */ + installationId: string; + + /** + * The role. + * + * @generated from field: string role = 3; + */ + role: string; + + /** + * The person; exactly one of person_id and team_id is set. + * + * @generated from field: string person_id = 4; + */ + personId: string; + + /** + * The team. + * + * @generated from field: string team_id = 5; + */ + teamId: string; }; /** - * Describes the message host.installation.v1.GrantToTeamRequest. - * Use `create(GrantToTeamRequestSchema)` to create a new message. + * Describes the message host.installation.v1.RevokeRoleRequest. + * Use `create(RevokeRoleRequestSchema)` to create a new message. */ -export const GrantToTeamRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_host_installation_v1_installation, 1); +export const RevokeRoleRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 2); + +/** + * ListRoleGrantsRequest names one installation. + * + * @generated from message host.installation.v1.ListRoleGrantsRequest + */ +export type ListRoleGrantsRequest = Message<"host.installation.v1.ListRoleGrantsRequest"> & { + /** + * The organization the installation belongs to. + * + * @generated from field: string organization_id = 1; + */ + organizationId: string; + + /** + * The installation. + * + * @generated from field: string installation_id = 2; + */ + installationId: string; +}; + +/** + * Describes the message host.installation.v1.ListRoleGrantsRequest. + * Use `create(ListRoleGrantsRequestSchema)` to create a new message. + */ +export const ListRoleGrantsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 3); + +/** + * ListRoleGrantsResponse is every grant on the installation. + * + * @generated from message host.installation.v1.ListRoleGrantsResponse + */ +export type ListRoleGrantsResponse = Message<"host.installation.v1.ListRoleGrantsResponse"> & { + /** + * The grants. + * + * @generated from field: repeated host.installation.v1.RoleGrant grants = 1; + */ + grants: RoleGrant[]; +}; + +/** + * Describes the message host.installation.v1.ListRoleGrantsResponse. + * Use `create(ListRoleGrantsResponseSchema)` to create a new message. + */ +export const ListRoleGrantsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 4); + +/** + * RoleGrant is one role held by one person or team. + * + * @generated from message host.installation.v1.RoleGrant + */ +export type RoleGrant = Message<"host.installation.v1.RoleGrant"> & { + /** + * The role. + * + * @generated from field: string role = 1; + */ + role: string; + + /** + * The person, when a person holds it. + * + * @generated from field: string person_id = 2; + */ + personId: string; + + /** + * The team, when a team holds it. + * + * @generated from field: string team_id = 3; + */ + teamId: string; +}; + +/** + * Describes the 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); /** * ListRequest asks for one organisation's installations. @@ -114,7 +264,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, 2); + messageDesc(file_host_installation_v1_installation, 6); /** * ListResponse lists installations. @@ -135,7 +285,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, 3); + messageDesc(file_host_installation_v1_installation, 7); /** * Installation is one organisation's installation of one target. @@ -179,35 +329,38 @@ export type Installation = Message<"host.installation.v1.Installation"> & { revision: bigint; /** - * The role the installation holds. + * The role the installation holds when it acts for itself (its agent's + * role); no person or team inherits it. * * @generated from field: string role = 6; */ role: string; /** - * The teams granted it. + * The namespace the installation's own authored content belongs to (an + * agent's source): the host names it, so a module never chooses which source + * an installation may run. A module installed by composition is + * ".". * - * @generated from field: repeated string team_ids = 7; + * @generated from field: string namespace = 9; */ - teamIds: string[]; + namespace: string; /** - * The principal the installation acts as for itself. + * The solution principal: the identity the installation's data is owned by + * and the installation acts as for itself, a stable UUID per organization + * and target, reused by a reinstall. Empty for a module installation. * - * @generated from field: string agent_principal_id = 8; + * @generated from field: string principal_id = 10; */ - agentPrincipalId: string; + principalId: string; /** - * The namespace the installation's own authored content belongs to (an - * agent's source): the host names it, so a module never chooses which source - * an installation may run. A module installed by composition is - * ".". + * The roles the solution declares, sorted. * - * @generated from field: string namespace = 9; + * @generated from field: repeated string roles = 11; */ - namespace: string; + roles: string[]; }; /** @@ -215,7 +368,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, 4); + messageDesc(file_host_installation_v1_installation, 8); /** * Status is the installation's lifecycle. @@ -256,7 +409,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, 4, 0); + enumDesc(file_host_installation_v1_installation, 8, 0); /** * InstallationNotice is what the host hands the solution's install step. @@ -277,7 +430,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, 5); + messageDesc(file_host_installation_v1_installation, 9); /** * InstallationAck acknowledges the install step. @@ -292,7 +445,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, 6); + messageDesc(file_host_installation_v1_installation, 10); /** * InstallationService is implemented by the host and by the local authority. @@ -312,15 +465,40 @@ export const InstallationService: GenService<{ output: typeof InstallationSchema; }, /** - * GrantToTeam exposes an installation to a team under the installation's role. + * 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 + * of the installer role on that installation, grants. + * + * @generated from rpc host.installation.v1.InstallationService.GrantRole + */ + grantRole: { + methodKind: "unary"; + input: typeof GrantRoleRequestSchema; + output: typeof InstallationSchema; + }, + /** + * RevokeRole ends one grant; every capability its subject holds from it is + * refused at once. Idempotent; the same callers as GrantRole. * - * @generated from rpc host.installation.v1.InstallationService.GrantToTeam + * @generated from rpc host.installation.v1.InstallationService.RevokeRole */ - grantToTeam: { + revokeRole: { methodKind: "unary"; - input: typeof GrantToTeamRequestSchema; + input: typeof RevokeRoleRequestSchema; output: typeof InstallationSchema; }, + /** + * ListRoleGrants answers who holds which role on the installation; the same + * callers as GrantRole. + * + * @generated from rpc host.installation.v1.InstallationService.ListRoleGrants + */ + listRoleGrants: { + methodKind: "unary"; + input: typeof ListRoleGrantsRequestSchema; + output: typeof ListRoleGrantsResponseSchema; + }, /** * List answers what one organisation has installed: an administrator's read. A * member's view, which installed solutions they may use, is their navigation and diff --git a/libraries/authority-client/typescript/tests/owner.test.mjs b/libraries/authority-client/typescript/tests/owner.test.mjs index e490b79d..40dfe84c 100644 --- a/libraries/authority-client/typescript/tests/owner.test.mjs +++ b/libraries/authority-client/typescript/tests/owner.test.mjs @@ -46,3 +46,15 @@ for (const [name, last, ok] of [ else assert.throws(() => read(context), e => e instanceof Unauthenticated); }); } + +// Core's resource_owner_id rule, read from core's own vectors: every owner +// core accepts authenticates, every one it refuses does not. +const vectors = JSON.parse(readFileSync(new URL('../../testdata/resource_owner_vectors.json',import.meta.url),'utf8')); +const ownerOf = v => v.repeat ? v.owner.repeat(v.repeat) : v.owner; +const withOwner = owner => { + const context = body('session'); + context.authorityScopes = [scope({resourceKind:'record',actions:['read'],resourceOwnerId:owner})]; + return () => read(context); +}; +for (const v of vectors.accepted) test(`core accepts owner: ${v.name}`, () => { withOwner(ownerOf(v))(); }); +for (const v of vectors.refused) test(`core refuses owner: ${v.name}`, () => { assert.throws(withOwner(ownerOf(v))); }); diff --git a/libraries/authority-core/go/authority/check.go b/libraries/authority-core/go/authority/check.go index cc2dcfc5..087b1de1 100644 --- a/libraries/authority-core/go/authority/check.go +++ b/libraries/authority-core/go/authority/check.go @@ -26,7 +26,7 @@ func (s *Service) Check(ctx context.Context, token, audience string) (*workconte wc := decoded.Context() if scope.HasOwner(effectiveOf(wc)) && wc.GetOperationBinding().GetBindingId() != "" { if current, berr := s.src.OperationBinding(ctx, wc.GetOperationBinding().GetBindingId()); berr == nil && current.Incarnation != wc.GetOperationBinding().GetIncarnation() { - return nil, 0, domain.Refuse("issued through a binding re-created since, or before %s last withdrew owner-aware checking; only a fresh capability carries owner-selected scopes", audience) + return nil, 0, domain.Refuse("the capability was revoked: it was issued through a binding re-created since, or before %s last withdrew owner-aware checking; only a fresh capability carries owner-selected scopes", audience) } } } diff --git a/libraries/authority-core/go/authority/child_provenance_test.go b/libraries/authority-core/go/authority/child_provenance_test.go new file mode 100644 index 00000000..d9636db5 --- /dev/null +++ b/libraries/authority-core/go/authority/child_provenance_test.go @@ -0,0 +1,117 @@ +package authority_test + +import ( + "reflect" + "slices" + "testing" + "time" + + "github.com/codefly-dev/authority/libraries/authority-core/go/authority" + "github.com/codefly-dev/core/workcontext" + "google.golang.org/protobuf/proto" +) + +// The public Issue request cannot originate optional task or actor provenance. +// Arrange an already verified parent using the host's production Core minter +// and live seal/revision sources; exercise only the public child and Check APIs. +func TestChildPreservesVerifiedTaskAndActorProvenance(t *testing.T) { + for _, tc := range []struct { + name string + populated bool + actors []string + }{ + {name: "absent"}, + {name: "task", populated: true}, + {name: "foreign-human", actors: []string{"human"}}, + {name: "agent", actors: []string{"agent"}}, + {name: "task-and-chain", populated: true, actors: []string{"agent", "human"}}, + } { + t.Run(tc.name, func(t *testing.T) { + h := newHarness(t) + narrow := sc(kind, "read") + narrow.ResourceIds = []string{"store-x"} + root := h.must(h.root("rob", "store", narrow)) + c := root.Context + minter := workcontext.Authority{Issuer: h.cfg.Issuer, KeyID: h.cfg.KeyID, Signer: h.cfg.Signer, + Seals: h.svc.Seals(), Revisions: h.svc.Revisions(), Now: h.cfg.Clock} + input := workcontext.StartInput{TenantID: c.GetTenantId(), OrganizationID: c.GetOrganizationId(), + OwnerPrincipalID: c.GetOwnerPrincipalId(), OwnerPrincipalKind: c.GetOwnerPrincipalKind(), + TaskID: c.GetTaskId(), Audience: c.GetAudience(), AuthorityScopes: c.GetAuthorityScopes(), + InstallationID: c.GetSeal().GetInstallationId(), AttributionTeamIDs: []string{"team_a"}, + TTL: 5 * time.Minute, ReplayPolicy: workcontext.ReplayIdempotent} + if tc.populated { + input.WorkspaceID, input.ProjectID = "workspace_a", "project_a" + } + token, _, err := minter.Start(h.ctx, input) + if err != nil { + t.Fatal(err) + } + for _, kind := range tc.actors { + parent, err := h.svc.Verify(h.ctx, token, "store") + if err != nil { + t.Fatal(err) + } + actor := workcontext.ChildInput{PrincipalID: "omar", PrincipalKind: kind, OrganizationID: globex, + DelegationID: parent.Context().GetSessionId(), Audience: "store", + GrantedScopes: parent.EffectiveScopes(), TTL: 4 * time.Minute} + if kind == "agent" { + actor.PrincipalID, actor.AgentID = h.principal(globex, "a"), "a" + } + token, _, err = minter.Child(h.ctx, parent, actor) + if err != nil { + t.Fatal(err) + } + } + // Both exchanges must preserve populated values and absent fields. + for step := range 2 { + parent, _, err := h.svc.Check(h.ctx, token, "store") + if err != nil { + t.Fatalf("parent %d was not valid: %v", step, err) + } + before := parent.Context() + child := h.must(h.svc.IssueChild(h.ctx, token, authority.Workload{Module: "store"}, authority.IssueRequest{Binding: "self"})) + checked, _, err := h.svc.Check(h.ctx, child.Token, "store") + if err != nil { + t.Fatal(err) + } + after := checked.Context() + if !proto.Equal(after, child.Context) { + t.Fatal("issued claims differ from the signed, checked claims") + } + if !reflect.DeepEqual(before.WorkspaceId, after.WorkspaceId) || !reflect.DeepEqual(before.ProjectId, after.ProjectId) { + t.Errorf("step %d changed workspace/project provenance: (%q, %q) -> (%q, %q)", step, + before.GetWorkspaceId(), before.GetProjectId(), after.GetWorkspaceId(), after.GetProjectId()) + } + if before.GetTenantId() != after.GetTenantId() || before.GetOrganizationId() != after.GetOrganizationId() || + before.GetOwnerPrincipalId() != after.GetOwnerPrincipalId() || before.GetOwnerPrincipalKind() != after.GetOwnerPrincipalKind() || + before.GetTaskId() != after.GetTaskId() || !slices.Equal(before.GetAttributionTeamIds(), after.GetAttributionTeamIds()) { + t.Fatal("child changed the task's attribution") + } + if len(after.GetActorChain()) != len(before.GetActorChain())+1 { + t.Fatal("child did not append exactly one actor") + } + for n, old := range before.GetActorChain() { + next := after.GetActorChain()[n] + if old.GetPrincipalId() != next.GetPrincipalId() || old.GetPrincipalKind() != next.GetPrincipalKind() || + old.GetDelegationId() != next.GetDelegationId() || !reflect.DeepEqual(old.AgentId, next.AgentId) || + !reflect.DeepEqual(old.OrganizationId, next.OrganizationId) { + t.Errorf("step %d changed actor %d identity: %v -> %v", step, n, old, next) + } + } + last := after.GetActorChain()[len(after.GetActorChain())-1] + if last.GetPrincipalId() != "store" || last.GetPrincipalKind() != "service" || last.GetDelegationId() != before.GetSessionId() || + after.GetParentSessionId() != before.GetSessionId() || after.GetSessionId() == before.GetSessionId() || + after.GetExpiresAtUnix() > before.GetExpiresAtUnix() || after.GetOperationBinding().GetBindingId() != acme+"/store/self" { + t.Fatal("child changed the required binding, lineage or lifetime") + } + // The binding allows read/write; the owner holds read, and the + // parent restricts its resources. Preserve both limits. + exactly(t, child, kind, "", []string{"read"}, "store-x") + if !workcontext.ScopesAttenuate(checked.EffectiveScopes(), parent.EffectiveScopes()) { + t.Fatal("child recovered authority the parent did not hold") + } + token = child.Token + } + }) + } +} diff --git a/libraries/authority-core/go/authority/harness_test.go b/libraries/authority-core/go/authority/harness_test.go index 0baf5d31..f1655791 100644 --- a/libraries/authority-core/go/authority/harness_test.go +++ b/libraries/authority-core/go/authority/harness_test.go @@ -135,7 +135,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, nil) if err != nil { h.t.Fatal(err) } diff --git a/libraries/authority-core/go/authority/issue.go b/libraries/authority-core/go/authority/issue.go index 0e37974c..4adde2a2 100644 --- a/libraries/authority-core/go/authority/issue.go +++ b/libraries/authority-core/go/authority/issue.go @@ -3,6 +3,7 @@ package authority import ( "context" "errors" + "fmt" "slices" "time" @@ -278,7 +279,7 @@ func (s *Service) IssueForWorkload(ctx context.Context, w Workload, r IssueReque } binding, ok := module.Bindings[r.Binding] if !ok { - return workcontext.ErrNoBinding + return fmt.Errorf("%w: operation binding %q is not one %s declares", workcontext.ErrNoBinding, r.Binding, module.Name) } if binding.Audience != audience.Principal { return domain.Refuse("binding %s is addressed to %s, not %s", r.Binding, binding.Audience, audience.Principal) @@ -561,6 +562,7 @@ func (s *Service) derive(ctx context.Context, verified *workcontext.Verified, am } token, _, err := s.authority.Start(ctx, workcontext.StartInput{ TenantID: tenant, OrganizationID: claims.GetOrganizationId(), + WorkspaceID: claims.GetWorkspaceId(), ProjectID: claims.GetProjectId(), OwnerPrincipalID: claims.GetOwnerPrincipalId(), OwnerPrincipalKind: claims.GetOwnerPrincipalKind(), OwnerAgentID: claims.GetOwnerAgentId(), TaskID: claims.GetTaskId(), Audience: audience.Principal, AuthorityScopes: authorityScopes, AttributionTeamIDs: claims.GetAttributionTeamIds(), InstallationID: sealTo, SessionID: rootSession, TTL: ttl, ReplayPolicy: workcontext.ReplayIdempotent, @@ -577,6 +579,7 @@ func (s *Service) derive(ctx context.Context, verified *workcontext.Verified, am if n < len(hops) { in.GrantedScopes = authorityScopes in.PrincipalID, in.PrincipalKind, in.DelegationID = hops[n].GetPrincipalId(), hops[n].GetPrincipalKind(), hops[n].GetDelegationId() + in.AgentID, in.OrganizationID = hops[n].GetAgentId(), hops[n].GetOrganizationId() if n == len(hops)-1 { in.SessionID = parentSession } diff --git a/libraries/authority-core/go/authority/lifecycle.go b/libraries/authority-core/go/authority/lifecycle.go index 68f1ebc1..4afed4b7 100644 --- a/libraries/authority-core/go/authority/lifecycle.go +++ b/libraries/authority-core/go/authority/lifecycle.go @@ -176,24 +176,43 @@ func (s *Service) WithdrawModule(ctx context.Context, name string) error { type InstallStep func(ctx context.Context, inst domain.Installation) error // Install installs a solution at an organization: pending until its step -// converges, then active. Its principal is stable per organization and +// converges, then active; the host's own setup asking the same install +// 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. A caller, when named, must administer the -// organization; a host's own setup names none. -func (s *Service) Install(ctx context.Context, caller *Person, org, target, agentRole, installedBy string, step InstallStep) (domain.Installation, error) { +// 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) +} + +// 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) +} + +func (s *Service) install(ctx context.Context, authorize func(Tx) error, org, target, agentRole, installedBy string, step InstallStep) (domain.Installation, error) { var inst domain.Installation var generation uint64 err := s.store.Write(ctx, func(tx Tx) error { - if caller != nil { - p, err := tx.Person(caller.ID) - if err != nil || p.Organization != org || !decide.IsAdministrator(p) { - return domain.Refuse("installation administration requires an administrator or owner of organization %q", org) - } + if err := authorize(tx); err != nil { + return err } if _, err := tx.Organization(org); err != nil { return notFound(err) } m, err := tx.ModuleNamed(target) + if err != nil && !errors.Is(err, domain.ErrNotFound) { + return err + } if err != nil || m.Kind != domain.KindSolution { return domain.Missing("%q is not a composed solution; a module is installed by composition", target) } @@ -201,11 +220,18 @@ func (s *Service) Install(ctx context.Context, caller *Person, org, target, agen if _, ok := m.Roles[agentRole]; !ok { return domain.Missing("role %q is not one %s declares", agentRole, target) } - if p, err := tx.Person(installedBy); err != nil || p.Organization != org { + p, err := tx.Person(installedBy) + if err != nil && !errors.Is(err, domain.ErrNotFound) { + return err + } + if err != nil || p.Organization != org { return domain.Refuse("owner %q is not a member of %s", installedBy, org) } id := s.cfg.InstallationID(org, target) existing, err := tx.Installation(id) + if err != nil && !errors.Is(err, domain.ErrNotFound) { + return err + } 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) @@ -222,15 +248,23 @@ func (s *Service) Install(ctx context.Context, caller *Person, org, target, agen principal := s.cfg.PrincipalID(org, target) if _, err := tx.Module(principal); err == nil { return domain.Refuse("the principal %q of %s at %s names a module", principal, target, org) + } else if err != nil && !errors.Is(err, domain.ErrNotFound) { + return err } if _, err := tx.Person(principal); err == nil { return domain.Refuse("the principal %q of %s at %s names a person", principal, target, org) + } else if err != nil && !errors.Is(err, domain.ErrNotFound) { + return err } if other, err := tx.InstallationByPrincipal(principal); err == nil && other.ID != id { return domain.Refuse("the principal %q of %s at %s is another installation's", principal, target, org) + } else if err != nil && !errors.Is(err, domain.ErrNotFound) { + return err } if _, err := tx.TombstoneByPrincipal(principal); err == nil { return domain.Refuse("the principal %q of %s at %s is a withdrawn module's", principal, target, org) + } else if err != nil && !errors.Is(err, domain.ErrNotFound) { + return err } inst = domain.Installation{ID: id, Organization: org, Target: target, Status: domain.Pending, Revision: existing.Revision + 1, AgentRole: agentRole, PrincipalID: principal, InstalledBy: installedBy} @@ -253,6 +287,9 @@ func (s *Service) Install(ctx context.Context, caller *Person, org, target, agen return notFound(err) } m, err := tx.ModuleNamed(target) + if err != nil && !errors.Is(err, domain.ErrNotFound) { + return err + } 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) } @@ -263,42 +300,55 @@ func (s *Service) Install(ctx context.Context, caller *Person, org, target, agen return inst, err } -// Uninstall disables an installation and its principal at once: every -// capability sealed to it is refused, every capability of its organization -// minted before is refused (a hop through the solution sealed elsewhere -// included), its role grants are cleared, and the data its principal owns -// stays owned. A caller, when named, must administer organization; a host's -// own setup names none. Another organization's installation answers as an -// absent one. -func (s *Service) Uninstall(ctx context.Context, caller *Person, organization, installationID string) error { +// Uninstall disables a solution installation and its principal at once, clears +// its grants and revokes capabilities sealed to it or its organization. The +// caller must currently administer the organization. Module installations +// belong to host composition and cannot be uninstalled by a person. +func (s *Service) Uninstall(ctx context.Context, caller Person, organization, installationID string) error { return s.store.Write(ctx, func(tx Tx) error { - // Anyone but a current administrator of the organization named learns - // nothing of its installations: the foreign answer comes before any - // installation is read. - if caller != nil { - p, err := tx.Person(caller.ID) - if err != nil || p.Organization != organization || !decide.IsAdministrator(p) { - return decide.ForeignInstallation(installationID, organization) - } + p, err := tx.Person(caller.ID) + if err != nil && !errors.Is(err, domain.ErrNotFound) { + return err } - inst, err := tx.Installation(installationID) - if errors.Is(err, domain.ErrNotFound) || (err == nil && inst.Organization != organization) { + // Authorize before reading the installation: unauthorized callers + // cannot tell a present installation from an absent one. + if caller.ID == "" || err != nil || p.Organization != organization || !decide.IsAdministrator(p) { return decide.ForeignInstallation(installationID, organization) } + inst, err := installationIn(tx, organization, installationID) if err != nil { return err } - // A module is installed by composition: no person uninstalls it. The - // host's own setup (no caller) may retire it at one organization; it - // stays retired, since only a withdrawal and a redelivery reactivate - // a module's installations. - if m, err := tx.ModuleNamed(inst.Target); caller != nil && err == nil && m.Kind != domain.KindSolution { + // This identity survives withdrawal of the module manifest. A + // solution always has both its own principal and an installer. + if inst.PrincipalID == "" || inst.InstalledBy == "" { return domain.Refuse("%s is a module, installed by composition: it is withdrawn, never uninstalled", inst.Target) } return s.revoke(tx, inst) }) } +// UninstallAsHost retires an installation for trusted host setup and teardown, +// never for a request handler. A module stays retired through routine delivery; +// only host withdrawal and redelivery reactivate it. +func (s *Service) UninstallAsHost(ctx context.Context, organization, installationID string) error { + return s.store.Write(ctx, func(tx Tx) error { + inst, err := installationIn(tx, organization, installationID) + if err != nil { + return err + } + return s.revoke(tx, inst) + }) +} + +func installationIn(tx Tx, organization, installationID string) (domain.Installation, error) { + inst, err := tx.Installation(installationID) + if errors.Is(err, domain.ErrNotFound) || (err == nil && inst.Organization != organization) { + return inst, decide.ForeignInstallation(installationID, organization) + } + return inst, err +} + // revoke disables inst: its revision and its organization's authorization // revision both move, and its grants are cleared. func (s *Service) revoke(tx Tx, inst domain.Installation) error { diff --git a/libraries/authority-core/go/authority/principal_test.go b/libraries/authority-core/go/authority/principal_test.go index fb112de8..0635890a 100644 --- a/libraries/authority-core/go/authority/principal_test.go +++ b/libraries/authority-core/go/authority/principal_test.go @@ -17,7 +17,7 @@ func TestThePrincipalIsStableDistinctAndPerOrganization(t *testing.T) { if u == h.inst(acme, "a").ID || u == h.principal(acme, "b") || u == h.principal(globex, "a") { t.Fatal("a principal is shared with an installation id, another solution or another organization") } - if err := h.svc.Uninstall(h.ctx, nil, acme, h.inst(acme, "a").ID); err != nil { + if err := h.svc.UninstallAsHost(h.ctx, acme, h.inst(acme, "a").ID); err != nil { t.Fatal(err) } if again := h.install(acme, "a"); again.PrincipalID != u { @@ -197,9 +197,9 @@ func TestWhoMayGrantAndEveryMalformedGrant(t *testing.T) { code domain.Code reason string }{ - "a viewer granting itself": {h.person("maya"), authority.RoleRequest{Organization: acme, InstallationID: instA, Role: domain.InstallerRole, PersonID: "maya"}, domain.Denied, "may not grant"}, + "a viewer granting itself": {h.person("maya"), authority.RoleRequest{Organization: acme, InstallationID: instA, Role: domain.InstallerRole, PersonID: "maya"}, domain.Denied, "belongs to another organization"}, "another organization's administrator": {h.person("zoe"), authority.RoleRequest{Organization: acme, InstallationID: instA, Role: "viewer", PersonID: "dev"}, domain.Denied, "belongs to another organization"}, - "A's installer on B": {ines, authority.RoleRequest{Organization: acme, InstallationID: instB, Role: "author", PersonID: "ines"}, domain.Denied, "may not grant"}, + "A's installer on B": {ines, authority.RoleRequest{Organization: acme, InstallationID: instB, Role: "author", PersonID: "ines"}, domain.Denied, "belongs to another organization"}, "another organization named": {h.person("alex"), authority.RoleRequest{Organization: globex, InstallationID: instA, Role: "viewer", PersonID: "dev"}, domain.Denied, "organization"}, "another organization's installation": {h.person("alex"), authority.RoleRequest{Organization: acme, InstallationID: elsewhere, Role: "viewer", PersonID: "dev"}, domain.Denied, "organization"}, "an undeclared role": {h.person("alex"), authority.RoleRequest{Organization: acme, InstallationID: instA, Role: "owner", PersonID: "dev"}, domain.NotFound, "declares no role"}, @@ -215,5 +215,5 @@ func TestWhoMayGrantAndEveryMalformedGrant(t *testing.T) { }) } _, err = h.svc.ListRoleGrants(h.ctx, h.person("maya"), acme, instA) - refused(t, err, "may not") + refused(t, err, "belongs to another organization") } diff --git a/libraries/authority-core/go/authority/r124_test.go b/libraries/authority-core/go/authority/r124_test.go index 5012ba55..904ba7d5 100644 --- a/libraries/authority-core/go/authority/r124_test.go +++ b/libraries/authority-core/go/authority/r124_test.go @@ -41,7 +41,7 @@ func TestARevokeOutlivesUninstallAndARetiredRole(t *testing.T) { h := newHarness(t) id := h.inst(acme, "a").ID h.grant("a", "viewer", "maya") - if err := h.svc.Uninstall(h.ctx, nil, acme, id); err != nil { + if err := h.svc.UninstallAsHost(h.ctx, acme, id); err != nil { t.Fatal(err) } h.revoke("a", "viewer", "maya", "") diff --git a/libraries/authority-core/go/authority/r143_test.go b/libraries/authority-core/go/authority/r143_test.go index 949a9f37..8f19ef7b 100644 --- a/libraries/authority-core/go/authority/r143_test.go +++ b/libraries/authority-core/go/authority/r143_test.go @@ -28,8 +28,8 @@ func TestAnOutsiderCannotTellInstalledFromAbsent(t *testing.T) { } return want } - answer(h.svc.Uninstall(h.ctx, &zoe, acme, present), present) - answer(h.svc.Uninstall(h.ctx, &zoe, acme, absent), absent) + answer(h.svc.Uninstall(h.ctx, zoe, acme, present), present) + answer(h.svc.Uninstall(h.ctx, zoe, acme, absent), absent) for _, id := range []string{present, absent} { _, err := h.svc.ListRoleGrants(h.ctx, zoe, acme, id) answer(err, id) @@ -114,7 +114,7 @@ func TestARevokedBindingIsNeverRevived(t *testing.T) { if err != nil { t.Fatal(err) } - if err := h.svc.Uninstall(h.ctx, nil, acme, h.inst(acme, "a").ID); err != nil { + if err := h.svc.UninstallAsHost(h.ctx, acme, h.inst(acme, "a").ID); err != nil { t.Fatal(err) } h.install(acme, "a") @@ -192,12 +192,12 @@ 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 { + if _, err := h.svc.Install(h.ctx, admin, globex, "b", "author", admin.ID, down); err == nil { t.Fatal("a failing step installed") } - _, err := h.svc.Install(h.ctx, &admin, globex, "b", "viewer", admin.ID, nil) + _, 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) + inst, err := h.svc.Install(h.ctx, admin, globex, "b", "author", admin.ID, nil) if err != nil || inst.Status != domain.Active { t.Fatalf("the retry: %+v %v", inst, err) } @@ -216,7 +216,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, 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 b14b8bec..985578b1 100644 --- a/libraries/authority-core/go/authority/r148_test.go +++ b/libraries/authority-core/go/authority/r148_test.go @@ -20,8 +20,8 @@ func storeModule(generation uint64) domain.Module { func TestAModuleInstallationIsNeverUninstalled(t *testing.T) { h := newHarness(t) alex := h.admin(acme) - refused(t, h.svc.Uninstall(h.ctx, &alex, acme, h.inst(acme, "store").ID), "never uninstalled") - if err := h.svc.Uninstall(h.ctx, &alex, acme, h.inst(acme, "a").ID); err != nil { + refused(t, h.svc.Uninstall(h.ctx, alex, acme, h.inst(acme, "store").ID), "never uninstalled") + if err := h.svc.Uninstall(h.ctx, alex, acme, h.inst(acme, "a").ID); err != nil { t.Fatal(err) } // A routine delivery of a solution leaves an uninstalled installation as it is. @@ -35,7 +35,7 @@ func TestAModuleInstallationIsNeverUninstalled(t *testing.T) { } // The host's own setup may retire a module at one organization; a // routine delivery leaves it retired. - if err := h.svc.Uninstall(h.ctx, nil, acme, h.inst(acme, "store").ID); err != nil { + if err := h.svc.UninstallAsHost(h.ctx, acme, h.inst(acme, "store").ID); err != nil { t.Fatal(err) } if _, err := h.svc.ApplyModule(h.ctx, storeModule(2)); err != nil { @@ -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, nil) code(t, err, domain.Denied) } @@ -85,8 +85,8 @@ func TestNoOneButAnAdministratorTellsPresentFromAbsent(t *testing.T) { for _, id := range []string{present, absent} { want := "installation " + id + " belongs to another organization than \"acme\"" for name, err := range map[string]error{ - "a member uninstalling": h.svc.Uninstall(h.ctx, &remi, acme, id), - "a deleted ex-member": h.svc.Uninstall(h.ctx, &maya, acme, id), + "a member uninstalling": h.svc.Uninstall(h.ctx, remi, acme, id), + "a deleted ex-member": h.svc.Uninstall(h.ctx, maya, acme, id), "a deleted ex-member listing": func() error { _, err := h.svc.ListRoleGrants(h.ctx, maya, acme, id) return err diff --git a/libraries/authority-core/go/authority/r152_test.go b/libraries/authority-core/go/authority/r152_test.go new file mode 100644 index 00000000..af111255 --- /dev/null +++ b/libraries/authority-core/go/authority/r152_test.go @@ -0,0 +1,127 @@ +package authority_test + +import ( + "context" + "errors" + "testing" + + "github.com/codefly-dev/authority/libraries/authority-core/go/authority" + "github.com/codefly-dev/authority/libraries/authority-core/go/domain" +) + +func TestWithdrawnModuleCannotBeUninstalledByAPerson(t *testing.T) { + h := newHarness(t) + id := h.inst(acme, "store").ID + if err := h.svc.WithdrawModule(h.ctx, "store"); err != nil { + t.Fatal(err) + } + before := h.inst(acme, "store") + refused(t, h.svc.Uninstall(h.ctx, h.admin(acme), acme, id), "never uninstalled") + if after := h.inst(acme, "store"); after != before { + t.Fatalf("refusal mutated installation: %+v", after) + } +} + +func TestRolelessMemberGetsForeignAnswerFromEveryRoleCall(t *testing.T) { + h := newHarness(t) + for _, id := range []string{h.inst(acme, "a").ID, authority.StableInstallationID(acme, "absent")} { + r := authority.RoleRequest{Organization: acme, InstallationID: id, Role: "viewer", PersonID: "maya"} + _, grant := h.svc.GrantRole(h.ctx, h.person("remi"), r) + _, revoke := h.svc.RevokeRole(h.ctx, h.person("remi"), r) + _, list := h.svc.ListRoleGrants(h.ctx, h.person("remi"), acme, id) + for name, err := range map[string]error{"grant": grant, "revoke": revoke, "list": list} { + want := "installation " + id + " belongs to another organization than \"acme\"" + if err == nil || err.Error() != want { + t.Errorf("%s: got %v, want %s", name, err, want) + } + } + } +} + +func TestMissingCallerCannotSelectHostPower(t *testing.T) { + h := newHarness(t) + before := h.inst(acme, "store") + code(t, h.svc.Uninstall(h.ctx, authority.Person{}, acme, before.ID), domain.Denied) + 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) + code(t, err, domain.Denied) + if err := h.store.Read(h.ctx, func(tx authority.Tx) error { + _, err := tx.Installation(authority.StableInstallationID(globex, "b")) + if !errors.Is(err, domain.ErrNotFound) { + t.Fatalf("empty caller installed solution: %v", err) + } + return nil + }); err != nil { + t.Fatal(err) + } + // The same trusted setup is available only through the explicit host methods. + if err := h.svc.UninstallAsHost(h.ctx, acme, before.ID); err != nil { + t.Fatal(err) + } + if _, err := h.svc.InstallAsHost(h.ctx, globex, "b", "author", h.admin(globex).ID, nil); err != nil { + t.Fatal(err) + } +} + +type lifecycleOutage struct { + authority.Store + fail string + err error +} +type lifecycleOutageTx struct { + authority.Tx + fail string + err error +} + +func (s lifecycleOutage) Write(ctx context.Context, fn func(authority.Tx) error) error { + return s.Store.Write(ctx, func(tx authority.Tx) error { return fn(lifecycleOutageTx{tx, s.fail, s.err}) }) +} +func (t lifecycleOutageTx) Person(id string) (domain.Person, error) { + if t.fail == "person" { + return domain.Person{}, t.err + } + return t.Tx.Person(id) +} +func (t lifecycleOutageTx) Installation(id string) (domain.Installation, error) { + if t.fail == "installation" { + return domain.Installation{}, t.err + } + return t.Tx.Installation(id) +} +func (t lifecycleOutageTx) ModuleNamed(name string) (domain.Module, error) { + if t.fail == "module" { + return domain.Module{}, t.err + } + return t.Tx.ModuleNamed(name) +} + +func TestLifecycleStoreFailuresAreReturnedWithoutMutation(t *testing.T) { + for _, fail := range []string{"person", "installation", "module"} { + t.Run(fail, func(t *testing.T) { + h := newHarness(t) + outage := errors.New("store unavailable") + svc, err := authority.New(lifecycleOutage{h.store, fail, outage}, h.cfg) + if err != nil { + t.Fatal(err) + } + before := h.inst(acme, "store") + err = svc.Uninstall(h.ctx, h.admin(acme), acme, before.ID) + if fail == "module" { + // Uninstall never needs a manifest lookup to recognize a module. + refused(t, err, "never uninstalled") + } else if !errors.Is(err, outage) { + t.Fatalf("uninstall hid outage: %v", err) + } + 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) + 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 dcdedab0..2bdb56dd 100644 --- a/libraries/authority-core/go/authority/race_test.go +++ b/libraries/authority-core/go/authority/race_test.go @@ -122,14 +122,14 @@ func TestAnUninstalledAgentsHopStaysDeadAfterReinstall(t *testing.T) { if err := h.check(deeper); err != nil { t.Fatal(err) } - if err := h.svc.Uninstall(h.ctx, nil, acme, h.inst(acme, "a").ID); err != nil { + if err := h.svc.UninstallAsHost(h.ctx, acme, h.inst(acme, "a").ID); err != nil { t.Fatal(err) } if h.check(deeper) == nil { 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, nil); err != nil { t.Fatal(err) } if h.check(deeper) == nil { @@ -146,7 +146,7 @@ func TestAnUninstallEndsEveryHopThroughTheSolution(t *testing.T) { if err := h.check(deeper); err != nil { t.Fatal(err) } - if err := h.svc.Uninstall(h.ctx, nil, acme, h.inst(acme, "a").ID); err != nil { + if err := h.svc.UninstallAsHost(h.ctx, acme, h.inst(acme, "a").ID); err != nil { t.Fatal(err) } if h.check(deeper) == nil { diff --git a/libraries/authority-core/go/authority/revocation_test.go b/libraries/authority-core/go/authority/revocation_test.go index 8e4ef190..317a27ce 100644 --- a/libraries/authority-core/go/authority/revocation_test.go +++ b/libraries/authority-core/go/authority/revocation_test.go @@ -75,7 +75,7 @@ func TestUninstallDisablesThePrincipalAndClearsGrants(t *testing.T) { parent := h.must(h.root("maya", "a")) agent := h.must(h.agent("a", "store")) inst := h.inst(acme, "a").ID - if err := h.svc.Uninstall(h.ctx, nil, acme, inst); err != nil { + if err := h.svc.UninstallAsHost(h.ctx, acme, inst); err != nil { t.Fatal(err) } for _, c := range []authority.Issued{before, agent} { diff --git a/libraries/authority-core/go/authority/roles.go b/libraries/authority-core/go/authority/roles.go index 84ecbf63..9c59d4f9 100644 --- a/libraries/authority-core/go/authority/roles.go +++ b/libraries/authority-core/go/authority/roles.go @@ -25,7 +25,11 @@ func decideRoleChange(tx Tx, caller Person, r RoleRequest, list, revoke bool) (d // it: the foreign answer comes before any installation is read. Then the // installation is resolved by (organization, id): another organization's // and an absent one answer alike, having done the same work. - if callerOrganization(tx, caller) != r.Organization { + c, err := tx.Person(caller.ID) + if err != nil && !errors.Is(err, domain.ErrNotFound) { + return domain.Installation{}, err + } + if caller.ID == "" || err != nil || c.Organization != r.Organization { return domain.Installation{}, decide.ForeignInstallation(r.InstallationID, r.Organization) } inst, err := tx.Installation(r.InstallationID) @@ -35,10 +39,6 @@ func decideRoleChange(tx Tx, caller Person, r RoleRequest, list, revoke bool) (d if err != nil { return inst, err } - c, err := tx.Person(caller.ID) - if err != nil { - c = domain.Person{ID: caller.ID, Organization: caller.Organization} - } grants, err := tx.RoleGrants(inst.ID) if err != nil { return inst, err diff --git a/libraries/authority-core/go/authority/service.go b/libraries/authority-core/go/authority/service.go index def6b9df..5be0cdcd 100644 --- a/libraries/authority-core/go/authority/service.go +++ b/libraries/authority-core/go/authority/service.go @@ -137,7 +137,12 @@ func (s *Service) Verify(ctx context.Context, token, audience string) (*workcont if err != nil { return nil, err } - return v.Verify(ctx, token) + verified, err := v.Verify(ctx, token) + if errors.Is(err, workcontext.ErrRevoked) { + // A seal, a grant or a membership moved since issue: say so by name. + return nil, fmt.Errorf("the capability was revoked: %w", err) + } + return verified, err } type noGrants struct{} diff --git a/libraries/authority-core/go/authority/sources_test.go b/libraries/authority-core/go/authority/sources_test.go index 779feb62..cbbc9668 100644 --- a/libraries/authority-core/go/authority/sources_test.go +++ b/libraries/authority-core/go/authority/sources_test.go @@ -22,14 +22,14 @@ func TestAReinstallNeverRepeatsThePrincipalsEpoch(t *testing.T) { if err != nil { t.Fatal(err) } - if err := h.svc.Uninstall(h.ctx, nil, acme, h.inst(acme, "a").ID); err != nil { + if err := h.svc.UninstallAsHost(h.ctx, acme, h.inst(acme, "a").ID); err != nil { t.Fatal(err) } if _, err := h.svc.Seals().PrincipalEpoch(h.ctx, u); err == nil { 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, nil); err != nil { t.Fatal(err) } after, err := h.svc.Seals().PrincipalEpoch(h.ctx, u) @@ -50,7 +50,7 @@ func TestAnUninstallMovesItsOrganizationsRevision(t *testing.T) { return r } acmeBefore, globexBefore := rev(acme), rev(globex) - if err := h.svc.Uninstall(h.ctx, nil, acme, h.inst(acme, "a").ID); err != nil { + if err := h.svc.UninstallAsHost(h.ctx, acme, h.inst(acme, "a").ID); err != nil { t.Fatal(err) } if rev(acme) <= acmeBefore || rev(globex) != globexBefore { @@ -66,9 +66,9 @@ func TestAnUninstallIsTheOrganizationsAdministrators(t *testing.T) { id := h.inst(acme, "a").ID maya, zoe := h.person("maya"), h.person("zoe") // A member who administers nothing learns nothing either (r148 m2). - refused(t, h.svc.Uninstall(h.ctx, &maya, acme, id), "belongs to another organization") - foreign := h.svc.Uninstall(h.ctx, &zoe, globex, id) - absent := h.svc.Uninstall(h.ctx, &zoe, globex, "no-such-installation") + refused(t, h.svc.Uninstall(h.ctx, maya, acme, id), "belongs to another organization") + foreign := h.svc.Uninstall(h.ctx, zoe, globex, id) + absent := h.svc.Uninstall(h.ctx, zoe, globex, "no-such-installation") code(t, foreign, domain.Denied) if want := "installation " + id + " belongs to another organization than \"globex\""; foreign.Error() != want { t.Fatalf("foreign %q", foreign) @@ -80,7 +80,7 @@ func TestAnUninstallIsTheOrganizationsAdministrators(t *testing.T) { t.Fatalf("a refused uninstall changed %s: %v", id, err) } alex := h.admin(acme) - if err := h.svc.Uninstall(h.ctx, &alex, acme, id); err != nil { + if err := h.svc.Uninstall(h.ctx, alex, acme, id); err != nil { t.Fatal(err) } } @@ -130,7 +130,7 @@ func TestTheSealRevisionNeverRepeats(t *testing.T) { t.Fatal(err) } step("owner-aware withdrawn") - if err := h.svc.Uninstall(h.ctx, nil, acme, id); err != nil { + if err := h.svc.UninstallAsHost(h.ctx, acme, id); err != nil { t.Fatal(err) } if _, err := h.svc.Seals().Seal(h.ctx, "maya", id); !errors.Is(err, workcontext.ErrNoSeal) { @@ -268,7 +268,7 @@ func TestARevokedGrantStaysGone(t *testing.T) { return len(g) } h.grant("a", "viewer", "maya") - if err := h.svc.Uninstall(h.ctx, nil, acme, id); err != nil { + if err := h.svc.UninstallAsHost(h.ctx, acme, id); err != nil { t.Fatal(err) } h.revoke("a", "viewer", "maya", "") diff --git a/libraries/authority-core/go/decide/decide.go b/libraries/authority-core/go/decide/decide.go index 2d4dc71f..c416667e 100644 --- a/libraries/authority-core/go/decide/decide.go +++ b/libraries/authority-core/go/decide/decide.go @@ -187,10 +187,7 @@ func ChangeRole(r RoleChange) error { return ForeignInstallation(r.RequestedID, r.RequestOrganization) } if r.Caller.Organization != inst.Organization || (!IsAdministrator(r.Caller) && !slices.Contains(r.CallerRoles, domain.InstallerRole)) { - if r.List { - return domain.Refuse("%s may not list the role grants of %s", r.Caller.ID, inst.ID) - } - return domain.Refuse("%s may not grant roles on %s: an administrator of %s or its installer grants", r.Caller.ID, inst.ID, inst.Organization) + return ForeignInstallation(r.RequestedID, r.RequestOrganization) } if r.Revoke { // A grant stays revocable whatever happened since: an installation diff --git a/libraries/authority-core/go/decide/decide_test.go b/libraries/authority-core/go/decide/decide_test.go index 4444c7b8..94fca055 100644 --- a/libraries/authority-core/go/decide/decide_test.go +++ b/libraries/authority-core/go/decide/decide_test.go @@ -129,18 +129,18 @@ func TestWhoMayChangeARoleInOrder(t *testing.T) { reason string }{ "another organization, even for a non-admin": {func(r *decide.RoleChange) { r.RequestOrganization = "globex"; r.Caller = member }, domain.Denied, "organization"}, - "a member": {func(r *decide.RoleChange) { r.Caller = member }, domain.Denied, "may not grant"}, + "a member": {func(r *decide.RoleChange) { r.Caller = member }, domain.Denied, "belongs to another organization"}, "another organization's admin": {func(r *decide.RoleChange) { r.Caller = domain.Person{ID: "zoe", Organization: "globex", Roles: []string{"admin"}} - }, domain.Denied, "may not grant"}, + }, domain.Denied, "belongs to another organization"}, "a disabled installation": {func(r *decide.RoleChange) { r.Installation.Status = domain.Revoked }, domain.Precondition, "not installed"}, "an undeclared role": {func(r *decide.RoleChange) { r.Role = "owner" }, domain.NotFound, "declares no role"}, "both subjects": {func(r *decide.RoleChange) { r.TeamID = "t" }, domain.Invalid, "exactly one"}, "no subject": {func(r *decide.RoleChange) { r.PersonID = "" }, domain.Invalid, "exactly one"}, "a person of another organization": {func(r *decide.RoleChange) { r.PersonID, r.Subject = "omar", &omar }, domain.Denied, "not a member"}, "a team of another organization": {func(r *decide.RoleChange) { r.PersonID, r.TeamID = "", "ops" }, domain.Denied, "not a team of"}, - "a member listing": {func(r *decide.RoleChange) { r.Caller, r.List = member, true }, domain.Denied, "may not list"}, - "a member revoking": {func(r *decide.RoleChange) { r.Caller, r.Revoke = member, true }, domain.Denied, "may not grant"}, + "a member listing": {func(r *decide.RoleChange) { r.Caller, r.List = member, true }, domain.Denied, "belongs to another organization"}, + "a member revoking": {func(r *decide.RoleChange) { r.Caller, r.Revoke = member, true }, domain.Denied, "belongs to another organization"}, } { t.Run(name, func(t *testing.T) { r := base() diff --git a/libraries/authority-core/go/go.mod b/libraries/authority-core/go/go.mod index 68a22f92..5bb7baf5 100644 --- a/libraries/authority-core/go/go.mod +++ b/libraries/authority-core/go/go.mod @@ -5,7 +5,7 @@ go 1.27.3 require ( github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261026023425-000d44495d93 github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261026023425-000d44495d93 - github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435 + github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515 github.com/google/uuid v1.6.0 google.golang.org/protobuf v1.36.12 ) diff --git a/libraries/authority-core/go/go.sum b/libraries/authority-core/go/go.sum index d1591e5d..b536cab0 100644 --- a/libraries/authority-core/go/go.sum +++ b/libraries/authority-core/go/go.sum @@ -14,8 +14,8 @@ github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261026023425-000 github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261026023425-000d44495d93/go.mod h1:XsOFnO2MQdr37vM3ULa0Uwi8ZWptyVx7hRSUyIdaZ1Q= github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261026023425-000d44495d93 h1:4EygH7/J0E6aM7hUPdbQGfit1rsLUNCtWJ8kMh2b9PE= github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261026023425-000d44495d93/go.mod h1:1rmjrVyr0h7zOuwOHS9i+BoeS9dr3XNvtupLZFwY6As= -github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435 h1:3M1c4Ver3YRaHj4rk1+mj1uweqTF5bO2QHZ50rI7sNI= -github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= +github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515 h1:u6dYtxKTnEYeOwlgRx3sfJbxDl6UWI4uhpvUqOR4Ho0= +github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/libraries/conformance/go/README.md b/libraries/conformance/go/README.md index 3672da43..b60834bc 100644 --- a/libraries/conformance/go/README.md +++ b/libraries/conformance/go/README.md @@ -95,9 +95,8 @@ implements it. The model, in full, is the doc comment on only by `AuthorizeTransfer`, with the person's own admin on that resource and write on U's data through their current roles, read live. -The target deletes `GrantToTeam`, `team_ids` and `agent_principal_id`, and -changes the composition contract's text that the installing person holds the -installer role. +The contract has no `GrantToTeam`, `team_ids` or `agent_principal_id`, and the +installing person holds no role unless granted one. The suite first names what the published contract lacks and still carries, then runs these cases, each making its own grants: @@ -152,7 +151,6 @@ installing person holds no role unless granted, so a Matrix row such as the installer publishing at a solution needs an explicit installer grant), and declares `owner_aware` on the modules solutions consume. -Run the conformance target with `AUTHORITY_TARGET=solution-principal` from -`modules/authority/services/local/code`: -`go test ./plugins/registry_test -run SolutionPrincipal`. It skips without it -until the local authority implements it. +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. diff --git a/libraries/conformance/go/conformance.go b/libraries/conformance/go/conformance.go index 7008b7a6..050f1ce8 100644 --- a/libraries/conformance/go/conformance.go +++ b/libraries/conformance/go/conformance.go @@ -50,12 +50,6 @@ type Fixture struct { InstalledSolution string // an active installation the viewer is entitled to InstalledCredential string // workload credential of InstalledAudience, required for installation-owned Issue InstalledAudience string // the installed solution principal; distinct from the organisation-role module Audience - // UnexposedTeam is a real team of Organization that InstalledSolution does - // not reach when the run starts, from the host's own fixture. The - // staleness probe exposes InstalledSolution to it, which moves the - // organisation's authority; its members keep that role for the rest of the - // run, so a team with no members is the safe choice. - UnexposedTeam string // LocalAdvisory enables the local-only advisory Check contract. LocalAdvisory bool @@ -70,10 +64,9 @@ type Fixture struct { // SolutionTools exercises a module binding to a solution with distinct person entitlements. SolutionTools *SolutionToolsCase - // A delivery, as Codefly signs it. When SignManifest is set the suite applies - // manifests as signed carriers: core's module manifest for HostCoordinate and - // HostDomain, signed by SignManifest, carried by DeliveryCredential(module). - // Nil applies the sketch, which the transition still allows. + // A delivery, as Codefly signs it. The suite requires signed carriers: + // core's module manifest for HostCoordinate and HostDomain, signed by + // SignManifest, carried by DeliveryCredential(module). HostCoordinate string HostDomain string SignManifest func(document []byte) (envelope []byte, err error) @@ -166,10 +159,9 @@ type Expectation struct { Allowed bool } -// applyManifest composes one module at one generation, as a signed carrier when -// the fixture signs, as the sketch otherwise. A solution declares its installer -// and viewer roles over what it serves: in the authority's document when it is -// carried, in the sketch otherwise, since core's manifest carries no roles. +// applyManifest composes one module at one generation as a signed carrier. +// A solution declares its installer and viewer roles in the paired authority +// document, since core's manifest carries no roles. func (c *client) applyManifest(ctx context.Context, f Fixture, alias, kind string, generation uint64, kinds []string) (protoreflect.Message, error) { var roles, ceilings []any if kind == "solution" { @@ -181,26 +173,12 @@ func (c *client) applyManifest(ctx context.Context, f Fixture, alias, kind strin roles = []any{map[string]any{"name": "installer", "scopes": all}, map[string]any{"name": "viewer", "scopes": reads}} ceilings = all } - if f.SignManifest == nil { - var rks []any - for _, k := range kinds { - rks = append(rks, map[string]any{"kind": k, "actions": []any{"read", "write"}}) - } - manifest := map[string]any{"module": alias, "principal": alias, "kind": kind, "generation": generation, "resource_kinds": rks} - if roles != nil { - manifest["roles"] = roles - } - return c.call(ctx, "host.composition.v1.CompositionService", "ApplyManifest", map[string]any{"manifest": manifest}) - } if roles == nil { return c.deliver(ctx, f, alias, kind, generation, kinds, nil, 0) } - document := map[string]any{"scope_ceilings": ceilings, "roles": roles} - if c.solutionPrincipal() { - // A solution receives owner-selected scopes, so it declares owner-aware - // checking under the solution-principal contract. - document["owner_aware"] = true - } + // A solution receives owner-selected scopes, so it declares owner-aware + // checking. + document := map[string]any{"scope_ceilings": ceilings, "roles": roles, "owner_aware": true} return c.deliver(ctx, f, alias, kind, generation, kinds, document, 0) } @@ -216,6 +194,12 @@ func (c *client) deliverWithMethods(ctx context.Context, f Fixture, alias string // when document names any field, the authority's own document signed by the same // key and bound to the manifest by module, generation and digest. func (c *client) deliver(ctx context.Context, f Fixture, alias, kind string, generation uint64, kinds []string, document map[string]any, skew uint64, bindings ...solutionhost.AuthorityBinding) (protoreflect.Message, error) { + if f.SignManifest == nil || f.DeliveryCredential == nil { + return nil, fmt.Errorf("composition requires a release signer and delivery workload credential") + } + if document != nil && f.SignAuthority == nil { + return nil, fmt.Errorf("composition with an authority document requires its release signer") + } manifest := &solutionhost.ModuleManifest{ Schema: solutionhost.SchemaModuleManifestV1, Binding: alias, Alias: alias, Kind: solutionhost.Kind(kind), Generation: generation, Host: solutionhost.HostTarget{Coordinate: f.HostCoordinate, Domain: f.HostDomain}, @@ -408,65 +392,42 @@ func Run(t *testing.T, conn grpc.ClientConnInterface, descriptors []byte, f Fixt _, err := c.call(ctx, "host.authority.v1.AuthorityService", "Check", map[string]any{"token": str(short, "token"), "audience": f.Audience}) refused(t, err, codes.Unauthenticated, "expired") }) - if f.InstalledSolution == "" || f.UnexposedTeam == "" { - t.Fatal("required conformance fixture missing: staleness needs an installation and a real team it is not yet exposed to") + if f.InstalledSolution == "" { + t.Fatal("required conformance fixture missing: staleness needs an installation") } admin := c.must(t, ctx, "host.session.v1.SessionService", "SignIn", map[string]any{"fixture_token": f.AdminToken}) c.must(t, ctx, "host.authority.v1.AuthorityService", "Check", map[string]any{"token": token, "audience": f.Audience}) - if c.solutionPrincipal() { - // Under the solution-principal contract a grant moves only its - // subject's authority: the callee refuses a capability once its - // person's roles are revoked. The fixture's grants are restored - // exactly, and a viewer reaching the installation through a team - // would keep access, so the fixture must not grant one. - viewer := c.must(t, ctx, "host.session.v1.SessionService", "SignIn", map[string]any{"fixture_token": f.ViewerToken}) - installation := map[string]any{"organization_id": f.Organization, "installation_id": f.InstalledSolution} - held := c.must(t, as(ctx, admin), "host.installation.v1.InstallationService", "ListRoleGrants", installation) - teams := listStrings(viewer, "team_ids") - var direct []string - for _, g := range listMsgs(held, "grants") { - if slices.Contains(teams, str(g, "team_id")) { - t.Fatalf("required conformance fixture invalid: the staleness probe needs the viewer to hold %s's roles only directly; team %s holds %s", f.InstalledSolution, str(g, "team_id"), str(g, "role")) - } - if str(g, "person_id") == str(viewer, "person_id") { - direct = append(direct, str(g, "role")) - } - } - role := func(name string) map[string]any { - return map[string]any{"organization_id": f.Organization, "installation_id": f.InstalledSolution, "role": name, "person_id": str(viewer, "person_id")} + // A grant moves only its subject's authority: the callee refuses a + // capability once its person's roles are revoked. The fixture's grants are restored + // exactly, and a viewer reaching the installation through a team + // would keep access, so the fixture must not grant one. + viewer := c.must(t, ctx, "host.session.v1.SessionService", "SignIn", map[string]any{"fixture_token": f.ViewerToken}) + installation := map[string]any{"organization_id": f.Organization, "installation_id": f.InstalledSolution} + held := c.must(t, as(ctx, admin), "host.installation.v1.InstallationService", "ListRoleGrants", installation) + teams := listStrings(viewer, "team_ids") + var direct []string + for _, g := range listMsgs(held, "grants") { + if slices.Contains(teams, str(g, "team_id")) { + t.Fatalf("required conformance fixture invalid: the staleness probe needs the viewer to hold %s's roles only directly; team %s holds %s", f.InstalledSolution, str(g, "team_id"), str(g, "role")) } - if len(direct) == 0 { - c.must(t, as(ctx, admin), "host.installation.v1.InstallationService", "GrantRole", role("viewer")) - } - token := str(c.must(t, as(ctx, viewer), "host.authority.v1.AuthorityService", "Issue", map[string]any{"person": map[string]any{}, "audience": f.InstalledAudience}), "token") - for _, name := range append(slices.Clone(direct), "viewer") { - c.must(t, as(ctx, admin), "host.installation.v1.InstallationService", "RevokeRole", role(name)) - } - _, err := c.call(ctx, "host.authority.v1.AuthorityService", "Check", map[string]any{"token": token, "audience": f.InstalledAudience}) - refused(t, err, codes.PermissionDenied, "revoked") - for _, name := range direct { - c.must(t, as(ctx, admin), "host.installation.v1.InstallationService", "GrantRole", role(name)) - } - return - } - // A team the installation already reaches moves nothing, as a second run - // against a long-lived authority finds: that is the fixture's failure. - listed := c.must(t, as(ctx, admin), "host.installation.v1.InstallationService", "List", map[string]any{"organization_id": f.Organization}) - for _, inst := range listMsgs(listed, "installations") { - if str(inst, "id") != f.InstalledSolution { - continue - } - teams := field(inst, "team_ids").List() - for i := 0; i < teams.Len(); i++ { - if teams.Get(i).String() == f.UnexposedTeam { - t.Fatalf("required conformance fixture invalid: UnexposedTeam %q is already exposed to %s", f.UnexposedTeam, f.InstalledSolution) - } + if str(g, "person_id") == str(viewer, "person_id") { + direct = append(direct, str(g, "role")) } } - c.must(t, as(ctx, admin), "host.installation.v1.InstallationService", "GrantToTeam", map[string]any{"organization_id": f.Organization, "installation_id": f.InstalledSolution, "team_id": f.UnexposedTeam}) - _, err := c.call(ctx, "host.authority.v1.AuthorityService", "Check", map[string]any{"token": token, "audience": f.Audience}) - if status.Code(err) != codes.PermissionDenied { - t.Fatalf("a token sealed before the organisation's authority moved is stale, refused as PermissionDenied: %v", err) + role := func(name string) map[string]any { + return map[string]any{"organization_id": f.Organization, "installation_id": f.InstalledSolution, "role": name, "person_id": str(viewer, "person_id")} + } + if len(direct) == 0 { + c.must(t, as(ctx, admin), "host.installation.v1.InstallationService", "GrantRole", role("viewer")) + } + revokedToken := str(c.must(t, as(ctx, viewer), "host.authority.v1.AuthorityService", "Issue", map[string]any{"person": map[string]any{}, "audience": f.InstalledAudience}), "token") + for _, name := range append(slices.Clone(direct), "viewer") { + c.must(t, as(ctx, admin), "host.installation.v1.InstallationService", "RevokeRole", role(name)) + } + _, err := c.call(ctx, "host.authority.v1.AuthorityService", "Check", map[string]any{"token": revokedToken, "audience": f.InstalledAudience}) + refused(t, err, codes.PermissionDenied, "revoked") + for _, name := range direct { + c.must(t, as(ctx, admin), "host.installation.v1.InstallationService", "GrantRole", role(name)) } }) t.Run("every refusal reaches the caller with a reason", func(t *testing.T) { @@ -532,9 +493,7 @@ func Run(t *testing.T, conn grpc.ClientConnInterface, descriptors []byte, f Fixt installation = i } } - // The principal the installation acts as for itself: agent_principal_id - // today, principal_id under the solution-principal target, which - // deletes agent_principal_id (installationPrincipal). + // The principal the installation acts as for itself. if installation == nil || enumName(installation, "status") != "STATUS_ACTIVE" || installationPrincipal(installation) == "" { t.Fatalf("installed solution = %v", installation) } diff --git a/libraries/conformance/go/go.mod b/libraries/conformance/go/go.mod index ed296dd7..1d9bcafb 100644 --- a/libraries/conformance/go/go.mod +++ b/libraries/conformance/go/go.mod @@ -5,7 +5,7 @@ go 1.27.3 require ( github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261025234644-fff5385f9203 github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261023203940-e720ad2851c7 - github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435 + github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515 google.golang.org/grpc v1.84.0 google.golang.org/protobuf v1.36.12 ) diff --git a/libraries/conformance/go/go.sum b/libraries/conformance/go/go.sum index d2fa6c9c..0d8a5270 100644 --- a/libraries/conformance/go/go.sum +++ b/libraries/conformance/go/go.sum @@ -32,8 +32,8 @@ github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-2026102523 github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261025234644-fff5385f9203/go.mod h1:Oher/sd93bXgASlhZgR1BAdrbxc7QLuGPCkDXllHvWU= github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261023203940-e720ad2851c7 h1:7vdTj6aLQVyFf0MWWgUnR/otX1v9xN6AbFKC2cHrQjE= github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261023203940-e720ad2851c7/go.mod h1:1rmjrVyr0h7zOuwOHS9i+BoeS9dr3XNvtupLZFwY6As= -github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435 h1:3M1c4Ver3YRaHj4rk1+mj1uweqTF5bO2QHZ50rI7sNI= -github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= +github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515 h1:u6dYtxKTnEYeOwlgRx3sfJbxDl6UWI4uhpvUqOR4Ho0= +github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= diff --git a/libraries/conformance/go/installation_lifecycle.go b/libraries/conformance/go/installation_lifecycle.go index 4b1426fc..ab95d049 100644 --- a/libraries/conformance/go/installation_lifecycle.go +++ b/libraries/conformance/go/installation_lifecycle.go @@ -23,7 +23,9 @@ func (c *client) installationIdentity(t *testing.T, ctx context.Context, f Fixtu name, id, credential, org, reason string code codes.Code }{ - {"an installation issue verifies the installation exists", "conformance-missing-installation", f.InstalledCredential, "", "installation", codes.NotFound}, + // An absent installation answers as another organization's does: a + // solution learns nothing of which organizations installed what. + {"an installation issue verifies the installation exists", "conformance-missing-installation", f.InstalledCredential, "", "installation", codes.PermissionDenied}, {"an installation issue authenticates the installed solution", f.InstalledSolution, f.WorkloadCredential, "", "installed solution", codes.PermissionDenied}, {"an installation issue cannot name a different organization", f.InstalledSolution, f.InstalledCredential, "conformance-foreign-org", "organization", codes.PermissionDenied}, } { @@ -88,22 +90,19 @@ func (c *client) installationLifecycle(t *testing.T, ctx context.Context, f Fixt issue := func() (protoreflect.Message, error) { return c.call(ctx, "host.authority.v1.AuthorityService", "Issue", map[string]any{"installation": map[string]any{"installation_id": id}, "actor": map[string]any{"credential": l.Credential}, "audience": l.Target}) } - grant := map[string]any{"organization_id": f.Organization, "installation_id": id, "team_id": l.Team} - // Exposure is GrantToTeam before the solution-principal contract and a - // team's role grant (GrantRole) under it, which deletes GrantToTeam. - expose := "GrantToTeam" - if c.solutionPrincipal() { - expose = "GrantRole" - grant["role"] = "viewer" - } + // Exposure is a team's role grant. + grant := map[string]any{"organization_id": f.Organization, "installation_id": id, "team_id": l.Team, "role": "viewer"} + const expose = "GrantRole" t.Run("pending installation cannot issue or be exposed", func(t *testing.T) { if enumName(pending, "status") != "STATUS_PENDING" { t.Fatal("installation became active before acknowledgement") } _, err := issue() - refused(t, err, codes.PermissionDenied, "pending") + // The installation is not installed yet: a state that may still + // change, as the solution-principal target names it. + refused(t, err, codes.FailedPrecondition, "pending") _, err = c.call(adminCtx, "host.installation.v1.InstallationService", expose, grant) - refused(t, err, codes.PermissionDenied, "pending") + refused(t, err, codes.FailedPrecondition, "pending") }) resume() select { @@ -115,14 +114,10 @@ func (c *client) installationLifecycle(t *testing.T, ctx context.Context, f Fixt t.Fatal("acknowledged install did not finish") } t.Run("acknowledged installation is exposed to its team", func(t *testing.T) { - exposed := c.must(t, adminCtx, "host.installation.v1.InstallationService", expose, grant) - if c.solutionPrincipal() { - listed := c.must(t, adminCtx, "host.installation.v1.InstallationService", "ListRoleGrants", map[string]any{"organization_id": f.Organization, "installation_id": id}) - 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) - } - } else if enumName(exposed, "status") != "STATUS_ACTIVE" || count(exposed, "team_ids") != 1 || u64(exposed, "revision") != u64(pending, "revision")+1 { - t.Fatal("acknowledged installation was not exposed exactly once") + c.must(t, adminCtx, "host.installation.v1.InstallationService", expose, grant) + listed := c.must(t, adminCtx, "host.installation.v1.InstallationService", "ListRoleGrants", map[string]any{"organization_id": f.Organization, "installation_id": id}) + 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") @@ -141,7 +136,7 @@ func (c *client) installationLifecycle(t *testing.T, ctx context.Context, f Fixt t.Fatal(err) } _, err := issue() - refused(t, err, codes.PermissionDenied, "revoked") + 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, "") }) diff --git a/libraries/conformance/go/solution_principal.go b/libraries/conformance/go/solution_principal.go index 8d1cef33..6c00a23c 100644 --- a/libraries/conformance/go/solution_principal.go +++ b/libraries/conformance/go/solution_principal.go @@ -344,14 +344,16 @@ func RunSolutionPrincipalTarget(t *testing.T, conn grpc.ClientConnInterface, des code codes.Code reason string }{ - "a viewer granting itself installer": {f.Viewer, "GrantRole", grant(instA, "installer", sp.person(t, f.Viewer)), codes.PermissionDenied, "may not grant"}, - "a viewer revoking the installer": {f.Viewer, "RevokeRole", grant(instA, "installer", sp.person(t, f.Installer)), codes.PermissionDenied, "may not grant"}, - "a viewer listing grants": {f.Viewer, "ListRoleGrants", map[string]any{"organization_id": f.Organization, "installation_id": instA}, codes.PermissionDenied, "may not"}, - "another organization's administrator granting": {f.OtherAdmin, "GrantRole", grant(instA, "viewer", member), codes.PermissionDenied, "may not grant"}, - "another organization's administrator revoking": {f.OtherAdmin, "RevokeRole", grant(instA, "viewer", sp.person(t, f.Viewer)), codes.PermissionDenied, "may not grant"}, - "another organization's administrator listing": {f.OtherAdmin, "ListRoleGrants", map[string]any{"organization_id": f.Organization, "installation_id": instA}, codes.PermissionDenied, "may not"}, - "A's installer granting on B": {f.Installer, "GrantRole", grant(instB, "author", sp.person(t, f.Installer)), codes.PermissionDenied, "may not grant"}, - "A's installer revoking on B": {f.Installer, "RevokeRole", grant(instB, "viewer", member), codes.PermissionDenied, "may not grant"}, + "a viewer granting itself installer": {f.Viewer, "GrantRole", grant(instA, "installer", sp.person(t, f.Viewer)), codes.PermissionDenied, "belongs to another organization"}, + "a viewer revoking the installer": {f.Viewer, "RevokeRole", grant(instA, "installer", sp.person(t, f.Installer)), codes.PermissionDenied, "belongs to another organization"}, + "a viewer listing grants": {f.Viewer, "ListRoleGrants", map[string]any{"organization_id": f.Organization, "installation_id": instA}, codes.PermissionDenied, "belongs to another organization"}, + // A caller of another organization learns nothing of this one: the + // installation answers as an absent one would (r143 M-a). + "another organization's administrator granting": {f.OtherAdmin, "GrantRole", grant(instA, "viewer", member), codes.PermissionDenied, "belongs to another organization"}, + "another organization's administrator revoking": {f.OtherAdmin, "RevokeRole", grant(instA, "viewer", sp.person(t, f.Viewer)), codes.PermissionDenied, "belongs to another organization"}, + "another organization's administrator listing": {f.OtherAdmin, "ListRoleGrants", map[string]any{"organization_id": f.Organization, "installation_id": instA}, codes.PermissionDenied, "belongs to another organization"}, + "A's installer granting on B": {f.Installer, "GrantRole", grant(instB, "author", sp.person(t, f.Installer)), codes.PermissionDenied, "belongs to another organization"}, + "A's installer revoking on B": {f.Installer, "RevokeRole", grant(instB, "viewer", member), codes.PermissionDenied, "belongs to another organization"}, "another organization's id": {f.Admin, "GrantRole", map[string]any{"organization_id": f.OtherOrganization, "installation_id": instA, "role": "viewer", "person_id": sp.personID(t, f.Member)}, codes.PermissionDenied, "organization"}, "granting on another organization's installation": {f.Admin, "GrantRole", grant(instElsewhere, "viewer", member), codes.PermissionDenied, "organization"}, "revoking on another organization's installation": {f.Admin, "RevokeRole", grant(instElsewhere, "viewer", sp.person(t, f.OtherMember)), codes.PermissionDenied, "organization"}, @@ -450,8 +452,8 @@ func RunSolutionPrincipalTarget(t *testing.T, conn grpc.ClientConnInterface, des "by the installation's agent": {byAgent, f.StoreCredential, f.Existing, f.Organization, U(t), codes.PermissionDenied, "a person's act"}, "asked by a module that does not serve the kind": {administers, f.A.Credential, f.Existing, f.Organization, U(t), codes.PermissionDenied, "does not serve"}, "with a capability addressed elsewhere": {atRoot, f.StoreCredential, f.Existing, f.Organization, U(t), codes.Unauthenticated, "audience"}, - "of the wildcard id": {administers, f.StoreCredential, "*", f.Organization, U(t), codes.PermissionDenied, "not a usable id"}, - "of an oversize id": {administers, f.StoreCredential, strings.Repeat("x", 129), f.Organization, U(t), codes.PermissionDenied, "not a usable id"}, + "of the wildcard id": {administers, f.StoreCredential, "*", f.Organization, U(t), codes.InvalidArgument, "not a usable id"}, + "of an oversize id": {administers, f.StoreCredential, strings.Repeat("x", 129), f.Organization, U(t), codes.InvalidArgument, "not a usable id"}, } { _, err := sp.transfer(t, r.capability, r.actor, r.id, r.from, r.to) t.Run(name, func(t *testing.T) { refused(t, err, r.code, r.reason) }) @@ -712,10 +714,12 @@ func (sp *principalSuite) uninstall(t *testing.T, org string, s SolutionUnderTes if err := sp.f.Uninstall(sp.ctx, str(sp.ensure(t, org, s), "id")); err != nil { t.Fatal(err) } - delete(sp.installed, org+"/"+s.Solution) + // The checks name the uninstalled installation; only then is it + // forgotten and installed again. for _, check := range whileDisabled { check() } + delete(sp.installed, org+"/"+s.Solution) sp.ensure(t, org, s) } @@ -921,14 +925,9 @@ func (c *client) hasElement(name, element string) bool { return false } -// installationPrincipal is the principal an installation acts as for itself: -// principal_id under the target contract, agent_principal_id before it. Run -// reads it so one host passes both Run and the target. +// installationPrincipal is the principal an installation acts as for itself. func installationPrincipal(installation protoreflect.Message) string { - if id := str(installation, "principal_id"); id != "" { - return id - } - return str(installation, "agent_principal_id") + return str(installation, "principal_id") } // exactOwnedScope requires the effective scope to be exactly kind and actions @@ -962,29 +961,14 @@ func listStrings(m protoreflect.Message, name string) []string { return out } -// solutionPrincipal reports whether the published contract is the target's. -// Run's cases that the target supersedes then assert the target's model, so -// one host passes both Run and RunSolutionPrincipalTarget. -func (c *client) solutionPrincipal() bool { - return c.hasElement("host.installation.v1.Installation", "principal_id") -} - // noRole is the reason a person holding no role on a solution is refused. -func (c *client) noRole() string { - if c.solutionPrincipal() { - return "holds no role" - } - return "holds nothing" -} +func (c *client) noRole() string { return "holds no role" } // installationOwner is the principal owning data reached through installation // id at f.Organization: its principal_id under the target contract, "" before // it (the earlier model's scopes name no owner). func (c *client) installationOwner(t *testing.T, ctx context.Context, f Fixture, match func(protoreflect.Message) bool) string { t.Helper() - if !c.solutionPrincipal() { - return "" - } admin := c.must(t, ctx, "host.session.v1.SessionService", "SignIn", map[string]any{"fixture_token": f.AdminToken}) list := c.must(t, as(ctx, admin), "host.installation.v1.InstallationService", "List", map[string]any{"organization_id": f.Organization}) for _, inst := range listMsgs(list, "installations") { @@ -1026,9 +1010,6 @@ func exactScopeOwnedBy(t *testing.T, wc protoreflect.Message, kind string, actio // the solution-principal contract (a solution's principal), "" before it, at a // module audience, or when the person holds nothing there. func (c *client) heldOwner(ctx context.Context, sess protoreflect.Message, audience, kind string) string { - if !c.solutionPrincipal() { - return "" - } out, err := c.call(as(ctx, sess), "host.authority.v1.AuthorityService", "Issue", map[string]any{"person": map[string]any{}, "audience": audience}) if err != nil { return "" diff --git a/libraries/conformance/go/solution_roles.go b/libraries/conformance/go/solution_roles.go index 08c8ba27..2515045c 100644 --- a/libraries/conformance/go/solution_roles.go +++ b/libraries/conformance/go/solution_roles.go @@ -65,9 +65,7 @@ func (c *client) solutionRoleCases(t *testing.T, ctx context.Context, f Fixture) map[string]any{"method": "GET /open/{path=**}", "any_checked_caller": true}, }, } - if c.solutionPrincipal() { - doc["owner_aware"] = true - } + doc["owner_aware"] = true if _, err := c.deliver(ctx, f, r.Solution, "solution", r.Generation, []string{r.OwnKind}, doc, 0, bindings...); err != nil { t.Fatalf("deliver solution roles and ceiling: %v", err) } diff --git a/libraries/hop/go/go.mod b/libraries/hop/go/go.mod index 7ff6aec2..d93cc6ce 100644 --- a/libraries/hop/go/go.mod +++ b/libraries/hop/go/go.mod @@ -4,7 +4,7 @@ go 1.27.3 require ( github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261026032520-f688c5493b4d - github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435 + github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515 google.golang.org/grpc v1.84.0 google.golang.org/protobuf v1.36.12 ) diff --git a/libraries/hop/go/go.sum b/libraries/hop/go/go.sum index e9f8f31a..944ab148 100644 --- a/libraries/hop/go/go.sum +++ b/libraries/hop/go/go.sum @@ -30,8 +30,8 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261026032520-f688c5493b4d h1:qHuS/Er1SXh+I/RT8ytyxtJf2V57lhMftCcx6Wx9rco= github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261026032520-f688c5493b4d/go.mod h1:gMef/i8/IgUAeIh6TUeHI1J6160a55Z1XJrMnfjb2d8= -github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435 h1:3M1c4Ver3YRaHj4rk1+mj1uweqTF5bO2QHZ50rI7sNI= -github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= +github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515 h1:u6dYtxKTnEYeOwlgRx3sfJbxDl6UWI4uhpvUqOR4Ho0= +github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= diff --git a/merge-notes.txt b/merge-notes.txt new file mode 100644 index 00000000..7d52599c --- /dev/null +++ b/merge-notes.txt @@ -0,0 +1,72 @@ +PR #25 merge resolution notes + +Initial branch: feat/local-authority-on-core at 2c7839badac54f9ef95194ed03c8800c35b2a14d. +Initial main merged: bc93d1003088c5901455c375cac17bb0b6f5a355 (PR #16). + +Four conflicted files: + +1. libraries/authority-client/contract/catalog.codefly.json + Regenerated with codefly generate client from the merged local service proto. + Preserves main's 0.1.0 / LocalService publication and the branch's full + authority, composition, installation and owner-qualified work-context contract. + The resulting descriptor digest is + sha256:4d92544f5e186880be4d17079ad6dc056a6f9a84a0e1ebb298a2ff9b72863a46. + +2. libraries/authority-client/contract/contract.binpb + Regenerated, never edited by hand or selected wholesale from either side. + Descriptor comparison excluding source locations confirms that the sole + semantic difference from the branch is GatewayService -> LocalService. + AuthorizeTransfer, principal/role installation data and strict owner fields + remain intact. Go, Python and TypeScript regeneration is byte-identical to + the automatically merged generated client sources. + +3. libraries/authority-client/library.codefly.yaml + Used the Codefly-generated manifest for all three languages. Retains main's + version 0.1.0 and LocalService name, records the combined descriptor digest, + and truthfully records the development CLI and pinned proto companion 0.0.17. + +4. modules/authority/services/local/code/go.mod + Kept both direct dependencies: main's authority-client for its transport + integration and the branch's authority-core adapter. Retained the local + authority-core replacement and the newer Core b57347f09515 pin. + +Integration follow-ups: +- Verified local protobuf generation with codefly generate proto and regenerated + clients with codefly generate client, using the installed codefly-main binary. + The older PATH binary could not resolve Python's buf.validate dependency; + restored its incomplete output before successfully generating all languages. +- Refreshed 14 reviewed tools/noproxygate/generated.json digests after generation; + retained every gate rule and its generated-file provenance checks. +- Go dependency resolution aligned the gate's Core pin with the branch. +- Made the Python key-rotation test hold the mocked refresh until the held-key + assertion finishes; this removes a scheduling race without changing production + verification or extending trust in retired keys. + +Main advanced during validation to 3daa64124b9a4fe5bc0338898f90d83229d328d7 +(PR #22). Merged that commit too without additional conflicts, retaining its +module-wide proof that generated loopback credentials reach plugin self-dials +unchanged. Its RegisterREST forwarding changes coexist with the branch's +core-backed authority and delivery/consent security fixes. + +Final validation on the combined tree (including #16 and #22): +- GOWORK=off, GOPRIVATE=github.com/codefly-dev,github.com/obin-ai, + GOFLAGS=-mod=mod, an isolated GOCACHE, and the requested scratchpad GOBIN. +- go vet ./... and go test -race -count=1 ./... passed in all 11 Go modules. +- Core conformance passed explicitly via + TestCoreConformanceKitPassesThroughCheckShapedVerification. +- Shared authority conformance passed explicitly via + TestTheLocalAuthorityPassesTheSuiteEveryImplementationMustPass. +- python -m pytest -q: 231 passed. +- npm test: 89 passed (includes TypeScript build). +- go mod tidy -diff checked all four service modules and the transport gate. + After #22 removed the plugins' direct host import, authority-client correctly + becomes indirect; go mod tidy also removes obsolete Core sums in the gate. +- All 67 reviewed generated digests match; all tracked go.sum and lock files + pass the x-access-token marker check; no forbidden local files are staged. +- The branch's issuer, composition, installation and security regression tests + are unchanged by this merge. No interface declaration was changed. + +Detailed local logs: ../authority-merge-validation/, +../authority-merge-pytest.log, and ../authority-merge-npm.log. +These are local checks against the local authority and Core verifier, not proof +of Accounts adoption/parity; that existing obligation remains with the host owner. diff --git a/modules/authority/module.codefly.yaml b/modules/authority/module.codefly.yaml index 6b7eb64c..75c9e1b0 100644 --- a/modules/authority/module.codefly.yaml +++ b/modules/authority/module.codefly.yaml @@ -8,9 +8,9 @@ interface: - service: local endpoint: grpc implements: - - codefly.dev/authority@0.2.0 - - codefly.dev/composition@0.1.0 - - codefly.dev/installation@0.1.0 + - codefly.dev/authority@0.3.0 + - codefly.dev/composition@0.2.0 + - codefly.dev/installation@0.2.0 - codefly.dev/session@0.2.0 # The REST face publishes the authority's keys as a JSON Web Key Set # (/.well-known/jwks.json), for a module that reads them by URL. diff --git a/modules/authority/services/edge/code/go.mod b/modules/authority/services/edge/code/go.mod index 696e0aa7..97c43163 100644 --- a/modules/authority/services/edge/code/go.mod +++ b/modules/authority/services/edge/code/go.mod @@ -4,7 +4,7 @@ go 1.27.3 require ( github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261024163421-423f79d08aa1 - github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435 + github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515 github.com/codefly-dev/sdk-go v0.4.0 google.golang.org/grpc v1.84.0 ) diff --git a/modules/authority/services/edge/code/go.sum b/modules/authority/services/edge/code/go.sum index 5e631158..4b511f53 100644 --- a/modules/authority/services/edge/code/go.sum +++ b/modules/authority/services/edge/code/go.sum @@ -28,8 +28,8 @@ github.com/cheggaaa/pb/v3 v3.2.1 h1:aprZbFRG+B7+ug76S8QZ6Y1PW168UHzOmbC3wa+aU6I= github.com/cheggaaa/pb/v3 v3.2.1/go.mod h1:U9hSVxoKqJrZIE3PkFG1xXXNaK/Ilzg+EF/scpXzEdw= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435 h1:3M1c4Ver3YRaHj4rk1+mj1uweqTF5bO2QHZ50rI7sNI= -github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= +github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515 h1:u6dYtxKTnEYeOwlgRx3sfJbxDl6UWI4uhpvUqOR4Ho0= +github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= github.com/codefly-dev/sdk-go v0.4.0 h1:3Jm0bP+UsLI4iZYp3wqVa42bcXBumMIIF/aeweKOAhY= diff --git a/modules/authority/services/edge/service.codefly.yaml b/modules/authority/services/edge/service.codefly.yaml index 617d07b4..1e700caf 100644 --- a/modules/authority/services/edge/service.codefly.yaml +++ b/modules/authority/services/edge/service.codefly.yaml @@ -15,9 +15,9 @@ agent: # saas accounts implements the same interfaces — and the local authority that # implements all three when it names none (code/host.go). service-dependencies: - - interface: codefly.dev/authority@0.2.0 + - interface: codefly.dev/authority@>=0.2.0 <0.4.0 - interface: codefly.dev/session@0.2.0 - - interface: codefly.dev/installation@0.1.0 + - interface: codefly.dev/installation@>=0.1.0 <0.3.0 workspace-configuration-dependencies: # The modules the edge routes to, their REST and Connect faces, and the # frontend every other path serves (code/main.go). diff --git a/modules/authority/services/hop/code/go.mod b/modules/authority/services/hop/code/go.mod index f74eae1b..86c8cf29 100644 --- a/modules/authority/services/hop/code/go.mod +++ b/modules/authority/services/hop/code/go.mod @@ -8,7 +8,7 @@ require ( connectrpc.com/vanguard v0.4.0 github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0 github.com/codefly-dev/authority/libraries/hop/go v0.0.0 - github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435 + github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515 github.com/codefly-dev/sdk-go v0.4.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 github.com/rs/cors v1.11.1 diff --git a/modules/authority/services/hop/code/go.sum b/modules/authority/services/hop/code/go.sum index 45ae2276..1c032116 100644 --- a/modules/authority/services/hop/code/go.sum +++ b/modules/authority/services/hop/code/go.sum @@ -36,8 +36,8 @@ github.com/cheggaaa/pb/v3 v3.2.1 h1:aprZbFRG+B7+ug76S8QZ6Y1PW168UHzOmbC3wa+aU6I= github.com/cheggaaa/pb/v3 v3.2.1/go.mod h1:U9hSVxoKqJrZIE3PkFG1xXXNaK/Ilzg+EF/scpXzEdw= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435 h1:3M1c4Ver3YRaHj4rk1+mj1uweqTF5bO2QHZ50rI7sNI= -github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= +github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515 h1:u6dYtxKTnEYeOwlgRx3sfJbxDl6UWI4uhpvUqOR4Ho0= +github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= github.com/codefly-dev/sdk-go v0.4.0 h1:3Jm0bP+UsLI4iZYp3wqVa42bcXBumMIIF/aeweKOAhY= diff --git a/modules/authority/services/hop/service.codefly.yaml b/modules/authority/services/hop/service.codefly.yaml index 75ccf5a2..a7015317 100644 --- a/modules/authority/services/hop/service.codefly.yaml +++ b/modules/authority/services/hop/service.codefly.yaml @@ -35,7 +35,7 @@ spec: # the composition names in the authority group's endpoint answers (saas # accounts, say), and the local authority when it names none (code/host.go). service-dependencies: - - interface: codefly.dev/authority@0.2.0 + - interface: codefly.dev/authority@>=0.2.0 <0.4.0 # The hop group: the run's proof, which the hop writes on every stamp so the # module behind it believes only stamps from this run's hop, and the module the # composition puts the hop in front of (MODULES). hop-routes is generated diff --git a/modules/authority/services/local/README.md b/modules/authority/services/local/README.md index 21f5fa9e..5f97649a 100644 --- a/modules/authority/services/local/README.md +++ b/modules/authority/services/local/README.md @@ -6,11 +6,37 @@ composition from the manifests the run delivers, and answers the installation and artifact interfaces. The repository's [README](../../../../README.md) is the story and the rules; this page is what the process keeps and where. +**A host over the core.** Every decision and every signature is the generic +authority core's (`libraries/authority-core/go`), over its in-memory store +(`memstore`): modules, installations, role grants, delegations, revisions and +issuance. This service keeps what a host owns: the fixture and its sign-in +tokens (`pkg/business/state` seeds the core's store from them), sessions, +workload attestation against the launcher's anchor, signed delivery +admission, method admission on `Check`, and approvals. The plugins translate +the wire to the core's calls (`pkg/business/issuer`, `plugins/installation`). + +Request-facing installation methods require a person value and current stored +authority. Trusted fixture setup and teardown use the separate `InstallAsHost` +and `UninstallAsHost` methods; a missing or unauthenticated person never selects +host power. Module installations remain composition-owned after withdrawal. +Role calls answer as foreign when the caller is neither administrator nor +installer, including for an installation in their own organization. + +Child issuance preserves the verified parent's workspace/project identifiers +and each prior actor's agent/organization identifiers while deriving the new +target's grants. These fields remain absent when the parent omitted them; +replaying an actor does not substitute the task's organization for that actor's. +The shared core's `TestChildPreservesVerifiedTaskAndActorProvenance` exercises +populated parents through its production minter, child issuance and Check over +two exchanges, including action/resource limits. The ordinary Issue request +does not originate these optional fields. This test does not establish a live +populated-parent fixture or cross-host conformance coverage. + | State | Where | Across a restart | |---|---|---| | who exists, what they hold, what is installed by fixture | `code/fixtures/.yaml`, selected by the run | re-read: fixture, not state | | the composition: each module's manifest, generation and bindings | memory, re-delivered by the run | re-delivered | -| sessions, delegations, installations made at runtime | memory | forgotten | +| sessions, delegations, installations and role grants made at runtime | memory (the core's `memstore`) | forgotten | | **approvals**: a person's consent to one exact artifact for one installation | **the module's [store](../store/README.md)**, `approvals` table, when the run composes it; memory otherwise | **kept**: a run of the approved artifact is answered after a restart, and a revoked one stays refused | **How it chooses.** `code/work.go` (`approvalsFor`) reads the store's @@ -54,6 +80,8 @@ roles: # grants added to roles the fixture defines scopes: - {kind: ledger.entry, actions: [read, write]} installations: # installations of a solution the run composes, the fixture's own path + # role: the installation's own (its agent's) role; owner: who installed it, + # granted the installer role; teams: each granted role on the installation. - {organization: acme, target: atlas, role: viewer, owner: alex, teams: [finance]} modules: # a module the run supervises itself, until its own delivery - module: oracle @@ -129,9 +157,10 @@ coordinate against which it is checked. An identical replay returns the original receipt without advancing revisions or reapplying grants. Changing, adding or removing the authority policy at the same -generation is refused. Once an alias is delivered as a signed carrier, the -transitional unsigned sketch cannot overwrite or resurrect it. Sketch-only -composition retains its existing strictly newer generation rule. +generation is refused. Every public composition call requires a signed carrier +and the authenticated delivery workload; unsigned manifests are refused before +any host operation, including pending fixture installations. Trusted fixture +setup uses the host-only state API and cannot overwrite a signed alias. A workload must still be declared by the current signed manifest to obtain new authority. Withdrawal removes the module and its principal, revokes its @@ -171,3 +200,16 @@ explicitly enabled only by the local runner. HTTP Check uses the concrete verb and canonical path, with literal precedence and ambiguity refusal over the delivered manifest. Published Connect calls use the protobuf procedure spelling; a dotted HTTP URL cannot borrow gRPC policy. + +## Artifact consent admission + +Approve, Authorize and Revoke check effective scope coverage against the target +installation's stored principal before accessing approvals. An owner-selected +capability cannot access another installation's consent. Module installations +without a solution principal require owner-unrestricted coverage. Artifact identities +must name that installation's host-defined namespace as their source. + +The policy resource id is not established by the opaque identity subject or an +approval id. Resource-limited scopes are therefore refused with that reason; +consent requires the policy action over the installation owner's whole kind. +The shared scope algebra enforces both owner and resource restrictions. diff --git a/modules/authority/services/local/code/composition_test.go b/modules/authority/services/local/code/composition_test.go index f89ecd9c..3329022f 100644 --- a/modules/authority/services/local/code/composition_test.go +++ b/modules/authority/services/local/code/composition_test.go @@ -58,7 +58,7 @@ func TestCompositionReachesBootThroughCodefly(t *testing.T) { } } k := testkit.New(t) - ledger := state.Manifest{Module: "ledger", Principal: "ledger", Kind: "module", Generation: 1, ResourceKinds: []*basev0.WorkScopeV1{testkit.Scope("ledger.entry", "write")}} + ledger := state.Manifest{Name: "ledger", Principal: "ledger", Kind: "module", Generation: 1, ResourceKinds: []*basev0.WorkScopeV1{testkit.Scope("ledger.entry", "write")}} if _, err := k.State.ApplyManifest(ledger); err != nil { t.Fatal(err) } diff --git a/modules/authority/services/local/code/go.mod b/modules/authority/services/local/code/go.mod index 4a4f5982..acb0109f 100644 --- a/modules/authority/services/local/code/go.mod +++ b/modules/authority/services/local/code/go.mod @@ -7,12 +7,12 @@ require ( buf.build/go/protovalidate v1.4.0 connectrpc.com/connect v1.21.0 connectrpc.com/vanguard v0.4.0 - github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261026032520-f688c5493b4d + github.com/codefly-dev/authority/libraries/authority-core/go v0.0.0 github.com/codefly-dev/authority/libraries/conformance/go v0.0.0 github.com/codefly-dev/authority/libraries/delivery/go v0.0.0 github.com/codefly-dev/authority/libraries/hop/go v0.0.0 github.com/codefly-dev/authority/libraries/resources/go v0.0.0 - github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435 + github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515 github.com/codefly-dev/sdk-go v0.4.0 github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 @@ -31,6 +31,7 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261026032520-f688c5493b4d // indirect github.com/go-openapi/analysis v1.0.0 // indirect github.com/go-openapi/errors v0.22.9 // indirect github.com/go-openapi/jsonpointer v1.0.1 // indirect @@ -77,3 +78,5 @@ replace github.com/codefly-dev/authority/libraries/delivery/go => ../../../../.. replace github.com/codefly-dev/authority/libraries/resources/go => ../../../../../libraries/resources/go replace github.com/codefly-dev/authority/libraries/hop/go => ../../../../../libraries/hop/go + +replace github.com/codefly-dev/authority/libraries/authority-core/go => ../../../../../libraries/authority-core/go diff --git a/modules/authority/services/local/code/go.sum b/modules/authority/services/local/code/go.sum index 47b4a8b3..c3b89701 100644 --- a/modules/authority/services/local/code/go.sum +++ b/modules/authority/services/local/code/go.sum @@ -38,8 +38,8 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261026032520-f688c5493b4d h1:qHuS/Er1SXh+I/RT8ytyxtJf2V57lhMftCcx6Wx9rco= github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261026032520-f688c5493b4d/go.mod h1:gMef/i8/IgUAeIh6TUeHI1J6160a55Z1XJrMnfjb2d8= -github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435 h1:3M1c4Ver3YRaHj4rk1+mj1uweqTF5bO2QHZ50rI7sNI= -github.com/codefly-dev/core v0.19.7-0.20261026022918-8af4b787a435/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= +github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515 h1:u6dYtxKTnEYeOwlgRx3sfJbxDl6UWI4uhpvUqOR4Ho0= +github.com/codefly-dev/core v0.19.7-0.20261026034911-b57347f09515/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= github.com/codefly-dev/sdk-go v0.4.0 h1:3Jm0bP+UsLI4iZYp3wqVa42bcXBumMIIF/aeweKOAhY= diff --git a/modules/authority/services/local/code/pkg/business/issuer/artifact.go b/modules/authority/services/local/code/pkg/business/issuer/artifact.go index 7b225873..9b299c05 100644 --- a/modules/authority/services/local/code/pkg/business/issuer/artifact.go +++ b/modules/authority/services/local/code/pkg/business/issuer/artifact.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "fmt" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" "github.com/codefly-dev/core/workcontext" "google.golang.org/protobuf/proto" @@ -31,6 +32,9 @@ func (i *Issuer) Artifact(ctx context.Context, action string, req *artifactv1.Ar if err != nil { return nil, err } + if req.GetIdentity().GetSource() != state.Namespace(inst) { + return nil, fmt.Errorf("%w: artifact source %q is not installation %s's namespace %q", ErrRefused, req.GetIdentity().GetSource(), inst.ID, state.Namespace(inst)) + } digest, err := identityDigest(req.GetIdentity()) if err != nil { return nil, err @@ -62,7 +66,7 @@ func (i *Issuer) RevokeArtifact(ctx context.Context, req *artifactv1.RevokeReque // artifactCaller checks who asks and what they hold: the parent is addressed to // the module proving itself, its owner's organisation is the installation's, -// and it holds the policy kind's action at the module. Consent (activate) is +// and it holds the policy action for that installation's owner. Consent (activate) is // the person's own: a human owner, with no module acting between. func (i *Issuer) artifactCaller(ctx context.Context, parent, credential, installation, policy, action string) (state.Installation, error) { if policy == "" { @@ -72,7 +76,7 @@ func (i *Issuer) artifactCaller(ctx context.Context, parent, credential, install if err != nil { return state.Installation{}, err } - verified, err := i.verifier(decoded.Context().GetAudience()).Verify(ctx, parent) + verified, err := i.svc.Verify(ctx, parent, decoded.Context().GetAudience()) if err != nil { return state.Installation{}, err } @@ -82,7 +86,7 @@ func (i *Issuer) artifactCaller(ctx context.Context, parent, credential, install return state.Installation{}, fmt.Errorf("an artifact request names the module asking: %w", err) } if claims.GetAudience() != actor.Principal { - return state.Installation{}, fmt.Errorf("%w: %s asks about a context addressed to %s", ErrRefused, actor.Module, claims.GetAudience()) + return state.Installation{}, fmt.Errorf("%w: %s asks about a context addressed to %s", ErrRefused, actor.Name, claims.GetAudience()) } inst, ok := i.state.Installation(installation) if !ok || inst.Status != state.Active || inst.Organization != claims.GetTenantId() { @@ -91,8 +95,15 @@ func (i *Issuer) artifactCaller(ctx context.Context, parent, credential, install if action == "activate" && (claims.GetOwnerPrincipalKind() != "human" || verified.Actor() != nil) { return state.Installation{}, fmt.Errorf("%w: consent to an artifact is given by a person present, not through a module acting for them", ErrRefused) } - if !state.HoldsAction(verified.EffectiveScopes(), policy, action) { - return state.Installation{}, fmt.Errorf("%w: %s does not hold %s:%s at %s", ErrRefused, claims.GetOwnerPrincipalId(), policy, action, actor.Module) + // The installation record establishes the owner of its consent records. + // Identity.Subject is opaque and Revoke carries only an approval id: neither + // establishes a policy resource id. Require coverage of the owner's whole + // policy kind, refusing resource-limited scopes rather than dropping ids. + // Module installations have no solution principal. An empty owner in needed + // requires owner-unrestricted coverage; it never admits an owned scope. + needed := []*basev0.WorkScopeV1{{ResourceKind: policy, Actions: []string{action}, ResourceOwnerId: inst.PrincipalID}} + if !state.Attenuates(needed, verified.EffectiveScopes()) { + return state.Installation{}, fmt.Errorf("%w: %s does not hold %s:%s for installation owner %q without resource-id restrictions; artifact identity does not establish a policy resource id", ErrRefused, claims.GetOwnerPrincipalId(), policy, action, inst.PrincipalID) } return inst, nil } diff --git a/modules/authority/services/local/code/pkg/business/issuer/artifact_test.go b/modules/authority/services/local/code/pkg/business/issuer/artifact_test.go index 25c0e43f..2b4a7534 100644 --- a/modules/authority/services/local/code/pkg/business/issuer/artifact_test.go +++ b/modules/authority/services/local/code/pkg/business/issuer/artifact_test.go @@ -22,10 +22,10 @@ import ( func composeRobin(t *testing.T, k *testkit.Kit) { t.Helper() for _, m := range []state.Manifest{ - {Module: "robin", Principal: "robin", Kind: "module", Generation: 1, + {Name: "robin", Principal: "robin", Kind: "module", Generation: 1, ResourceKinds: []*basev0.WorkScopeV1{testkit.Scope("robin.intents", "read", "run", "import", "configure", "activate"), testkit.Scope("robin.tasks", "execute", "read")}, Bindings: map[string]state.Binding{}}, - {Module: "assistant", Principal: "assistant", Kind: "module", Generation: 1, + {Name: "assistant", Principal: "assistant", Kind: "module", Generation: 1, ResourceKinds: []*basev0.WorkScopeV1{testkit.Scope("assistant.task", "run")}, Bindings: map[string]state.Binding{"robin": {ID: "robin", Audience: "robin", InvokeScopes: []*basev0.WorkScopeV1{testkit.Scope("robin.intents", "activate", "run")}}}}, @@ -153,7 +153,7 @@ func TestAnInstallationNamesItsNamespace(t *testing.T) { k := testkit.New(t) composeRobin(t, k) inst, ok := k.State.Installation(state.InstallationID("acme", "robin")) - if !ok || inst.Namespace() != "acme.robin" { - t.Fatalf("namespace = %q (%v)", inst.Namespace(), ok) + if !ok || state.Namespace(inst) != "acme.robin" { + t.Fatalf("namespace = %q (%v)", state.Namespace(inst), ok) } } diff --git a/modules/authority/services/local/code/pkg/business/issuer/child_a_test.go b/modules/authority/services/local/code/pkg/business/issuer/child_a_test.go index 8fb66872..fbd92f9d 100644 --- a/modules/authority/services/local/code/pkg/business/issuer/child_a_test.go +++ b/modules/authority/services/local/code/pkg/business/issuer/child_a_test.go @@ -198,31 +198,3 @@ func TestPersonIssueAskPersistsForTheSameKind(t *testing.T) { t.Fatalf("a per-issue narrowing was recovered: %v", effectiveScopes(out.Context)) } } - -func TestInstallationChildProjectsTheTargetRoleAndBinding(t *testing.T) { - k, _ := solutionRoleKit(t, testkit.Scope("documents", "read")) - actor := &authorityv1.WorkloadCredential{Credential: k.Credential(t, "wiki")} - root, err := k.Issuer.Issue(context.Background(), &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: state.InstallationID("acme", "wiki")}}, - Actor: actor, Audience: "wiki", - }) - if err != nil { - t.Fatal(err) - } - req := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_ParentToken{ParentToken: root.Token}, - Actor: actor, Audience: "documents", OperationBindingId: "documents"} - out, err := k.Issuer.Issue(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if len(effectiveScopes(out.Context)) != 1 || actions(out.Context, "documents") != "read" { - t.Fatalf("installation child retained unrelated role scopes: %v", effectiveScopes(out.Context)) - } - if _, err := k.Issuer.Check(context.Background(), out.Token, "documents", ""); err != nil { - t.Fatal(err) - } - req.Scopes = scopes(testkit.Scope("documents", "write")) - if _, err := k.Issuer.Issue(context.Background(), req); !errors.Is(err, issuer.ErrRefused) { - t.Fatalf("installation role widened by target binding: %v", err) - } -} diff --git a/modules/authority/services/local/code/pkg/business/issuer/child_binding_preflight_test.go b/modules/authority/services/local/code/pkg/business/issuer/child_binding_preflight_test.go deleted file mode 100644 index fe7c4da7..00000000 --- a/modules/authority/services/local/code/pkg/business/issuer/child_binding_preflight_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package issuer_test - -import ( - "crypto/ed25519" - "crypto/rand" - "testing" - "time" - - "gateway/pkg/business/issuer" - "gateway/pkg/business/state" - "gateway/pkg/business/testkit" - authorityv1 "gateway/pkg/gen/host/authority/v1" - base "github.com/codefly-dev/core/generated/go/codefly/base/v0" - "github.com/codefly-dev/core/workcontext" - "github.com/google/uuid" -) - -func TestInvalidChildBindingDoesNotSpendSingleUseParent(t *testing.T) { - for _, binding := range []string{"", "undeclared", "records"} { - t.Run(binding, func(t *testing.T) { - k := testkit.New(t) - _, key, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - t.Fatal(err) - } - now := time.Now // the replay store expires entries against wall-clock time - k.Issuer = issuer.New(k.State, testkit.Audience, "one-use", key, now) - authority := &workcontext.Authority{Issuer: testkit.Audience, KeyID: "one-use", Key: key, Revisions: k.State, Seals: k.State, MaxTTL: time.Minute, Now: now} - parent, _, err := authority.Start(t.Context(), workcontext.StartInput{ - TenantID: "acme", OrganizationID: "acme", OwnerPrincipalID: "remi", OwnerPrincipalKind: "human", - TaskID: uuid.NewString(), Audience: "org-directory", AuthorityScopes: []*base.WorkScopeV1{testkit.Scope("directory.entry", "read")}, - InstallationID: state.InstallationID("acme", "org-directory"), ReplayPolicy: workcontext.ReplaySingleUse, TTL: time.Minute, - }) - if err != nil { - t.Fatal(err) - } - _, err = k.Issuer.Issue(t.Context(), &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_ParentToken{ParentToken: parent}, - Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "org-directory")}, Audience: "documents", OperationBindingId: binding}) - if err == nil { - t.Fatal("invalid binding was accepted") - } - if _, err := k.Issuer.Check(t.Context(), parent, "org-directory", ""); err != nil { - t.Fatalf("refused hop spent its parent: %v", err) - } - if _, err := k.Issuer.Check(t.Context(), parent, "org-directory", ""); err == nil { - t.Fatal("fixture is not single use") - } - }) - } -} diff --git a/modules/authority/services/local/code/pkg/business/issuer/child_delegation.go b/modules/authority/services/local/code/pkg/business/issuer/child_delegation.go deleted file mode 100644 index bcfd3190..00000000 --- a/modules/authority/services/local/code/pkg/business/issuer/child_delegation.go +++ /dev/null @@ -1,35 +0,0 @@ -package issuer - -import ( - "errors" - "fmt" - "gateway/pkg/business/state" - - base "github.com/codefly-dev/core/generated/go/codefly/base/v0" -) - -// delegationCeiling reads durable restrictions from verified provenance. -// Ordinary child hops name parent sessions, not stored Delegate records. -// Revoked records are retained; the final parent recheck fences concurrent -// revocation, so reconstruction cannot refresh an invalidated grant. -func (i *Issuer) delegationCeiling(parent *base.WorkContextV1, ceiling []*base.WorkScopeV1) ([]*base.WorkScopeV1, error) { - for _, hop := range parent.GetActorChain() { - d, err := i.state.DelegationByID(hop.GetDelegationId()) - if errors.Is(err, state.ErrUnknown) { - continue - } - if err != nil { - return nil, err - } - module, err := i.state.ModuleByPrincipal(hop.GetPrincipalId()) - if err != nil { - return nil, err - } - if d.Revoked || d.Grantor != parent.GetOwnerPrincipalId() || - d.Organization != parent.GetOrganizationId() || d.Module != module.Module { - return nil, fmt.Errorf("%w: the explicit delegation does not authorize this actor chain", ErrRefused) - } - ceiling = state.Intersect(ceiling, d.WorkScopes()) - } - return ceiling, nil -} diff --git a/modules/authority/services/local/code/pkg/business/issuer/delegation_revocation_test.go b/modules/authority/services/local/code/pkg/business/issuer/delegation_revocation_test.go deleted file mode 100644 index f4d2edbd..00000000 --- a/modules/authority/services/local/code/pkg/business/issuer/delegation_revocation_test.go +++ /dev/null @@ -1,195 +0,0 @@ -package issuer_test - -import ( - "context" - "crypto/ed25519" - "crypto/rand" - "errors" - "fmt" - "gateway/pkg/business/issuer" - "testing" - "time" - - "gateway/pkg/business/testkit" - authorityv1 "gateway/pkg/gen/host/authority/v1" - "github.com/codefly-dev/core/workcontext" -) - -func TestDelegationRevocationInvalidatesExistingUsesAndPreservesOtherTenants(t *testing.T) { - k := testkit.New(t) - declareDelegationChain(t, k) - ctx := context.Background() - grantor := person(t, k, "fixture-remi") - d, err := k.Issuer.Delegate(grantor, "org-directory", - append(testkit.OrgDirectory(1).Bindings["documents"].InvokeScopes, testkit.Documents(1).Bindings["model"].InvokeScopes...), "bounded work") - if err != nil { - t.Fatal(err) - } - delegated, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Delegation{Delegation: &authorityv1.DelegationRef{DelegationId: d.ID}}, - Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "org-directory")}, - Audience: "documents", OperationBindingId: "documents", - }) - if err != nil { - t.Fatal(err) - } - other, err := k.Issuer.Issue(person(t, k, "fixture-zoe"), &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents", - }) - if err != nil { - t.Fatal(err) - } - descendant, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_ParentToken{ParentToken: delegated.GetToken()}, - Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "documents")}, - Audience: "model", OperationBindingId: "model", - }) - if err != nil { - t.Fatal(err) - } - uses := map[string]func() error{ - "existing descendant": func() error { _, err := k.Issuer.Check(ctx, descendant.GetToken(), "model", ""); return err }, - "check": func() error { _, err := k.Issuer.Check(ctx, delegated.GetToken(), "documents", ""); return err }, - "resources": func() error { - _, err := k.Issuer.ListResources(ctx, delegated.GetToken(), "documents", "documents", "read") - return err - }, - "child": func() error { - _, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_ParentToken{ParentToken: delegated.GetToken()}, - Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "documents")}, - Audience: "model", OperationBindingId: "model", - }) - return err - }, - } - for name, use := range uses { - if err := use(); err != nil { - t.Fatalf("%s before revocation: %v", name, err) - } - } - if _, err := k.Issuer.RevokeDelegation(grantor, d.ID); err != nil { - t.Fatal(err) - } - for name, use := range uses { - if err := use(); !errors.Is(err, workcontext.ErrRevoked) { - t.Errorf("%s after revocation: %v", name, err) - } - } - if _, err := k.Issuer.Check(ctx, other.GetToken(), "documents", ""); err != nil { - t.Fatalf("another tenant lost authority: %v", err) - } - fresh, err := k.Issuer.Issue(grantor, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents", - }) - if err != nil { - t.Fatal(err) - } - if _, err := k.Issuer.RevokeDelegation(grantor, d.ID); err != nil { - t.Fatal(err) - } - if _, err := k.Issuer.Check(ctx, fresh.GetToken(), "documents", ""); err != nil { - t.Fatalf("repeating an already-complete revocation invalidated fresh authority: %v", err) - } -} - -// Exercise revocation at every clock boundary of issuance. The clock remains -// fixed; only the interleaving changes. A refusal or a token rejected by the -// ordinary verifier is safe, but refreshed authority after revocation is not. -func TestRevocationDuringIssuanceCannotRefreshAuthority(t *testing.T) { - for _, child := range []bool{false, true} { - name := "delegation" - if child { - name = "parent" - } - t.Run(name, func(t *testing.T) { - run := func(t *testing.T, revokeAt int) int { - k := testkit.New(t) - declareDelegationChain(t, k) - _, key, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - t.Fatal(err) - } - ticks, armed := 0, false - var delegationID string - grantor := person(t, k, "fixture-remi") - k.Issuer = issuer.New(k.State, testkit.Audience, "race-test", key, func() time.Time { - if armed { - ticks++ - if ticks == revokeAt { - if _, err := k.State.RevokeDelegation("remi", delegationID); err != nil { - t.Fatal(err) - } - } - } - return k.Now - }) - d, err := k.Issuer.Delegate(grantor, "org-directory", append(testkit.OrgDirectory(1).Bindings["documents"].InvokeScopes, testkit.Documents(1).Bindings["model"].InvokeScopes...), "race test") - if err != nil { - t.Fatal(err) - } - delegationID = d.ID - req := &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Delegation{Delegation: &authorityv1.DelegationRef{DelegationId: d.ID}}, - Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "org-directory")}, - Audience: "documents", OperationBindingId: "documents", - } - if child { - parent, err := k.Issuer.Issue(context.Background(), req) - if err != nil { - t.Fatal(err) - } - req = &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_ParentToken{ParentToken: parent.GetToken()}, - Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "documents")}, - Audience: "model", OperationBindingId: "model", - } - } - armed = true - out, issueErr := k.Issuer.Issue(context.Background(), req) - armed = false - if revokeAt == 0 { - if issueErr != nil { - t.Fatal(issueErr) - } - } else { - if ticks < revokeAt { - t.Fatalf("revocation point %d not reached (%d)", revokeAt, ticks) - } - if issueErr == nil { - if _, err := k.Issuer.Check(context.Background(), out.GetToken(), req.Audience, ""); !errors.Is(err, workcontext.ErrRevoked) { - t.Fatalf("issuance refreshed revoked authority at clock boundary %d: %v", revokeAt, err) - } - } - } - return ticks - } - count := run(t, 0) - for tick := 1; tick <= count; tick++ { - t.Run(fmt.Sprintf("clock-%d", tick), func(t *testing.T) { run(t, tick) }) - } - }) - } -} - -func TestFailedRevocationDoesNotInvalidateExistingAuthority(t *testing.T) { - k := testkit.New(t) - grantor := person(t, k, "fixture-remi") - d, err := k.Issuer.Delegate(grantor, "org-directory", testkit.OrgDirectory(1).Bindings["documents"].InvokeScopes, "failure isolation") - if err != nil { - t.Fatal(err) - } - before, err := k.State.AuthorizationRevision(context.Background(), d.Organization) - if err != nil { - t.Fatal(err) - } - for _, attempt := range []struct{ person, id string }{{"maya", d.ID}, {"remi", "unknown-delegation"}} { - if _, err := k.State.RevokeDelegation(attempt.person, attempt.id); err == nil { - t.Fatal("invalid revocation succeeded") - } - after, err := k.State.AuthorizationRevision(context.Background(), d.Organization) - if err != nil || after != before { - t.Fatalf("failed revocation changed revision: %d -> %d (%v)", before, after, err) - } - } -} diff --git a/modules/authority/services/local/code/pkg/business/issuer/delegation_test.go b/modules/authority/services/local/code/pkg/business/issuer/delegation_test.go index bf65393d..a9b32d29 100644 --- a/modules/authority/services/local/code/pkg/business/issuer/delegation_test.go +++ b/modules/authority/services/local/code/pkg/business/issuer/delegation_test.go @@ -7,7 +7,6 @@ import ( "testing" "gateway/pkg/business/issuer" - "gateway/pkg/business/state" "gateway/pkg/business/testkit" authorityv1 "gateway/pkg/gen/host/authority/v1" ) @@ -82,7 +81,7 @@ func TestADelegationIssuesAsItsGrantorUntilRevoked(t *testing.T) { if _, err := issue("documents"); !errors.Is(err, issuer.ErrRefused) { t.Fatalf("another module presents the delegation: %v", err) } - if _, err := k.Issuer.RevokeDelegation(person(t, k, "fixture-maya"), d.ID); !errors.Is(err, state.ErrForbidden) { + if _, err := k.Issuer.RevokeDelegation(person(t, k, "fixture-maya"), d.ID); !errors.Is(err, issuer.ErrRefused) { t.Fatalf("someone else revokes it: %v", err) } if _, err := k.Issuer.RevokeDelegation(remi, d.ID); err != nil { @@ -96,7 +95,7 @@ func TestADelegationIssuesAsItsGrantorUntilRevoked(t *testing.T) { func TestNobodyDelegatesWhatTheyDoNotHold(t *testing.T) { k := testkit.New(t) write := []*authorityv1.WorkScopeV1{{ResourceKind: "documents", Actions: []string{"write"}}} - if _, err := k.Issuer.Delegate(person(t, k, "fixture-maya"), "org-directory", issuer.FromWire(write), "x"); !errors.Is(err, state.ErrForbidden) { + if _, err := k.Issuer.Delegate(person(t, k, "fixture-maya"), "org-directory", issuer.FromWire(write), "x"); !errors.Is(err, issuer.ErrRefused) { t.Fatalf("a viewer delegates write: %v", err) } if _, err := k.Issuer.Delegate(context.Background(), "org-directory", issuer.FromWire(write), "x"); !errors.Is(err, issuer.ErrRefused) { diff --git a/modules/authority/services/local/code/pkg/business/issuer/installation_chain_test.go b/modules/authority/services/local/code/pkg/business/issuer/installation_chain_test.go deleted file mode 100644 index 92df88ef..00000000 --- a/modules/authority/services/local/code/pkg/business/issuer/installation_chain_test.go +++ /dev/null @@ -1,183 +0,0 @@ -package issuer_test - -import ( - "context" - "crypto/ed25519" - "crypto/rand" - "fmt" - "testing" - "time" - - "gateway/pkg/business/issuer" - "gateway/pkg/business/state" - "gateway/pkg/business/testkit" - authorityv1 "gateway/pkg/gen/host/authority/v1" -) - -func installationChainParent(t *testing.T, k *testkit.Kit) *authorityv1.IssueResponse { - t.Helper() - ctx := context.Background() - credential := &authorityv1.WorkloadCredential{Credential: k.Credential(t, "wiki")} - root, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: state.InstallationID("acme", "wiki")}}, - Actor: credential, Audience: "wiki", - }) - if err != nil { - t.Fatal(err) - } - parent, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_ParentToken{ParentToken: root.Token}, Actor: credential, - Audience: "org-directory", OperationBindingId: "directory", - }) - if err != nil { - t.Fatal(err) - } - return parent -} - -func installationChainRequest(t *testing.T, k *testkit.Kit, parent *authorityv1.IssueResponse) *authorityv1.IssueRequest { - t.Helper() - return &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_ParentToken{ParentToken: parent.Token}, - Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "org-directory")}, - Audience: "documents", OperationBindingId: "documents", - } -} - -func TestInstallationChainRetainsOwnerRoleResourcesAndProvenance(t *testing.T) { - held := testkit.Scope("documents", "read") - held.ResourceIds = []string{"one"} - k, _ := solutionRoleKit(t, held) - parent := installationChainParent(t, k) - req := installationChainRequest(t, k, parent) - ctx := context.Background() - out, err := k.Issuer.Issue(ctx, req) - if err != nil { - t.Fatal(err) - } - wc, before := out.Context, parent.Context - if wc.OwnerPrincipalId != state.InstallationID("acme", "wiki")+"/agent" || - wc.GetOwnerPrincipalKind() != "agent" || wc.GetOwnerAgentId() != "wiki" || - wc.TenantId != before.TenantId || wc.TaskId != before.TaskId || - wc.GetParentSessionId() != before.SessionId || wc.ExpiresAtUnix > before.ExpiresAtUnix || - len(wc.ActorChain) != 2 || wc.ActorChain[0].PrincipalId != "wiki" || wc.ActorChain[1].PrincipalId != "org-directory" || - wc.Seal.InstallationId != state.InstallationID("acme", "org-directory") || - wc.OperationBinding.BindingId != "acme/org-directory/documents" { - t.Fatalf("installation provenance changed: %v", wc) - } - if len(effectiveScopes(wc)) != 1 || actions(wc, "documents") != "read" || - len(effectiveScopes(wc)[0].ResourceIds) != 1 || effectiveScopes(wc)[0].ResourceIds[0] != "one" { - t.Fatalf("role projection changed: %v", effectiveScopes(wc)) - } - if _, err := k.Issuer.Check(ctx, out.Token, "documents", ""); err != nil { - t.Fatal(err) - } - grants, err := k.Issuer.ListResources(ctx, out.Token, "documents", "documents", "read") - if err != nil || !covered(t, grants).Matches("one") || covered(t, grants).Matches("two") || covered(t, grants).All() { - t.Fatalf("resource ceiling lost: %v %v", grants, err) - } - for _, ask := range []*authorityv1.WorkScopeV1{ - {ResourceKind: "documents", Actions: []string{"write"}, ResourceIds: []string{"one"}}, - {ResourceKind: "documents", Actions: []string{"read"}}, - {ResourceKind: "documents", Actions: []string{"read"}, ResourceIds: []string{"two"}}, - } { - req.Scopes = []*authorityv1.WorkScopeV1{ask} - if _, err := k.Issuer.Issue(ctx, req); err == nil { - t.Fatalf("role restriction widened: %v", ask) - } - } - req.Scopes = nil - req.Actor.Credential = k.Credential(t, "documents") - if _, err := k.Issuer.Issue(ctx, req); err == nil { - t.Fatal("wrong actor accepted") - } -} - -func TestInstallationChainRevocation(t *testing.T) { - for _, change := range []string{"source uninstall", "actor uninstall", "role narrowing", "binding narrowing"} { - t.Run(change, func(t *testing.T) { - k, solution := solutionRoleKit(t, testkit.Scope("documents", "read")) - parent := installationChainParent(t, k) - req := installationChainRequest(t, k, parent) - ctx := context.Background() - out, err := k.Issuer.Issue(ctx, req) - if err != nil { - t.Fatal(err) - } - switch change { - case "source uninstall": - err = k.State.Revoke(state.InstallationID("acme", "wiki")) - case "actor uninstall": - err = k.State.Revoke(state.InstallationID("acme", "org-directory")) - case "role narrowing": - solution.Generation++ - solution.Roles["viewer"] = append(testkit.Wiki(1).Roles["viewer"], testkit.Scope("directory.entry", "read")) - _, err = k.State.ApplyManifest(solution) - case "binding narrowing": - module := testkit.OrgDirectory(2) - delete(module.Bindings, "documents") - _, err = k.State.ApplyManifest(module) - } - if err != nil { - t.Fatal(err) - } - if _, err := k.Issuer.Check(ctx, out.Token, "documents", ""); err == nil { - t.Fatal("old child survived revocation") - } - if _, err := k.Issuer.Issue(ctx, req); err == nil { - t.Fatal("parent refreshed revoked authority") - } - if change == "role narrowing" || change == "binding narrowing" { - fresh := installationChainRequest(t, k, installationChainParent(t, k)) - if _, err := k.Issuer.Issue(ctx, fresh); err == nil { - t.Fatal("fresh chain recovered removed authority") - } - } - }) - } -} - -func TestInstallationUninstallDuringSecondHopCannotRefreshAuthority(t *testing.T) { - run := func(t *testing.T, revokeAt int) int { - k, _ := solutionRoleKit(t, testkit.Scope("documents", "read")) - _, key, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - t.Fatal(err) - } - ticks, armed := 0, false - k.Issuer = issuer.New(k.State, testkit.Audience, "installation-race", key, func() time.Time { - if armed { - ticks++ - if ticks == revokeAt { - if err := k.State.Revoke(state.InstallationID("acme", "wiki")); err != nil { - t.Fatal(err) - } - } - } - return k.Now - }) - req := installationChainRequest(t, k, installationChainParent(t, k)) - armed = true - out, issueErr := k.Issuer.Issue(context.Background(), req) - armed = false - if revokeAt == 0 { - if issueErr != nil { - t.Fatal(issueErr) - } - } else { - if ticks < revokeAt { - t.Fatalf("revocation point %d not reached (%d)", revokeAt, ticks) - } - if issueErr == nil { - if _, err := k.Issuer.Check(context.Background(), out.Token, "documents", ""); err == nil { - t.Fatalf("issuance refreshed revoked installation at clock %d", revokeAt) - } - } - } - return ticks - } - count := run(t, 0) - for tick := 1; tick <= count; tick++ { - t.Run(fmt.Sprintf("clock-%d", tick), func(t *testing.T) { run(t, tick) }) - } -} diff --git a/modules/authority/services/local/code/pkg/business/issuer/installation_roles_test.go b/modules/authority/services/local/code/pkg/business/issuer/installation_roles_test.go deleted file mode 100644 index 908ac342..00000000 --- a/modules/authority/services/local/code/pkg/business/issuer/installation_roles_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package issuer_test - -import ( - "context" - "errors" - "testing" - - "gateway/pkg/business/issuer" - "gateway/pkg/business/state" - "gateway/pkg/business/testkit" - authorityv1 "gateway/pkg/gen/host/authority/v1" - basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" - "github.com/codefly-dev/core/workcontext" -) - -func installationManifest(name, role string, generation uint64, actions ...string) state.Manifest { - kind := name + ".item" - return state.Manifest{Module: name, Principal: name, Kind: "solution", Generation: generation, - ResourceKinds: []*basev0.WorkScopeV1{testkit.Scope(kind, "read", "write")}, - Roles: map[string][]*basev0.WorkScopeV1{state.InstallerRole: {testkit.Scope(kind, "read", "write")}, role: {testkit.Scope(kind, actions...)}}, - Bindings: map[string]state.Binding{"self": {ID: "self", Audience: name, InvokeScopes: []*basev0.WorkScopeV1{testkit.Scope(kind, "read", "write")}}}} -} - -func TestInstallationRoleComesFromItsSolutionNotTheOrganization(t *testing.T) { - for _, role := range []string{"viewer", "task_operator"} { - t.Run(role, func(t *testing.T) { - k := testkit.New(t) - ctx := context.Background() - for _, target := range []struct{ name, action string }{{"conformance-solution", "read"}, {"assistant", "write"}} { - if _, err := k.State.ApplyManifest(installationManifest(target.name, role, 1, target.action)); err != nil { - t.Fatal(err) - } - inst, err := k.State.Install(ctx, "acme", target.name, role, "alex") - if err != nil { - t.Fatal(err) - } - request := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: inst.ID}}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, inst.Target)}, Audience: target.name} - out, err := k.Issuer.Issue(ctx, request) - if err != nil { - t.Fatalf("declared solution role cannot issue: %v", err) - } - held := out.GetContext().GetAuthorityScopes() - if len(held) != 1 || held[0].GetResourceKind() != target.name+".item" || len(held[0].GetActions()) != 1 || held[0].GetActions()[0] != target.action { - t.Fatalf("wrong role source: %v", held) - } - if _, err := k.Issuer.Check(ctx, out.GetToken(), target.name, ""); err != nil { - t.Fatal(err) - } - request.Audience = "documents" - if _, err := k.Issuer.Issue(ctx, request); !errors.Is(err, issuer.ErrRefused) { - t.Fatalf("organisation role supplied unrelated Documents authority: %v", err) - } - } - }) - } -} - -func TestSolutionRoleNarrowingRevokesExistingRootsAndChildren(t *testing.T) { - for _, remove := range []bool{false, true} { - name := "narrow" - if remove { - name = "remove" - } - t.Run(name, func(t *testing.T) { - k := testkit.New(t) - ctx := context.Background() - manifest := installationManifest("conformance-solution", "task_operator", 1, "read", "write") - if _, err := k.State.ApplyManifest(manifest); err != nil { - t.Fatal(err) - } - inst, err := k.State.Install(ctx, "acme", manifest.Module, "task_operator", "alex") - if err != nil { - t.Fatal(err) - } - request := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: inst.ID}}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, inst.Target)}, Audience: manifest.Principal, Scopes: scopes(testkit.Scope("conformance-solution.item", "write"))} - root, err := k.Issuer.Issue(ctx, request) - if err != nil { - t.Fatal(err) - } - child, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_ParentToken{ParentToken: root.GetToken()}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, manifest.Module)}, Audience: manifest.Principal, OperationBindingId: "self", Scopes: request.Scopes}) - if err != nil { - t.Fatal(err) - } - for _, token := range []string{root.GetToken(), child.GetToken()} { - if _, err := k.Issuer.Check(ctx, token, manifest.Principal, ""); err != nil { - t.Fatalf("before narrowing: %v", err) - } - } - other, err := k.Issuer.Issue(person(t, k, "fixture-zoe"), &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents"}) - if err != nil { - t.Fatal(err) - } - next := installationManifest("conformance-solution", "task_operator", 2, "read") - if remove { - delete(next.Roles, "task_operator") - } - if _, err := k.State.ApplyManifest(next); err != nil { - t.Fatal(err) - } - for _, token := range []string{root.GetToken(), child.GetToken()} { - if _, err := k.Issuer.Check(ctx, token, manifest.Principal, ""); !errors.Is(err, workcontext.ErrRevoked) { - t.Fatalf("old role capability after narrowing: %v", err) - } - } - if _, err := k.Issuer.Issue(ctx, request); err == nil { - t.Fatal("fresh issuance recovered removed write") - } - request.Scopes = scopes(testkit.Scope("conformance-solution.item", "read")) - if _, err := k.Issuer.Issue(ctx, request); (err == nil) == remove { - t.Fatalf("fresh read after role change remove=%v: %v", remove, err) - } - if _, err := k.Issuer.Check(ctx, other.GetToken(), "documents", ""); err != nil { - t.Fatalf("unrelated organization revoked: %v", err) - } - }) - } -} - -func TestInstallerNarrowingRevokesTheOwnerButRoleExpansionKeepsExistingAuthority(t *testing.T) { - k := testkit.New(t) - ctx := context.Background() - m := installationManifest("conformance-solution", "viewer", 1, "read") - if _, err := k.State.ApplyManifest(m); err != nil { - t.Fatal(err) - } - inst, err := k.State.Install(ctx, "acme", m.Module, "viewer", "alex") - if err != nil { - t.Fatal(err) - } - agent, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: inst.ID}}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, inst.Target)}, Audience: m.Principal}) - if err != nil { - t.Fatal(err) - } - owner, err := k.Issuer.Issue(person(t, k, "fixture-alex"), &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: m.Principal, Scopes: scopes(testkit.Scope(m.Module+".item", "write"))}) - if err != nil { - t.Fatal(err) - } - // Expanding the installed viewer role leaves already-issued narrow grants valid. - next := installationManifest(m.Module, "viewer", 2, "read", "write") - if _, err := k.State.ApplyManifest(next); err != nil { - t.Fatal(err) - } - if _, err := k.Issuer.Check(ctx, agent.GetToken(), m.Principal, ""); err != nil { - t.Fatalf("expansion invalidated old scope: %v", err) - } - // The owner's installer role is independent of that selected viewer role. - next = installationManifest(m.Module, "viewer", 3, "read", "write") - next.Roles[state.InstallerRole] = []*basev0.WorkScopeV1{testkit.Scope(m.Module+".item", "read")} - if _, err := k.State.ApplyManifest(next); err != nil { - t.Fatal(err) - } - if _, err := k.Issuer.Check(ctx, owner.GetToken(), m.Principal, ""); !errors.Is(err, workcontext.ErrRevoked) { - t.Fatalf("installer lost write but old token still works: %v", err) - } -} diff --git a/modules/authority/services/local/code/pkg/business/issuer/issuance_consistency_test.go b/modules/authority/services/local/code/pkg/business/issuer/issuance_consistency_test.go deleted file mode 100644 index a6bc6909..00000000 --- a/modules/authority/services/local/code/pkg/business/issuer/issuance_consistency_test.go +++ /dev/null @@ -1,293 +0,0 @@ -package issuer_test - -import ( - "context" - "crypto/rand" - "testing" - - "gateway/pkg/business/state" - "gateway/pkg/business/testkit" - authorityv1 "gateway/pkg/gen/host/authority/v1" - basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" - "github.com/google/uuid" -) - -// UUID creation is an existing injection point around Core's live version reads. -// These tests are deliberately serial: uuid's reader is process-wide. -type issuanceChangeReader struct { - remaining int - apply func() -} - -func (r *issuanceChangeReader) Read(p []byte) (int, error) { - r.remaining-- - if r.remaining == 0 { - r.apply() - } - return rand.Read(p) -} - -func interleaveIssuance(t *testing.T, stage string, apply func(), issue func() (*authorityv1.IssueResponse, error)) (*authorityv1.IssueResponse, error) { - t.Helper() - applied := false - change := func() { apply(); applied = true } - switch stage { - case "before_resolution": - change() - case "before_mint", "during_mint": - n := 1 - if stage == "during_mint" { - n = 2 - } - uuid.SetRand(&issuanceChangeReader{remaining: n, apply: change}) - defer uuid.SetRand(nil) - } - out, err := issue() - uuid.SetRand(nil) - if stage == "after_issue" { - if err != nil { - t.Fatalf("ordinary issuance: %v", err) - } - change() - } - if !applied { - t.Fatal("interleaving not reached") - } - return out, err -} - -func TestRoleNarrowingBetweenResolutionAndMint(t *testing.T) { - for _, owner := range []string{"installation", "installation_removed", "person", "delegation", "child"} { - for _, stage := range []string{"before_resolution", "before_mint", "during_mint", "after_issue"} { - t.Run(owner+"/"+stage, func(t *testing.T) { - k := testkit.New(t) - ctx := context.Background() - old := installationManifest("conformance-solution", "task_operator", 1, "read", "write") - if _, err := k.State.ApplyManifest(old); err != nil { - t.Fatal(err) - } - inst, err := k.State.Install(ctx, "acme", old.Module, "task_operator", "alex") - if err != nil { - t.Fatal(err) - } - req := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: inst.ID}}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, inst.Target)}, Audience: old.Principal, Scopes: scopes(testkit.Scope(old.Module+".item", "write"))} - next := installationManifest(old.Module, "task_operator", 2, "read") - if owner == "person" { - ctx = person(t, k, "fixture-alex") - req.As = &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}} - } - if owner == "person" || owner == "delegation" { - next.Roles[state.InstallerRole] = []*basev0.WorkScopeV1{testkit.Scope(old.Module+".item", "read")} - } - if owner == "installation_removed" { - delete(next.Roles, "task_operator") - } - if owner == "delegation" { - ctx = person(t, k, "fixture-alex") - d, err := k.Issuer.Delegate(ctx, old.Module, []*basev0.WorkScopeV1{testkit.Scope(old.Module+".item", "write")}, "consistency regression") - if err != nil { - t.Fatal(err) - } - req.As = &authorityv1.IssueRequest_Delegation{Delegation: &authorityv1.DelegationRef{DelegationId: d.ID}} - req.Actor = &authorityv1.WorkloadCredential{Credential: k.Credential(t, old.Module)} - req.OperationBindingId = "self" - } - if owner == "child" { - parent, err := k.Issuer.Issue(ctx, req) - if err != nil { - t.Fatal(err) - } - req.As = &authorityv1.IssueRequest_ParentToken{ParentToken: parent.Token} - req.Actor = &authorityv1.WorkloadCredential{Credential: k.Credential(t, old.Module)} - req.OperationBindingId = "self" - } - // Prove this owner can issue the exact requested scope before scheduling a change. - before, err := k.Issuer.Issue(ctx, req) - if err != nil { - t.Fatal(err) - } - if _, err := k.Issuer.Check(ctx, before.Token, old.Principal, ""); err != nil { - t.Fatal(err) - } - out, err := interleaveIssuance(t, stage, func() { - if _, err := k.State.ApplyManifest(next); err != nil { - t.Fatal(err) - } - }, func() (*authorityv1.IssueResponse, error) { return k.Issuer.Issue(ctx, req) }) - if err != nil { - t.Logf("safely refused: %v", err) - return - } - if _, err := k.Issuer.Check(ctx, out.Token, old.Principal, ""); err != nil { - t.Logf("safely rejected: %v", err) - return - } - t.Fatalf("stale write accepted: signed revision=%d installation revision=%d", out.Context.GetAuthorizationRevision(), out.Context.GetSeal().GetInstallationRevision()) - }) - } - } -} - -func TestBindingNarrowingBetweenResolutionAndMint(t *testing.T) { - for _, owner := range []string{"workload", "delegation", "child", "person"} { - for _, changeName := range []string{"narrow", "remove", "retarget", "resource"} { - for _, stage := range []string{"before_resolution", "before_mint", "during_mint", "after_issue"} { - t.Run(owner+"/"+changeName+"/"+stage, func(t *testing.T) { - k := testkit.New(t) - ctx := context.Background() - credential := k.Credential(t, "documents") - req := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Workload{Workload: &authorityv1.WorkloadCredential{Credential: credential}}, OrganizationId: "acme", Audience: "model", OperationBindingId: "model", Scopes: scopes(testkit.Scope("modelservice.profiles", "invoke"))} - if owner == "person" { - ctx = person(t, k, "fixture-dev") - req.As = &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}} - req.Audience = "documents" - } - if owner == "delegation" { - ctx = person(t, k, "fixture-dev") - d, err := k.Issuer.Delegate(ctx, "documents", []*basev0.WorkScopeV1{testkit.Scope("modelservice.profiles", "invoke")}, "binding consistency regression") - if err != nil { - t.Fatal(err) - } - req.As = &authorityv1.IssueRequest_Delegation{Delegation: &authorityv1.DelegationRef{DelegationId: d.ID}} - req.Actor = &authorityv1.WorkloadCredential{Credential: credential} - } - if owner == "child" { - ctx = person(t, k, "fixture-dev") - parent, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents", Scopes: scopes(testkit.Scope("documents", "read"))}) - if err != nil { - t.Fatal(err) - } - req.As = &authorityv1.IssueRequest_ParentToken{ParentToken: parent.Token} - req.Actor = &authorityv1.WorkloadCredential{Credential: credential} - } - before, err := k.Issuer.Issue(ctx, req) - if err != nil { - t.Fatal(err) - } - if _, err := k.Issuer.Check(ctx, before.Token, req.Audience, ""); err != nil { - t.Fatal(err) - } - next := testkit.Documents(2) - binding := next.Bindings["model"] - switch changeName { - case "narrow": - binding.HeadlessScopes = []*basev0.WorkScopeV1{testkit.Scope("modelservice.profiles", "read")} - binding.InvokeScopes = binding.HeadlessScopes - case "retarget": - binding.Audience = "runtime" - case "resource": - scoped := testkit.Scope("modelservice.profiles", "invoke", "read") - scoped.ResourceIds = []string{"profile-one"} - binding.HeadlessScopes = []*basev0.WorkScopeV1{scoped} - binding.InvokeScopes = binding.HeadlessScopes - } - next.Bindings["model"] = binding - if changeName == "remove" { - delete(next.Bindings, "model") - } - out, err := interleaveIssuance(t, stage, func() { - if _, err := k.State.ApplyManifest(next); err != nil { - t.Fatal(err) - } - }, func() (*authorityv1.IssueResponse, error) { return k.Issuer.Issue(ctx, req) }) - if err != nil { - t.Logf("safely refused: %v", err) - return - } - if _, err := k.Issuer.Check(ctx, out.Token, req.Audience, ""); err != nil { - t.Logf("safely rejected: %v", err) - return - } - t.Fatal("old invoke accepted after binding narrowing/removal") - }) - } - } - } -} - -func TestPersonBindingReadRemainsUsableAfterNarrowingAndExpansion(t *testing.T) { - k := testkit.New(t) - ctx := person(t, k, "fixture-dev") - req := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents", OperationBindingId: "model", Scopes: scopes(testkit.Scope("modelservice.profiles", "read"))} - next := testkit.Documents(2) - binding := next.Bindings["model"] - binding.InvokeScopes = []*basev0.WorkScopeV1{testkit.Scope("modelservice.profiles", "read")} - binding.HeadlessScopes = binding.InvokeScopes - next.Bindings["model"] = binding - if _, err := k.State.ApplyManifest(next); err != nil { - t.Fatal(err) - } - read, err := k.Issuer.Issue(ctx, req) - if err != nil { - t.Fatalf("fresh read after narrowing: %v", err) - } - if _, err := k.Issuer.Check(ctx, read.Token, req.Audience, ""); err != nil { - t.Fatal(err) - } - if _, err := k.State.ApplyManifest(testkit.Documents(3)); err != nil { - t.Fatal(err) - } - if _, err := k.Issuer.Check(ctx, read.Token, req.Audience, ""); err != nil { - t.Fatalf("unchanged person read revoked by expansion: %v", err) - } - req.Scopes = scopes(testkit.Scope("modelservice.profiles", "invoke")) - invoke, err := k.Issuer.Issue(ctx, req) - if err != nil { - t.Fatalf("fresh invoke after expansion: %v", err) - } - if _, err := k.Issuer.Check(ctx, invoke.Token, req.Audience, ""); err != nil { - t.Fatal(err) - } -} - -// Issue reads its audience before sessionOf calls Value; capture a delivery -// there to exercise the early audience snapshot independently of mint UUIDs. -type deliveryOnSession struct { - context.Context - apply func() -} - -func (c *deliveryOnSession) Value(key any) any { - value := c.Context.Value(key) - if value != nil && c.apply != nil { - apply := c.apply - c.apply = nil - apply() - } - return value -} -func TestBindingChangeBeforePersonRevisionSnapshot(t *testing.T) { - k := testkit.New(t) - ctx := person(t, k, "fixture-dev") - req := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents", OperationBindingId: "model", Scopes: scopes(testkit.Scope("modelservice.profiles", "invoke"))} - original, err := k.Issuer.Issue(ctx, req) - if err != nil { - t.Fatal(err) - } - if _, err := k.Issuer.Check(ctx, original.Token, "documents", ""); err != nil { - t.Fatal(err) - } - next := testkit.Documents(2) - b := next.Bindings["model"] - b.InvokeScopes = []*basev0.WorkScopeV1{testkit.Scope("modelservice.profiles", "read")} - b.HeadlessScopes = b.InvokeScopes - next.Bindings["model"] = b - interleaved := &deliveryOnSession{Context: ctx, apply: func() { - if _, err := k.State.ApplyManifest(next); err != nil { - t.Fatal(err) - } - }} - out, err := k.Issuer.Issue(interleaved, req) - if interleaved.apply != nil { - t.Fatal("early interleaving not reached") - } - if err != nil { - t.Logf("refused stale audience: %v", err) - return - } - if _, err := k.Issuer.Check(ctx, out.Token, "documents", ""); err != nil { - t.Logf("rejected stale audience: %v", err) - return - } - t.Fatalf("stale early audience accepted with authorization revision %d", out.Context.GetAuthorizationRevision()) -} diff --git a/modules/authority/services/local/code/pkg/business/issuer/issuer.go b/modules/authority/services/local/code/pkg/business/issuer/issuer.go index 9d14ae1b..0525d9b0 100644 --- a/modules/authority/services/local/code/pkg/business/issuer/issuer.go +++ b/modules/authority/services/local/code/pkg/business/issuer/issuer.go @@ -1,8 +1,9 @@ -// Package issuer is the authority interface on top of core's Work Context -// library: Issue for every owner shape, Check for every callee, Keys for every -// verifier. It mints with core's Authority and verifies with core's Verifier, -// so the laptop signs and refuses exactly as a cell does; what it adds is only -// the resolution of an owner shape into core's inputs against the state. +// Package issuer is the authority interface over the generic authority core: +// Issue for every owner shape, Check for every callee, Keys for every +// verifier. Every decision and every signature is the core's; what this +// package adds is the wire: a session bearer resolved to the person, a +// workload credential attested to its module, and a method's declared policy +// applied on Check. package issuer import ( @@ -10,14 +11,12 @@ import ( "crypto/ed25519" "errors" "fmt" - resources "github.com/codefly-dev/authority/libraries/resources/go" - "slices" - "strings" "time" + "github.com/codefly-dev/authority/libraries/authority-core/go/authority" + "github.com/codefly-dev/authority/libraries/authority-core/go/domain" basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" "github.com/codefly-dev/core/workcontext" - "github.com/google/uuid" "google.golang.org/protobuf/proto" "gateway/pkg/business/state" @@ -41,101 +40,69 @@ func sessionOf(ctx context.Context) (string, bool) { return id, ok && id != "" } -// ErrRefused is the umbrella for a refusal the caller can do nothing about. +// ErrRefused is the umbrella for a refusal the caller can do nothing about: +// this wire layer's own, and every refusal the core denies. var ErrRefused = errors.New("issuer: refused") -// Issuer signs and verifies. +// classify keeps the core's refusal (its code still answers domain.CodeOf) +// and names its family as this host's callers test it: a denial is +// ErrRefused, a missing thing state.ErrUnknown. +func classify(err error) error { + code, ok := domain.CodeOf(err) + switch { + case !ok: + return err + case code == domain.Denied: + return fmt.Errorf("%w: %w", ErrRefused, err) + case code == domain.NotFound: + return fmt.Errorf("%w: %w", state.ErrUnknown, err) + } + return err +} + +// Issuer is the wire over one host and its core. type Issuer struct { - state *state.State - authority *workcontext.Authority - replay workcontext.ReplayStore - public ed25519.PublicKey + state *state.State + svc *authority.Service + issuer string + keyID string + public ed25519.PublicKey } -// New builds an issuer for one host identity and one signing key. -func New(st *state.State, issuer, keyID string, key ed25519.PrivateKey, now func() time.Time) *Issuer { - return &Issuer{ - state: st, - public: key.Public().(ed25519.PublicKey), - replay: workcontext.NewMemoryReplayStore(), - authority: &workcontext.Authority{ - Issuer: issuer, KeyID: keyID, Key: key, Revisions: st, Seals: st, MaxTTL: MaxTTL, Now: now, - }, - } +// New builds the issuer for a host whose core signs as issuer with keyID, its +// public half published by Keys. +func New(st *state.State, issuer, keyID string, public ed25519.PublicKey) *Issuer { + return &Issuer{state: st, svc: st.Service(), issuer: issuer, keyID: keyID, public: public} +} + +// Config is the core configuration for one signing key: what state.New +// takes. The local authority is one process on a laptop, so its replay store +// is in memory, chosen here by name rather than defaulted. +func Config(issuer, keyID string, key ed25519.PrivateKey, now func() time.Time) authority.Config { + replay := workcontext.NewMemoryReplayStore() + replay.Now = now + return authority.Config{Issuer: issuer, KeyID: keyID, Signer: key, Clock: now, MaxTTL: MaxTTL, Replay: replay} } // Keys is what a verifier pins. func (i *Issuer) Keys() *authorityv1.KeysResponse { - return &authorityv1.KeysResponse{Issuer: i.authority.Issuer, Keys: []*authorityv1.PublicKey{{ - KeyId: i.authority.KeyID, Algorithm: workcontext.Algorithm, PublicKey: []byte(i.public), + return &authorityv1.KeysResponse{Issuer: i.issuer, Keys: []*authorityv1.PublicKey{{ + KeyId: i.keyID, Algorithm: workcontext.Algorithm, PublicKey: []byte(i.public), }}} } -func (i *Issuer) verifier(audience string) *workcontext.Verifier { - return &workcontext.Verifier{ - Issuer: i.authority.Issuer, Audience: audience, - Keys: map[string]ed25519.PublicKey{i.authority.KeyID: i.public}, - Revisions: i.state, Replay: i.replay, Seals: i.state, Grants: i.state, Now: i.authority.Now, - } +// ListResources answers which resources of kind a checked capability may act +// on with action, decided now by the core. +func (i *Issuer) ListResources(ctx context.Context, token, audience, kind, action string) ([]authority.Grant, error) { + out, err := i.listResources(ctx, token, audience, kind, action) + return out, classify(err) } -// ListResources answers which resources of kind a checked capability may act -// on with action, decided now rather than carried in the capability. The -// capability must hold the action on the kind. A person's answer is what her -// roles grant; a module's or an installation's own work is bounded by the -// capability alone. When the capability's chain names resource ids, only those -// the owner's grants cover remain. -func (i *Issuer) ListResources(ctx context.Context, token, audience, kind, action string) ([]resources.Grant, error) { +func (i *Issuer) listResources(ctx context.Context, token, audience, kind, action string) ([]authority.Grant, error) { if audience == "" || kind == "" || action == "" { return nil, fmt.Errorf("%w: a resource question names the audience, a kind and an action", ErrRefused) } - verified, err := i.verifier(audience).Verify(ctx, token) - if err != nil { - return nil, err - } - held, whole := false, false - var ids []string - for _, sc := range verified.EffectiveScopes() { - if sc.GetResourceKind() != kind || (!slices.Contains(sc.GetActions(), action) && !slices.Contains(sc.GetActions(), state.AnyAction)) { - continue - } - held = true - if len(sc.GetResourceIds()) == 0 { - whole = true - } - ids = append(ids, sc.GetResourceIds()...) - } - if !held { - return nil, fmt.Errorf("%w: the capability holds no %s:%s", ErrRefused, kind, action) - } - wc := verified.Context() - grants := []resources.Grant{{All: true}} - if wc.GetOwnerPrincipalKind() == "human" { - ceiling, bounded, err := i.solutionCeiling(wc.GetOwnerPrincipalId(), solutionPrincipals(wc, wc.GetAudience())) - if err != nil { - return nil, err - } - if bounded { - return roleResources(state.Intersect(verified.EffectiveScopes(), ceiling), kind, action), nil - } - if grants, err = i.state.ResourceGrants(wc.GetOwnerPrincipalId(), audience, kind, action); err != nil { - return nil, err - } - } - if whole { - return grants, nil - } - covered, err := resources.Compile(grants) - if err != nil { - return nil, err - } - narrowed := []resources.Grant{} - for _, id := range ids { - if covered.Matches(id) { - narrowed = append(narrowed, resources.Grant{ID: id}) - } - } - return narrowed, nil + return i.svc.ListResources(ctx, token, audience, kind, action) } // Check verifies a capability for the audience the caller is and, when method @@ -147,16 +114,20 @@ func (i *Issuer) Check(ctx context.Context, token, audience, method string) (*au // Advise is Check with the method's policy decided but not enforced: the // capability is verified and refused when unsound; what the policy would have // refused is answered in CheckResponse.Refusal beside the verified capability. -// A hop running permissive asks this way and logs the refusal it did not make. func (i *Issuer) Advise(ctx context.Context, token, audience, method string) (*authorityv1.CheckResponse, error) { return i.check(ctx, token, audience, method, true) } func (i *Issuer) check(ctx context.Context, token, audience, method string, advisory bool) (*authorityv1.CheckResponse, error) { + out, err := i.checked(ctx, token, audience, method, advisory) + return out, classify(err) +} + +func (i *Issuer) checked(ctx context.Context, token, audience, method string, advisory bool) (*authorityv1.CheckResponse, error) { if audience == "" { return nil, fmt.Errorf("%w: a check names the audience the caller is", ErrRefused) } - verified, err := i.verifier(audience).Verify(ctx, token) + verified, revision, err := i.svc.Check(ctx, token, audience) if err != nil { return nil, err } @@ -173,587 +144,153 @@ func (i *Issuer) check(ctx context.Context, token, audience, method string, advi if err != nil { return nil, err } - revision, err := i.state.AuthorizationRevision(ctx, verified.Context().GetTenantId()) - if err != nil { - return nil, err - } return &authorityv1.CheckResponse{Context: wc, AuthorizationRevision: revision, Refusal: refusal}, nil } -// Issue signs a capability for one owner shape. +// Issue signs a capability for one owner shape, through the core. func (i *Issuer) Issue(ctx context.Context, req *authorityv1.IssueRequest) (*authorityv1.IssueResponse, error) { - audienceName := req.GetAudience() - if audienceName == "" && req.GetOperationBindingId() != "" { - // A caller that names the binding it exercises need not know where the - // binding points: the audience is the binding's. Resolved from the - // module acting, a workload by its credential or a hop's actor. - derived, err := i.audienceOfBinding(ctx, req) - if err != nil { - return nil, err - } - audienceName = derived - req.Audience = audienceName - } - if req.GetAudience() == "" { - return nil, fmt.Errorf("%w: an issue names its audience, or the binding whose audience it is", ErrRefused) - } - audience, err := i.state.ModuleByPrincipal(req.GetAudience()) - if err != nil { - return nil, err - } - asked := FromWire(req.GetScopes()) - if err := refuseOwners(asked); err != nil { - return nil, err - } - ttl := time.Duration(req.GetTtlSeconds()) * time.Second - if ttl <= 0 || ttl > MaxTTL { - ttl = MaxTTL - } - var token string - var wc *basev0.WorkContextV1 + out, err := i.issue(ctx, req) + return out, classify(err) +} + +func (i *Issuer) issue(ctx context.Context, req *authorityv1.IssueRequest) (*authorityv1.IssueResponse, error) { + r := authority.IssueRequest{Audience: req.GetAudience(), Binding: req.GetOperationBindingId(), Operation: req.GetOperation(), + Organization: req.GetOrganizationId(), Scopes: FromWire(req.GetScopes()), TTL: time.Duration(req.GetTtlSeconds()) * time.Second} + var out authority.Issued + var err error switch as := req.GetAs().(type) { case *authorityv1.IssueRequest_Person: sessionID, ok := sessionOf(ctx) if !ok { return nil, fmt.Errorf("%w: a person-owned issue is made by a signed-in person; the call carries no session", ErrRefused) } - token, wc, err = i.forPerson(ctx, sessionID, audience, req.GetOperationBindingId(), asked, ttl) + person, perr := i.state.Caller(sessionID) + if perr != nil { + return nil, perr + } + out, err = i.svc.IssueForPerson(ctx, person, r) case *authorityv1.IssueRequest_Workload: - token, wc, err = i.forWorkload(ctx, as.Workload.GetCredential(), req.GetOrganizationId(), req.GetOperationBindingId(), req.GetOperation(), audience, asked, ttl) + w, werr := i.workload(ctx, as.Workload.GetCredential(), "a workload issue names the module asking") + if werr != nil { + return nil, werr + } + out, err = i.svc.IssueForWorkload(ctx, w, r) case *authorityv1.IssueRequest_Installation: - token, wc, err = i.forInstallation(ctx, as.Installation.GetInstallationId(), req.GetOrganizationId(), req.GetActor().GetCredential(), audience, asked, ttl) + w, werr := i.workload(ctx, req.GetActor().GetCredential(), "an installation acts through its solution's workload") + if werr != nil { + return nil, werr + } + out, err = i.svc.IssueForInstallation(ctx, w, as.Installation.GetInstallationId(), r) case *authorityv1.IssueRequest_ParentToken: - token, wc, err = i.child(ctx, as.ParentToken, req.GetActor().GetCredential(), req.GetOperationBindingId(), req.GetOperation(), audience, asked, ttl) + w, werr := i.workload(ctx, req.GetActor().GetCredential(), "a child hop names the module acting") + if werr != nil { + return nil, werr + } + out, err = i.svc.IssueChild(ctx, as.ParentToken, w, r) case *authorityv1.IssueRequest_Delegation: - token, wc, err = i.forDelegation(ctx, as.Delegation.GetDelegationId(), req.GetActor().GetCredential(), req.GetOperationBindingId(), req.GetOperation(), audience, asked, ttl) + w, werr := i.workload(ctx, req.GetActor().GetCredential(), "a delegated issue names the module acting") + if werr != nil { + return nil, werr + } + out, err = i.svc.IssueForDelegation(ctx, as.Delegation.GetDelegationId(), w, r) default: return nil, fmt.Errorf("%w: an issue names whom it is for", ErrRefused) } if err != nil { return nil, err } - wire, err := ToWire(wc) + wire, err := ToWire(out.Context) if err != nil { return nil, err } - return &authorityv1.IssueResponse{Token: token, ExpiresAtUnix: wc.GetExpiresAtUnix(), Context: wire}, nil -} - -// forPerson: owner = actor = the person; scopes = asked ∩ what their roles hold -// at the audience; sealed to the audience module's installation at their organisation. -func (i *Issuer) forPerson(ctx context.Context, sessionID string, audience state.Manifest, bindingID string, asked []*basev0.WorkScopeV1, ttl time.Duration) (string, *basev0.WorkContextV1, error) { - sess, err := i.state.Session(sessionID) - if err != nil { - return "", nil, err - } - revision, err := i.state.AuthorizationRevision(ctx, sess.Organization) - if err != nil { - return "", nil, err - } - // Issue's audience lookup precedes this revision snapshot. Resolve any - // binding again below it so a concurrent delivery cannot supply old scopes. - audience, err = i.state.ModuleByPrincipal(audience.Principal) - if err != nil { - return "", nil, err - } - if err := i.state.RequireInstallation(sess.Organization, audience.Module, audience.Principal); err != nil { - return "", nil, err - } - held, err := i.state.PersonScopes(sess.Person, audience.Principal) - if err != nil { - return "", nil, err - } - if bindingID != "" { - // The person's token names a binding of the audience module, so the - // module's hop through that binding carries exactly what the person - // holds at the binding's audience, and nothing is typed per organisation. - binding, declared := audience.Bindings[bindingID] - if !declared { - return "", nil, fmt.Errorf("%w: operation binding %q is not one %s declares", state.ErrUnknown, bindingID, audience.Module) - } - target, err := i.state.ModuleByPrincipal(binding.Audience) - if err != nil { - return "", nil, err - } - there, _, _, err := i.personThroughSolutions(sess.Person, target, []string{audience.Principal}) - if err != nil { - return "", nil, err - } - through := state.Intersect(there, binding.InvokeScopes) - if len(through) == 0 { - return "", nil, fmt.Errorf("%w: %s holds nothing under operation binding %q", ErrRefused, sess.Person, bindingID) - } - held = state.Normalize(append(held, through...)) - } - scopes, err := grant(asked, held) - if err != nil { - return "", nil, err - } - // Attribution is a set on the wire; never sort the session's shared slice. - teams := slices.Clone(sess.Teams) - slices.Sort(teams) - teams = slices.Compact(teams) - return i.startAtRevision(ctx, revision, workcontext.StartInput{ - TenantID: sess.Organization, OrganizationID: sess.Organization, - OwnerPrincipalID: sess.Person, OwnerPrincipalKind: "human", - TaskID: uuid.NewString(), SessionID: sess.ID, - Audience: audience.Principal, AuthorityScopes: scopeLimits{}.project(held, scopes), AttributionTeamIDs: teams, - InstallationID: state.InstallationID(sess.Organization, audience.Module), TTL: ttl, - }) + return &authorityv1.IssueResponse{Token: out.Token, ExpiresAtUnix: out.Context.GetExpiresAtUnix(), Context: wire}, nil } -// forWorkload: owner = actor = the module; scopes = the binding's headless -// scopes ∩ asked; the binding names the audience, so the request's must agree. -func (i *Issuer) forWorkload(ctx context.Context, credential, org, bindingID, operation string, audience state.Manifest, asked []*basev0.WorkScopeV1, ttl time.Duration) (string, *basev0.WorkContextV1, error) { - module, err := i.state.VerifyWorkload(ctx, credential) - if err != nil { - return "", nil, err - } - if bindingID == "" { - return "", nil, fmt.Errorf("%w: a workload issue names the binding it exercises", ErrRefused) - } - if org == "" { - organizations := i.state.ActiveInstallationOrganizations(module.Module) - switch len(organizations) { - case 0: - return "", nil, fmt.Errorf("%w: module %q has no active installation; a workload must name the organization of the work item", ErrRefused, module.Module) - case 1: - org = organizations[0] - default: - return "", nil, fmt.Errorf("%w: module %q has ambiguous active installations across organizations; a workload must name the organization of the work item", ErrRefused, module.Module) - } - } - fullID := org + "/" + module.Module + "/" + bindingID - binding, ok := module.Bindings[bindingID] - if !ok { - return "", nil, fmt.Errorf("%w: %s declares no binding %q", workcontext.ErrNoBinding, module.Module, bindingID) - } - if binding.Audience != audience.Principal { - return "", nil, fmt.Errorf("%w: binding %s is addressed to %s, not %s", ErrRefused, bindingID, binding.Audience, audience.Principal) - } - through, err := scopesFor(binding, operation, "headless") - if err != nil { - return "", nil, err - } - scopes, err := grant(asked, through) - if err != nil { - return "", nil, err - } - token, signed, err := i.authority.Start(ctx, workcontext.StartInput{ - TenantID: org, OrganizationID: org, - OwnerPrincipalID: module.Principal, OwnerPrincipalKind: "service", - TaskID: uuid.NewString(), Audience: audience.Principal, AuthorityScopes: scopeLimits{}.project(through, scopes), - InstallationID: state.InstallationID(org, module.Module), OperationBindingID: fullID, TTL: ttl, - }) +// workload attests a credential to the composed module it proves. +func (i *Issuer) workload(ctx context.Context, credential, why string) (authority.Workload, error) { + m, err := i.state.VerifyWorkload(ctx, credential) if err != nil { - return "", nil, err + return authority.Workload{}, fmt.Errorf("%s: %w", why, err) } - if signed.GetSeal().GetPrincipalEpoch() != module.Generation || signed.GetOperationBinding().GetRevision() != module.Generation { - return "", nil, fmt.Errorf("%w: workload binding changed during issuance", ErrRefused) - } - return token, signed, nil -} - -// forInstallation: owner = actor = the installation's agent; scopes = asked ∩ -// the installation's role ∩ the audience's ceiling. Its solution authenticates -// with its own workload credential; an installation ID is not a credential. -func (i *Issuer) forInstallation(ctx context.Context, id, org, credential string, audience state.Manifest, asked []*basev0.WorkScopeV1, ttl time.Duration) (string, *basev0.WorkContextV1, error) { - actor, err := i.state.VerifyWorkload(ctx, credential) - if err != nil { - return "", nil, err - } - inst, held, revision, err := i.state.InstallationScopes(id, audience.Principal) - if err != nil { - return "", nil, err - } - if org != "" && org != inst.Organization { - return "", nil, fmt.Errorf("%w: installation %s belongs to organization %q, not %q", state.ErrForbidden, id, inst.Organization, org) - } - if actor.Kind != "solution" || actor.Module != inst.Target { - return "", nil, fmt.Errorf("%w: installation issuance requires the installed solution's workload", state.ErrForbidden) - } - - scopes, err := grant(asked, held) - if err != nil { - return "", nil, err - } - token, signed, err := i.startAtRevision(ctx, revision, workcontext.StartInput{ - TenantID: inst.Organization, OrganizationID: inst.Organization, - OwnerPrincipalID: inst.AgentPrincipal(), OwnerPrincipalKind: "agent", OwnerAgentID: inst.Target, - TaskID: uuid.NewString(), Audience: audience.Principal, AuthorityScopes: scopeLimits{}.project(held, scopes), - InstallationID: inst.ID, TTL: ttl, - }) - if err != nil { - return "", nil, err - } - // The installation's agent epoch does not identify its caller workload. - // Fence the authenticated solution generation separately, as workload Issue - // does, so a delivery in between authentication and mint cannot revive it. - generation, err := i.state.PrincipalEpoch(ctx, actor.Principal) - if err != nil { - return "", nil, err - } - if generation != actor.Generation { - return "", nil, fmt.Errorf("%w: installation workload changed during issuance", ErrRefused) - } - return token, signed, nil + return authority.Workload{Module: m.Name}, nil } -// startAtRevision binds the permission reads to the revision Core stamps. -// A concurrent grant change must not turn old scopes into fresh authority. -// A change after Core reads the revision is rejected by ordinary verification. -func (i *Issuer) startAtRevision(ctx context.Context, revision uint64, in workcontext.StartInput) (string, *basev0.WorkContextV1, error) { - token, signed, err := i.authority.Start(ctx, in) - if err != nil { - return "", nil, err - } - if signed.GetAuthorizationRevision() != revision { - return "", nil, fmt.Errorf("%w: permissions changed during issuance", ErrRefused) - } - return token, signed, nil +// AuthorizeTransfer decides whether an existing resource joins a solution +// principal, the store asking with the person's capability. +func (i *Issuer) AuthorizeTransfer(ctx context.Context, req *authorityv1.AuthorizeTransferRequest) (*authorityv1.AuthorizeTransferResponse, error) { + out, err := i.authorizeTransfer(ctx, req) + return out, classify(err) } -// child: one more hop, inherited from the owner. The parent is verified for its -// own audience and must be addressed to the module acting; the actor proves itself -// and names the binding it exercises, addressed to the hop's audience. The child -// is owned by the parent's owner and carries the parent's actors and then this -// one. Human and installation owners supply current target authority through the -// binding. A service owner carries only its existing authority. Stored -// delegations remain complete ceilings. Per-kind parent limits also persist -// across audiences; carried caps are never effective off-target grants. -func (i *Issuer) child(ctx context.Context, parent, actorCredential, bindingID, operation string, audience state.Manifest, asked []*basev0.WorkScopeV1, ttl time.Duration) (string, *basev0.WorkContextV1, error) { - actor, err := i.state.VerifyWorkload(ctx, actorCredential) +func (i *Issuer) authorizeTransfer(ctx context.Context, req *authorityv1.AuthorizeTransferRequest) (*authorityv1.AuthorizeTransferResponse, error) { + w, err := i.workload(ctx, req.GetActor().GetCredential(), "a transfer is asked by the module serving the kind") if err != nil { - return "", nil, fmt.Errorf("a child hop names the module acting: %w", err) - } - if bindingID == "" { - return "", nil, fmt.Errorf("%w: a module's hop names the binding it exercises", ErrRefused) - } - binding, declared := actor.Bindings[bindingID] - if !declared { - return "", nil, fmt.Errorf("%w: operation binding %q is not one %s declares", state.ErrUnknown, bindingID, actor.Module) - } - if binding.Audience != audience.Principal { - return "", nil, fmt.Errorf("%w: binding %q is addressed to %s, not %s", ErrRefused, bindingID, binding.Audience, audience.Principal) - } - decoded, err := workcontext.Decode(parent) - if err != nil { - return "", nil, err - } - verified, err := i.verifier(decoded.Context().GetAudience()).Verify(ctx, parent) - if err != nil { - return "", nil, err - } - claims := verified.Context() - if claims.GetAudience() != actor.Principal { - return "", nil, fmt.Errorf("%w: %s hops only on a token addressed to it; this one is addressed to %s", ErrRefused, actor.Module, claims.GetAudience()) - } - var held, sourceCeiling []*basev0.WorkScopeV1 - sourceBounded := false - if claims.GetOwnerPrincipalKind() == "human" { - held, sourceCeiling, sourceBounded, err = i.personThroughSolutions(claims.GetOwnerPrincipalId(), audience, solutionPrincipals(claims, actor.Principal)) - } else { - held, err = i.ownerHolds(claims, audience) - if claims.GetOwnerPrincipalKind() != "agent" { - held = verified.EffectiveScopes() - } - } - if err != nil { - return "", nil, err - } - through, err := scopesFor(binding, operation, "invoke") - if err != nil { - return "", nil, err - } - ceiling := state.Intersect(held, through) - if sourceBounded { - ceiling = state.Intersect(ceiling, sourceCeiling) - } - ceiling, err = i.delegationCeiling(claims, ceiling) - if err != nil { - return "", nil, err - } - limits := parentScopeLimits(claims) - ceiling = limits.constrain(ceiling) - if len(ceiling) == 0 { - return "", nil, fmt.Errorf("%w: %s holds nothing at %s through binding %q", ErrRefused, claims.GetOwnerPrincipalId(), audience.Module, bindingID) - } - scopes := ceiling - if len(asked) > 0 { - if !state.Attenuates(asked, ceiling) { - return "", nil, fmt.Errorf("%w: a child widens what its owner holds through binding %q", ErrRefused, bindingID) - } - scopes = state.Normalize(asked) - } - authorityScopes := limits.project(through, scopes) - // The child never outlives its parent, on the authority's own clock. - now := time.Now() - if i.authority.Now != nil { - now = i.authority.Now() - } - if remaining := time.Unix(claims.GetExpiresAtUnix(), 0).Sub(now); remaining < ttl { - ttl = remaining - } - if ttl <= 0 { - return "", nil, fmt.Errorf("%w: the parent has expired", ErrRefused) - } - tenant := claims.GetTenantId() - hops := append([]*basev0.WorkActorV1(nil), claims.GetActorChain()...) - // The chain is rebuilt for the new audience, and the token the final hop is - // derived from carries the parent's own session, so the child descends from - // the context it was asked from (its parent_session_id is that session), as - // a caller holding the parent checks. - parentSession := claims.GetSessionId() - rootSession := "" - if len(hops) == 0 { - rootSession = parentSession - } - token, _, err := i.authority.Start(ctx, workcontext.StartInput{ - TenantID: tenant, OrganizationID: claims.GetOrganizationId(), - OwnerPrincipalID: claims.GetOwnerPrincipalId(), OwnerPrincipalKind: claims.GetOwnerPrincipalKind(), OwnerAgentID: claims.GetOwnerAgentId(), - TaskID: claims.GetTaskId(), Audience: audience.Principal, AuthorityScopes: authorityScopes, AttributionTeamIDs: claims.GetAttributionTeamIds(), - InstallationID: state.InstallationID(tenant, actor.Module), SessionID: rootSession, TTL: ttl, ReplayPolicy: workcontext.ReplayIdempotent, - }) - if err != nil { - return "", nil, err - } - for n := 0; n <= len(hops); n++ { - current, err := i.verifier(audience.Principal).Verify(ctx, token) - if err != nil { - return "", nil, fmt.Errorf("verify the hop a child is derived from: %w", err) - } - in := workcontext.ChildInput{GrantedScopes: scopes, Audience: audience.Principal, TTL: ttl, ReplayPolicy: workcontext.ReplayIdempotent} - if n < len(hops) { - in.GrantedScopes = authorityScopes - in.PrincipalID, in.PrincipalKind, in.DelegationID = hops[n].GetPrincipalId(), hops[n].GetPrincipalKind(), hops[n].GetDelegationId() - if n == len(hops)-1 { - in.SessionID = parentSession - } - } else { - in.PrincipalID, in.PrincipalKind, in.DelegationID = actor.Principal, "service", claims.GetSessionId() - in.OperationBindingID = tenant + "/" + actor.Module + "/" + bindingID - } - var signed *basev0.WorkContextV1 - token, signed, err = i.authority.Child(ctx, current, in) - if err != nil { - return "", nil, err - } - if n == len(hops) { - if signed.GetOperationBinding().GetRevision() != actor.Generation { - return "", nil, fmt.Errorf("%w: actor binding changed during issuance", ErrRefused) - } - // Reconstruction must not refresh a parent across revocation. Check - // the original capability after the final child has been signed. - if err := i.verifier(claims.GetAudience()).Recheck(ctx, verified); err != nil { - return "", nil, err - } - return token, signed, nil - } - } - return "", nil, fmt.Errorf("%w: no hop was derived", ErrRefused) -} - -// scopesFor is what a binding may do for one operation: invoke, headless or -// lookup, or def when the request names none. -func scopesFor(b state.Binding, operation, def string) ([]*basev0.WorkScopeV1, error) { - if operation == "" { - operation = def - } - switch operation { - case "invoke": - return b.InvokeScopes, nil - case "headless": - return b.HeadlessScopes, nil - case "lookup": - return b.LookupScopes, nil - } - return nil, fmt.Errorf("%w: operation %q is none of invoke, headless and lookup", ErrRefused, operation) -} - -// forDelegation issues for a delegation's grantor through the module it was -// granted to. No live capability is presented: the delegation must stand, the -// actor must be its module, and the scopes are what was delegated, cut to what -// the grantor holds at the audience now and to the binding. The capability is -// the grantor's, with the module as actor and the delegation on its hop. -func (i *Issuer) forDelegation(ctx context.Context, id, actorCredential, bindingID, operation string, audience state.Manifest, asked []*basev0.WorkScopeV1, ttl time.Duration) (string, *basev0.WorkContextV1, error) { - d, err := i.state.DelegationByID(id) - if err != nil { - return "", nil, err - } - if d.Revoked { - return "", nil, fmt.Errorf("%w: delegation %s is revoked", ErrRefused, id) - } - revision, err := i.state.AuthorizationRevision(ctx, d.Organization) - if err != nil { - return "", nil, err - } - actor, err := i.state.VerifyWorkload(ctx, actorCredential) - if err != nil { - return "", nil, fmt.Errorf("an issue from a delegation names the module acting: %w", err) - } - if actor.Module != d.Module { - return "", nil, fmt.Errorf("%w: delegation %s is granted to %s, not %s", ErrRefused, id, d.Module, actor.Module) - } - if bindingID == "" { - return "", nil, fmt.Errorf("%w: an issue from a delegation names the binding it exercises", ErrRefused) - } - binding, declared := actor.Bindings[bindingID] - if !declared { - return "", nil, fmt.Errorf("%w: operation binding %q is not one %s declares", state.ErrUnknown, bindingID, actor.Module) - } - if binding.Audience != audience.Principal { - return "", nil, fmt.Errorf("%w: binding %q is addressed to %s, not %s", ErrRefused, bindingID, binding.Audience, audience.Principal) - } - through, err := scopesFor(binding, operation, "invoke") - if err != nil { - return "", nil, err - } - held, _, _, err := i.personThroughSolutions(d.Grantor, audience, []string{actor.Principal}) - if err != nil { - return "", nil, err - } - ceiling := state.Intersect(state.Intersect(d.WorkScopes(), held), through) - if len(ceiling) == 0 { - return "", nil, fmt.Errorf("%w: delegation %s grants nothing %s still holds at %s through binding %q", ErrRefused, id, d.Grantor, audience.Module, bindingID) - } - scopes, err := grant(asked, ceiling) - if err != nil { - return "", nil, err - } - token, _, err := i.startAtRevision(ctx, revision, workcontext.StartInput{ - TenantID: d.Organization, OrganizationID: d.Organization, - OwnerPrincipalID: d.Grantor, OwnerPrincipalKind: "human", - TaskID: uuid.NewString(), Audience: audience.Principal, AuthorityScopes: scopeLimits{}.project(through, scopes), - InstallationID: state.InstallationID(d.Organization, actor.Module), TTL: ttl, ReplayPolicy: workcontext.ReplayIdempotent, - }) - if err != nil { - return "", nil, err - } - current, err := i.verifier(audience.Principal).Verify(ctx, token) - if err != nil { - return "", nil, fmt.Errorf("verify the capability a delegation's hop is derived from: %w", err) - } - token, signed, err := i.authority.Child(ctx, current, workcontext.ChildInput{ - PrincipalID: actor.Principal, PrincipalKind: "service", DelegationID: d.ID, - GrantedScopes: scopes, Audience: audience.Principal, TTL: ttl, ReplayPolicy: workcontext.ReplayIdempotent, - OperationBindingID: d.Organization + "/" + actor.Module + "/" + bindingID, - }) - if err != nil { - return "", nil, err + return nil, err } - // Start may have observed a newer revision than the initial delegation - // read. Recheck after signing so that revocation cannot be laundered into - // a fresh capability; a later revocation invalidates its signed revision. - d, err = i.state.DelegationByID(id) + d, err := i.svc.AuthorizeTransfer(ctx, authority.TransferRequest{Token: req.GetToken(), Store: w, Kind: req.GetResourceKind(), + ResourceID: req.GetResourceId(), From: req.GetFromOwnerId(), To: req.GetToPrincipalId()}) if err != nil { - return "", nil, err - } - if d.Revoked { - return "", nil, fmt.Errorf("%w: delegation %s is revoked", ErrRefused, id) - } - if signed.GetOperationBinding().GetRevision() != actor.Generation { - return "", nil, fmt.Errorf("%w: actor binding changed during issuance", ErrRefused) + return nil, err } - return token, signed, nil + return &authorityv1.AuthorizeTransferResponse{PrincipalId: d.PrincipalID, AuthorizedBy: d.AuthorizedBy}, nil } // Delegate records the signed-in person's grant to a module. func (i *Issuer) Delegate(ctx context.Context, module string, scopes []*basev0.WorkScopeV1, purpose string) (state.Delegation, error) { - if err := refuseOwners(scopes); err != nil { - return state.Delegation{}, err - } + out, err := i.delegate(ctx, module, scopes, purpose) + return out, classify(err) +} + +func (i *Issuer) delegate(ctx context.Context, module string, scopes []*basev0.WorkScopeV1, purpose string) (state.Delegation, error) { person, err := i.signedIn(ctx) if err != nil { return state.Delegation{}, err } - return i.state.Delegate(person, module, scopes, purpose) + return i.svc.Delegate(ctx, person, module, scopes, purpose) } // GetDelegation reads a delegation for the module it is granted to or for its // grantor. Anyone else learns nothing, not even that it exists. func (i *Issuer) GetDelegation(ctx context.Context, id, actorCredential string) (state.Delegation, error) { - d, err := i.state.DelegationByID(id) - if err != nil { - return state.Delegation{}, err - } + out, err := i.getDelegation(ctx, id, actorCredential) + return out, classify(err) +} + +func (i *Issuer) getDelegation(ctx context.Context, id, actorCredential string) (state.Delegation, error) { if actorCredential != "" { - if actor, err := i.state.VerifyWorkload(ctx, actorCredential); err == nil && actor.Module == d.Module { - return d, nil + w, err := i.workload(ctx, actorCredential, "a delegation is read by its module") + if err != nil { + return state.Delegation{}, err } - } else if person, err := i.signedIn(ctx); err == nil && person == d.Grantor { - return d, nil + return i.svc.Delegation(ctx, id, nil, &w) + } + person, err := i.signedIn(ctx) + if err != nil { + return state.Delegation{}, err } - return state.Delegation{}, fmt.Errorf("%w: delegation %q", state.ErrUnknown, id) + return i.svc.Delegation(ctx, id, &person, nil) } // RevokeDelegation ends one of the signed-in person's delegations. func (i *Issuer) RevokeDelegation(ctx context.Context, id string) (state.Delegation, error) { + out, err := i.revokeDelegation(ctx, id) + return out, classify(err) +} + +func (i *Issuer) revokeDelegation(ctx context.Context, id string) (state.Delegation, error) { person, err := i.signedIn(ctx) if err != nil { return state.Delegation{}, err } - return i.state.RevokeDelegation(person, id) + return i.svc.RevokeDelegation(ctx, person, id) } -func (i *Issuer) signedIn(ctx context.Context) (string, error) { +func (i *Issuer) signedIn(ctx context.Context) (authority.Person, error) { sessionID, ok := sessionOf(ctx) if !ok { - return "", fmt.Errorf("%w: a delegation is made by a signed-in person; the call carries no session", ErrRefused) - } - sess, err := i.state.Session(sessionID) - if err != nil { - return "", err - } - return sess.Person, nil -} - -// ownerHolds is what the owner of a verified context holds at the audience now: -// a person's roles, or an installation's role, each within what the audience -// serves. -func (i *Issuer) ownerHolds(claims *basev0.WorkContextV1, audience state.Manifest) ([]*basev0.WorkScopeV1, error) { - owner := claims.GetOwnerPrincipalId() - switch claims.GetOwnerPrincipalKind() { - case "human": - return i.state.PersonScopes(owner, audience.Principal) - case "agent": - _, scopes, _, err := i.state.InstallationScopes(strings.TrimSuffix(owner, "/agent"), audience.Principal) - return scopes, err - } - // A module's own context inherits nothing more at the next audience: its - // child carries what it was already delegated. - return nil, nil -} - -// grant is asked ∩ held, or held when nothing was asked; asking beyond held is refused. -func grant(asked, held []*basev0.WorkScopeV1) ([]*basev0.WorkScopeV1, error) { - if len(asked) == 0 { - if len(held) == 0 { - return nil, fmt.Errorf("%w: the owner holds nothing at this audience", ErrRefused) - } - for _, sc := range held { - if slices.Contains(sc.GetActions(), state.AnyAction) { - return nil, fmt.Errorf("%w: the audience declares %s without its actions; ask for the scopes you need", ErrRefused, sc.GetResourceKind()) - } - } - return held, nil - } - if !state.Attenuates(asked, held) { - return nil, fmt.Errorf("%w: asked beyond what the owner holds", ErrRefused) + return authority.Person{}, fmt.Errorf("%w: a delegation is made by a signed-in person; the call carries no session", ErrRefused) } - return state.Normalize(asked), nil -} - -// refuseOwners refuses an owner-selected scope by name: this authority's -// algebra cannot compare owners, so honouring one would issue the whole kind, -// wider than asked. It mints owner-selected scopes only once it issues through -// the owner-aware core, behind the audience's owner_aware declaration. -func refuseOwners(scopes []*basev0.WorkScopeV1) error { - for _, sc := range scopes { - if sc.GetResourceOwnerId() != "" { - return fmt.Errorf("%w: %s names resource owner %q; this authority issues no owner-selected scope yet", ErrRefused, sc.GetResourceKind(), sc.GetResourceOwnerId()) - } - } - return nil + return i.state.Caller(sessionID) } // ToWire converts core's message to the interface's, byte for byte. @@ -777,26 +314,3 @@ func FromWire(in []*authorityv1.WorkScopeV1) []*basev0.WorkScopeV1 { } return out } - -// audienceOfBinding answers the audience of the binding a workload or a hop -// names, from the module its credential attests. -func (i *Issuer) audienceOfBinding(ctx context.Context, req *authorityv1.IssueRequest) (string, error) { - var credential string - switch as := req.GetAs().(type) { - case *authorityv1.IssueRequest_Workload: - credential = as.Workload.GetCredential() - case *authorityv1.IssueRequest_ParentToken, *authorityv1.IssueRequest_Delegation: - credential = req.GetActor().GetCredential() - default: - return "", fmt.Errorf("%w: only a workload or a hop names a binding in place of an audience", ErrRefused) - } - module, err := i.state.VerifyWorkload(ctx, credential) - if err != nil { - return "", err - } - binding, declared := module.Bindings[req.GetOperationBindingId()] - if !declared { - return "", fmt.Errorf("%w: operation binding %q is not one %s declares", state.ErrUnknown, req.GetOperationBindingId(), module.Module) - } - return binding.Audience, nil -} diff --git a/modules/authority/services/local/code/pkg/business/issuer/issuer_test.go b/modules/authority/services/local/code/pkg/business/issuer/issuer_test.go index 09482e6d..89620672 100644 --- a/modules/authority/services/local/code/pkg/business/issuer/issuer_test.go +++ b/modules/authority/services/local/code/pkg/business/issuer/issuer_test.go @@ -194,7 +194,7 @@ func TestAnUncomposedAudienceIsRefusedByName(t *testing.T) { _, err := k.Issuer.Issue(maya, &authorityv1.IssueRequest{ As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "modelservice", }) - if !errors.Is(err, state.ErrUnknown) || !strings.Contains(err.Error(), `"modelservice" is not composed`) { + if !errors.Is(err, state.ErrUnknown) || !strings.Contains(err.Error(), `"modelservice"`) { t.Fatalf("the audience typo of this week: %v", err) } } @@ -236,7 +236,7 @@ func TestAnUninstallRefusesAnOutstandingTokenOnTheNextCheck(t *testing.T) { if _, err := k.Issuer.Check(ctx, out.GetToken(), "wiki", ""); err != nil { t.Fatalf("before: %v", err) } - if err := k.State.Revoke(wiki.ID); err != nil { + if err := k.State.UninstallAsHost(wiki.ID); err != nil { t.Fatal(err) } _, err = k.Issuer.Check(ctx, out.GetToken(), "wiki", "") @@ -245,29 +245,6 @@ func TestAnUninstallRefusesAnOutstandingTokenOnTheNextCheck(t *testing.T) { } } -func TestAnInstallationActsForItselfWithinItsRoleAndTheAudiencesCeiling(t *testing.T) { - k := testkit.New(t) - ctx := context.Background() - out, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: state.InstallationID("acme", "wiki")}}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "wiki")}, Audience: "wiki", - }) - if err != nil { - t.Fatal(err) - } - wc := out.GetContext() - if wc.GetOwnerPrincipalId() != state.InstallationID("acme", "wiki")+"/agent" || wc.GetOwnerPrincipalKind() != "agent" || wc.GetOwnerAgentId() != "wiki" { - t.Fatalf("owner = %s/%s/%s", wc.GetOwnerPrincipalId(), wc.GetOwnerPrincipalKind(), wc.GetOwnerAgentId()) - } - if wc.GetAuthorityScopes()[0].GetResourceKind() != "wiki.page" || strings.Join(wc.GetAuthorityScopes()[0].GetActions(), ",") != "read" { - t.Fatalf("viewer role at its solution = %v", wc.GetAuthorityScopes()) - } - if _, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: state.InstallationID("acme", "nothing")}}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "wiki")}, Audience: "documents", - }); !errors.Is(err, state.ErrUnknown) { - t.Fatalf("unknown installation: %v", err) - } -} - func TestAPendingInstallationIsIssuedNothing(t *testing.T) { k := testkit.New(t) ctx := context.Background() @@ -275,13 +252,13 @@ func TestAPendingInstallationIsIssuedNothing(t *testing.T) { if _, err := k.State.ApplyManifest(testkit.Proposals(1)); err != nil { t.Fatal(err) } - if _, err := k.State.Install(ctx, "acme", "proposals", "viewer", "alex"); err == nil { + if _, err := k.State.InstallAsHost(ctx, "acme", "proposals", "viewer", "alex"); err == nil { t.Fatal("the install step failed; Install must say so") } _, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: state.InstallationID("acme", "proposals")}}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "proposals")}, Audience: "documents", }) - if !errors.Is(err, workcontext.ErrNoSeal) || !strings.Contains(err.Error(), "pending") { + if err == nil || !strings.Contains(err.Error(), "pending") { t.Fatalf("pending installation: %v", err) } } @@ -314,7 +291,7 @@ func TestAWorkloadIsIssuedItsBindingsHeadlessScopesForTheBindingsAudience(t *tes want error text string }{ - {"undeclared binding", &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Workload{Workload: &authorityv1.WorkloadCredential{Credential: cred}}, Audience: "model", OrganizationId: "acme", OperationBindingId: "tracing"}, workcontext.ErrNoBinding, `no binding "tracing"`}, + {"undeclared binding", &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Workload{Workload: &authorityv1.WorkloadCredential{Credential: cred}}, Audience: "model", OrganizationId: "acme", OperationBindingId: "tracing"}, workcontext.ErrNoBinding, "operation binding"}, {"binding addressed elsewhere", &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Workload{Workload: &authorityv1.WorkloadCredential{Credential: cred}}, Audience: "documents", OrganizationId: "acme", OperationBindingId: "model"}, issuer.ErrRefused, "addressed to model"}, {"beyond headless", &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Workload{Workload: &authorityv1.WorkloadCredential{Credential: cred}}, Audience: "model", OrganizationId: "acme", OperationBindingId: "model", Scopes: scopes(testkit.Scope("modelservice.profiles", "admin"))}, issuer.ErrRefused, "beyond"}, {"no organisation", &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Workload{Workload: &authorityv1.WorkloadCredential{Credential: cred}}, Audience: "model", OperationBindingId: "model"}, issuer.ErrRefused, "organization"}, @@ -343,7 +320,7 @@ func TestAWorkloadInfersOnlyOneActiveInstallingOrganization(t *testing.T) { t.Run(tc.name, func(t *testing.T) { k := testkit.New(t) for _, org := range tc.revoke { - if err := k.State.Revoke(state.InstallationID(org, "documents")); err != nil { + if err := k.State.UninstallAsHost(state.InstallationID(org, "documents")); err != nil { t.Fatal(err) } } @@ -445,7 +422,7 @@ func TestKeysLetAnIndependentVerifierCheckWhatWasIssued(t *testing.T) { verifier := &workcontext.Verifier{ Issuer: keys.GetIssuer(), Audience: "documents", Keys: map[string]ed25519.PublicKey{keys.GetKeys()[0].GetKeyId(): ed25519.PublicKey(keys.GetKeys()[0].GetPublicKey())}, - Revisions: k.State, Seals: k.State, Grants: k.State, Replay: workcontext.NewMemoryReplayStore(), Now: func() time.Time { return k.Now }, + Revisions: k.State.Service().Revisions(), Seals: k.State.Service().Seals(), Grants: noGrants{}, Replay: workcontext.NewMemoryReplayStore(), Now: func() time.Time { return k.Now }, } if _, err := verifier.Verify(ctx, out.GetToken()); err != nil { t.Fatalf("an independent core verifier refused what we issued: %v", err) @@ -510,11 +487,11 @@ func TestABindingNamingIssueMayLeaveItsAudienceToTheBinding(t *testing.T) { // declarations arrive. The developer role never invents actions from a wildcard. func TestTheDeveloperRoleHoldsWhatEachComposedModuleServes(t *testing.T) { k := testkit.New(t) - held, err := k.State.PersonScopes("dev", "records") + held, err := k.PersonScopes(t, "dev", "records") if err != nil || len(held) != 2 { t.Fatalf("dev at records: %v %v", held, err) } - if _, err := k.State.ApplyManifest(state.Manifest{Module: "probe", Principal: "probe", Kind: "module", Generation: 1, + if _, err := k.State.ApplyManifest(state.Manifest{Name: "probe", Principal: "probe", Kind: "module", Generation: 1, ResourceKinds: []*basev0.WorkScopeV1{testkit.Scope("probe.items", state.AnyAction)}}); err != nil { t.Fatal(err) } @@ -611,3 +588,10 @@ func TestGlobexCollectionReadIsAnExplicitFixtureGrant(t *testing.T) { }) } } + +// noGrants is a module verifier's grant source when it accepts no approval grant. +type noGrants struct{} + +func (noGrants) Grant(context.Context, string) (*workcontext.Grant, error) { + return nil, errors.New("no approval grants") +} diff --git a/modules/authority/services/local/code/pkg/business/issuer/owner_refused_test.go b/modules/authority/services/local/code/pkg/business/issuer/owner_refused_test.go deleted file mode 100644 index 202dd251..00000000 --- a/modules/authority/services/local/code/pkg/business/issuer/owner_refused_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package issuer_test - -import ( - "errors" - "strings" - "testing" - - "gateway/pkg/business/issuer" - "gateway/pkg/business/testkit" - authorityv1 "gateway/pkg/gen/host/authority/v1" - base "github.com/codefly-dev/core/generated/go/codefly/base/v0" -) - -// The wire carries resource_owner_id, and this authority cannot compare -// owners yet: an owner-selected ask is refused by name, never issued as the -// whole kind, and so is an owner-selected delegation. -func TestAnOwnerSelectedAskIsRefusedNeverWidened(t *testing.T) { - k := testkit.New(t) - owned := []*authorityv1.WorkScopeV1{{ResourceKind: "records.records", Actions: []string{"read"}, ResourceOwnerId: "principal-u"}} - _, err := k.Issuer.Issue(person(t, k, "fixture-maya"), &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "records", Scopes: owned}) - if !errors.Is(err, issuer.ErrRefused) || !strings.Contains(err.Error(), "resource owner") { - t.Fatalf("an owner-selected ask: %v", err) - } - _, err = k.Issuer.Delegate(person(t, k, "fixture-remi"), "org-directory", - []*base.WorkScopeV1{{ResourceKind: "documents", Actions: []string{"read"}, ResourceOwnerId: "principal-u"}}, "owned") - if !errors.Is(err, issuer.ErrRefused) || !strings.Contains(err.Error(), "resource owner") { - t.Fatalf("an owner-selected delegation: %v", err) - } -} diff --git a/modules/authority/services/local/code/pkg/business/issuer/parent_rows_test.go b/modules/authority/services/local/code/pkg/business/issuer/parent_rows_test.go index d512cc5b..bc35bdba 100644 --- a/modules/authority/services/local/code/pkg/business/issuer/parent_rows_test.go +++ b/modules/authority/services/local/code/pkg/business/issuer/parent_rows_test.go @@ -9,58 +9,6 @@ import ( base "github.com/codefly-dev/core/generated/go/codefly/base/v0" ) -func TestRootPreservesActionSpecificResources(t *testing.T) { - for _, shape := range []string{"person", "workload", "installation", "delegation"} { - t.Run(shape, func(t *testing.T) { - k, _ := solutionRoleKit(t, testkit.Scope("documents", "read", "write")) - rows := []*base.WorkScopeV1{ - {ResourceKind: "documents", Actions: []string{"read"}, ResourceIds: []string{"a"}}, - {ResourceKind: "documents", Actions: []string{"write"}, ResourceIds: []string{"b"}}, - } - ctx := person(t, k, "fixture-remi") - req := &authorityv1.IssueRequest{Audience: "documents", Scopes: scopes(rows...)} - switch shape { - case "person": - req.As = &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}} - case "workload": - module := testkit.OrgDirectory(2) - binding := module.Bindings["documents"] - binding.HeadlessScopes = []*base.WorkScopeV1{testkit.Scope("documents", "read", "write")} - module.Bindings["documents"] = binding - if _, err := k.State.ApplyManifest(module); err != nil { - t.Fatal(err) - } - req.As = &authorityv1.IssueRequest_Workload{Workload: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "org-directory")}} - req.OrganizationId, req.OperationBindingId = "acme", "documents" - case "installation": - req.As = &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: state.InstallationID("acme", "wiki")}} - req.Actor = &authorityv1.WorkloadCredential{Credential: k.Credential(t, "wiki")} - case "delegation": - d, err := k.Issuer.Delegate(ctx, "org-directory", rows, "action-specific resources") - if err != nil { - t.Fatal(err) - } - req.As = &authorityv1.IssueRequest_Delegation{Delegation: &authorityv1.DelegationRef{DelegationId: d.ID}} - req.Actor = &authorityv1.WorkloadCredential{Credential: k.Credential(t, "org-directory")} - req.OperationBindingId = "documents" - } - out, err := k.Issuer.Issue(ctx, req) - if err != nil { - t.Fatal(err) - } - if _, err = k.Issuer.Check(t.Context(), out.Token, "documents", ""); err != nil { - t.Fatal(err) - } - for action, id := range map[string]string{"read": "a", "write": "b"} { - grants, err := k.Issuer.ListResources(t.Context(), out.Token, "documents", "documents", action) - if err != nil || len(grants) != 1 || grants[0].ID != id { - t.Fatalf("%s must grant only %s: grants=%v err=%v", action, id, grants, err) - } - } - }) - } -} - func TestMalformedAskDoesNotGainAuthority(t *testing.T) { for _, scope := range []*authorityv1.WorkScopeV1{nil, {}, {ResourceKind: "modelservice.profiles"}, {Actions: []string{"invoke"}}} { k := testkit.New(t) diff --git a/modules/authority/services/local/code/pkg/business/issuer/resources_test.go b/modules/authority/services/local/code/pkg/business/issuer/resources_test.go index dfaca8f2..b98fb72b 100644 --- a/modules/authority/services/local/code/pkg/business/issuer/resources_test.go +++ b/modules/authority/services/local/code/pkg/business/issuer/resources_test.go @@ -3,6 +3,7 @@ package issuer_test import ( "context" "errors" + "github.com/codefly-dev/authority/libraries/authority-core/go/authority" "testing" resources "github.com/codefly-dev/authority/libraries/resources/go" @@ -24,8 +25,15 @@ func issued(t *testing.T, k *testkit.Kit, fixtureToken string, scopes []*authori return out.GetToken() } -func covered(t *testing.T, grants []resources.Grant) *resources.Matcher { +func covered(t *testing.T, owned []authority.Grant) *resources.Matcher { t.Helper() + var grants []resources.Grant + for _, g := range owned { + if g.Owner != "" { + t.Fatalf("an organisation grant names an owner: %+v", g) + } + grants = append(grants, g.Grant) + } m, err := resources.Compile(grants) if err != nil { t.Fatal(err) diff --git a/modules/authority/services/local/code/pkg/business/issuer/scenario_test.go b/modules/authority/services/local/code/pkg/business/issuer/scenario_test.go deleted file mode 100644 index afa11244..00000000 --- a/modules/authority/services/local/code/pkg/business/issuer/scenario_test.go +++ /dev/null @@ -1,227 +0,0 @@ -package issuer_test - -// The primer's section 4, one scenario, one hop at a time, run against the local -// authority with the same cast: Acme, Maya on Finance, Rémi the engineer, the -// wiki, documents, model, runtime, records and the org-directory page. Each -// step asserts what the primer says the token carries and what refuses today. - -import ( - "context" - "errors" - "strings" - "testing" - - "github.com/codefly-dev/core/workcontext" - - "gateway/pkg/business/issuer" - "gateway/pkg/business/state" - "gateway/pkg/business/testkit" - authorityv1 "gateway/pkg/gen/host/authority/v1" -) - -func TestTheScenarioOneHopAtATime(t *testing.T) { - k := testkit.New(t) - ctx := context.Background() - documents := &authorityv1.WorkloadCredential{Credential: k.Credential(t, "documents")} - var maya state.Session - var asMaya context.Context - var mayaAtDocuments string - - t.Run("1 Maya signs in", func(t *testing.T) { - var err error - maya, err = k.State.SignIn("fixture-maya") - if err != nil || maya.Organization != "acme" || maya.Teams[0] != "finance" { - t.Fatalf("Maya's session: %v %+v", err, maya) - } - asMaya = issuer.WithSession(ctx, maya.ID) // the bearer every call of hers carries - // Her session says who she is, not what she may do in documents. - if len(maya.Roles) != 1 || maya.Roles[0] != "viewer" { - t.Fatalf("roles = %v", maya.Roles) - } - }) - - t.Run("2 Maya sees the wiki in her navigation", func(t *testing.T) { - nav := k.State.Entitled(maya.Person) - if len(nav) != 1 || nav[0].Target != "wiki" { - t.Fatalf("Maya's navigation = %+v", nav) - } - // Rémi on engineering sees it too: the installation reaches both teams. - if nav := k.State.Entitled("remi"); len(nav) != 1 || nav[0].Target != "wiki" { - t.Fatalf("Rémi's navigation = %+v", nav) - } - }) - - t.Run("3 the wiki reads a page body from documents on Maya's behalf", func(t *testing.T) { - out, err := k.Issuer.Issue(asMaya, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, - Audience: "documents", Scopes: scopes(testkit.Scope("documents", "read")), - }) - if err != nil { - t.Fatal(err) - } - wc := out.GetContext() - // owner = actor = Maya; audience documents; scopes = roles ∩ ceiling; sealed to documents at Acme. - if wc.GetOwnerPrincipalId() != "maya" || len(wc.GetActorChain()) != 0 || wc.GetAudience() != "documents" || - wc.GetSeal().GetInstallationId() != state.InstallationID("acme", "documents") || strings.Join(wc.GetAuthorityScopes()[0].GetActions(), ",") != "read" { - t.Fatalf("step 3 token = %v", wc) - } - if _, err := k.Issuer.Check(ctx, out.GetToken(), "documents", ""); err != nil { - t.Fatalf("documents checks it: %v", err) - } - mayaAtDocuments = out.GetToken() - // Refuses today: a role the solution cannot declare. Here the viewer role - // is declared in the fixture and write is refused by name. - _, err = k.Issuer.Issue(asMaya, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, - Audience: "documents", Scopes: scopes(testkit.Scope("documents", "write")), - }) - if !errors.Is(err, issuer.ErrRefused) { - t.Fatalf("a viewer writing: %v", err) - } - }) - - t.Run("4 documents embeds the page through the model gateway, for itself", func(t *testing.T) { - out, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Workload{Workload: documents}, Audience: "model", OrganizationId: "acme", OperationBindingId: "model", - }) - if err != nil { - t.Fatal(err) - } - wc := out.GetContext() - if wc.GetOwnerPrincipalId() != "documents" || wc.GetOwnerPrincipalKind() != "service" || wc.GetAudience() != "model" || - wc.GetOperationBinding().GetBindingId() != "acme/documents/model" || wc.GetSeal().GetInstallationId() != state.InstallationID("acme", "documents") { - t.Fatalf("step 4 token = %v", wc) - } - if _, err := k.Issuer.Check(ctx, out.GetToken(), "model", ""); err != nil { - t.Fatalf("model checks it: %v", err) - } - // Refuses today, the audience: the composition typed modelservice in a second key. - _, err = k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Workload{Workload: documents}, Audience: "modelservice", OrganizationId: "acme", OperationBindingId: "model", - }) - if !errors.Is(err, state.ErrUnknown) || !strings.Contains(err.Error(), `"modelservice" is not composed`) { - t.Fatalf("the audience typo: %v", err) - } - // Refuses today, all or nothing: the envelope forgot runtime-admission and - // documents lost its whole entry. Here documents keeps both bindings. - if _, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Workload{Workload: documents}, Audience: "runtime", OrganizationId: "acme", OperationBindingId: "runtime-admission", - }); err != nil { - t.Fatalf("the second binding: %v", err) - } - }) - - t.Run("5 documents fans out only for a runtime-entitled person", func(t *testing.T) { - // Maya has no runtime grant. Her documents read must not stand in for it. - _, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_ParentToken{ParentToken: mayaAtDocuments}, Actor: documents, - Audience: "runtime", OperationBindingId: "runtime-admission", TtlSeconds: 60, - }) - if !errors.Is(err, issuer.ErrRefused) { - t.Fatalf("Maya's absent runtime authority: %v", err) - } - parent, err := k.Issuer.Issue(person(t, k, "fixture-dev"), &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents", - }) - if err != nil { - t.Fatal(err) - } - child, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_ParentToken{ParentToken: parent.Token}, Actor: documents, - Audience: "runtime", OperationBindingId: "runtime-admission", TtlSeconds: 60, - }) - if err != nil { - t.Fatal(err) - } - wc := child.GetContext() - if wc.GetOwnerPrincipalId() != "dev" || len(wc.GetActorChain()) != 1 || wc.GetActorChain()[0].GetPrincipalId() != "documents" || - wc.GetAudience() != "runtime" || wc.GetExpiresAtUnix()-k.Now.Unix() < 60 || wc.GetExpiresAtUnix()-k.Now.Unix() > 62 { - t.Fatalf("step 5 child = %v", wc) - } - if _, err := k.Issuer.Check(ctx, child.GetToken(), "runtime", ""); err != nil { - t.Fatalf("the runtime checks the child: %v", err) - } - if len(effectiveScopes(wc)) != 1 || actions(wc, "runtime.task") != "admit,read" { - t.Fatalf("runtime child carries scopes outside its binding: %v", effectiveScopes(wc)) - } - // Even the developer cannot carry documents scope through this binding. - _, err = k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_ParentToken{ParentToken: parent.Token}, Actor: documents, - Audience: "runtime", OperationBindingId: "runtime-admission", Scopes: scopes(testkit.Scope("documents", "write")), - }) - if !errors.Is(err, issuer.ErrRefused) { - t.Fatalf("widening through the runtime: %v", err) - } - }) - - t.Run("6 overnight, the wiki reconciles, as itself", func(t *testing.T) { - out, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: state.InstallationID("acme", "wiki")}}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "wiki")}, Audience: "wiki", - }) - if err != nil { - t.Fatal(err) - } - wc := out.GetContext() - // owner = actor = the wiki at Acme; scopes = what Acme consented to at install (viewer) ∩ the solution's own ceiling. No downstream grant is implied. - if wc.GetOwnerPrincipalId() != state.InstallationID("acme", "wiki")+"/agent" || wc.GetOwnerAgentId() != "wiki" || wc.GetSeal().GetInstallationId() != state.InstallationID("acme", "wiki") || - strings.Join(wc.GetAuthorityScopes()[0].GetActions(), ",") != "read" { - t.Fatalf("step 6 token = %v", wc) - } - // Acme's administrator uninstalls the wiki at 10:00; the 09:58 token is refused at 10:00:01. - if err := k.State.Revoke(state.InstallationID("acme", "wiki")); err != nil { - t.Fatal(err) - } - if _, err := k.Issuer.Check(ctx, out.GetToken(), "wiki", ""); err == nil { - t.Fatal("an uninstalled wiki still acted") - } - }) - - t.Run("7 Maya opens a module page that reads records", func(t *testing.T) { - // Today a module page has no path to a records-audience token. Here the - // page is a module, Maya holds records read through her role, and the - // page asks for Maya at records: the module-for-a-person shape. - out, err := k.Issuer.Issue(asMaya, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "records", - }) - if err != nil { - t.Fatal(err) - } - wc := out.GetContext() - kinds := map[string]string{} - for _, sc := range wc.GetAuthorityScopes() { - kinds[sc.GetResourceKind()] = strings.Join(sc.GetActions(), ",") - } - if kinds["records.records"] != "read" || kinds["records.types"] != "read" || wc.GetSeal().GetInstallationId() != state.InstallationID("acme", "records") { - t.Fatalf("step 7 token = %v", wc) - } - // Rémi, an editor, is also issued write on records; the UI follows the token. - remi, _ := k.State.SignIn("fixture-remi") - out, err = k.Issuer.Issue(issuer.WithSession(ctx, remi.ID), &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "records", - }) - if err != nil { - t.Fatal(err) - } - for _, sc := range out.GetContext().GetAuthorityScopes() { - if sc.GetResourceKind() == "records.records" && strings.Join(sc.GetActions(), ",") != "read,write" { - t.Fatalf("Rémi at records = %v", sc) - } - } - }) - - t.Run("8 Rémi runs all of this on a laptop", func(t *testing.T) { - // Everything above ran with no accounts service, no store, no envelope, no - // anchor file: one launcher key, one fixture, one issuer. The only thing a - // laptop and a cell share is the code that issued and checked. - keys := k.Issuer.Keys() - if keys.GetIssuer() == "" || len(keys.GetKeys()) != 1 { - t.Fatalf("keys = %v", keys) - } - if _, err := k.State.VerifyWorkload(ctx, documents.GetCredential()); err != nil { - t.Fatalf("the launcher's word for documents: %v", err) - } - if _, err := k.Issuer.Check(ctx, workcontext.FixtureForeignToken(), "documents", ""); err == nil { - t.Fatal("a token that is not ours verified") - } - }) -} diff --git a/modules/authority/services/local/code/pkg/business/issuer/scope_limits.go b/modules/authority/services/local/code/pkg/business/issuer/scope_limits.go deleted file mode 100644 index 0560d1d5..00000000 --- a/modules/authority/services/local/code/pkg/business/issuer/scope_limits.go +++ /dev/null @@ -1,80 +0,0 @@ -package issuer - -import ( - "sort" - - "gateway/pkg/business/state" - base "github.com/codefly-dev/core/generated/go/codefly/base/v0" - "google.golang.org/protobuf/proto" -) - -// scopeLimits are lasting bounds on visited kinds. A zero-action scope is an -// explicit denial; an absent kind has not been constrained. Never normalize the -// entire set through the grant helpers, which intentionally omit empty grants. -type scopeLimits map[string][]*base.WorkScopeV1 - -func parentScopeLimits(parent *base.WorkContextV1) scopeLimits { - sets := [][]*base.WorkScopeV1{parent.GetAuthorityScopes()} - for _, actor := range parent.GetActorChain() { - sets = append(sets, actor.GetGrantedScopes()) - } - limits := scopeLimits{} - for _, scopes := range sets { - for kind, rows := range scopesByKind(scopes) { - if previous, ok := limits[kind]; ok { - limits[kind] = state.Intersect(previous, rows) - } else { - limits[kind] = rows - } - } - } - return limits -} - -func (limits scopeLimits) constrain(scopes []*base.WorkScopeV1) []*base.WorkScopeV1 { - var out []*base.WorkScopeV1 - for _, scope := range scopes { - if bound, ok := limits[scope.GetResourceKind()]; ok { - out = append(out, state.Intersect([]*base.WorkScopeV1{scope}, bound)...) - } else { - out = append(out, scope) - } - } - return out -} - -// project updates only the current binding's kinds, retaining other audience -// caps. An omitted or disjoint target kind stays explicitly denied downstream. -func (limits scopeLimits) project(domain, granted []*base.WorkScopeV1) []*base.WorkScopeV1 { - for _, scope := range domain { - limits[scope.GetResourceKind()] = nil - } - for kind, rows := range scopesByKind(granted) { - limits[kind] = rows - } - out := make([]*base.WorkScopeV1, 0, len(limits)) - kinds := make([]string, 0, len(limits)) - for kind := range limits { - kinds = append(kinds, kind) - } - sort.Strings(kinds) - for _, kind := range kinds { - rows := state.Normalize(limits[kind]) - if len(rows) == 0 { - rows = []*base.WorkScopeV1{{ResourceKind: kind}} - } - out = append(out, rows...) - } - return out -} - -// Keep every action/resource row together. Combining their actions and ids -// separately would authorize the cross product; keeping only one loses grants. -func scopesByKind(scopes []*base.WorkScopeV1) scopeLimits { - grouped := scopeLimits{} - for _, scope := range scopes { - kind := scope.GetResourceKind() - grouped[kind] = append(grouped[kind], proto.CloneOf(scope)) - } - return grouped -} diff --git a/modules/authority/services/local/code/pkg/business/issuer/solution_roles.go b/modules/authority/services/local/code/pkg/business/issuer/solution_roles.go deleted file mode 100644 index a16f7be9..00000000 --- a/modules/authority/services/local/code/pkg/business/issuer/solution_roles.go +++ /dev/null @@ -1,90 +0,0 @@ -package issuer - -import ( - "gateway/pkg/business/state" - resources "github.com/codefly-dev/authority/libraries/resources/go" - basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" - "slices" -) - -// solutionPrincipals reads provenance already verified by Core. A module hop -// retains its predecessors; it cannot drop a solution's permission source. -func solutionPrincipals(wc *basev0.WorkContextV1, current string) []string { - out := []string{current} - for _, actor := range wc.GetActorChain() { - if actor.GetPrincipalKind() == "service" { - out = append(out, actor.GetPrincipalId()) - } - } - return out -} - -// solutionCeiling combines the current role of each solution in the verified -// path. The boolean distinguishes no solution from a solution granting nothing. -func (i *Issuer) solutionCeiling(person string, principals []string) ([]*basev0.WorkScopeV1, bool, error) { - seen := map[string]bool{} - var ceiling []*basev0.WorkScopeV1 - bounded := false - for _, principal := range principals { - if seen[principal] { - continue - } - seen[principal] = true - m, err := i.state.ModuleByPrincipal(principal) - if err != nil { - return nil, false, err - } - if m.Kind != "solution" { - continue - } - role, err := i.state.PersonSolutionScopes(person, principal) - if err != nil { - return nil, false, err - } - if bounded { - ceiling = state.Intersect(ceiling, role) - } else { - ceiling = role - } - bounded = true - } - return ceiling, bounded, nil -} - -// personThroughSolutions selects solution roles instead of organization roles -// when a solution acts. A target solution additionally requires its own grant. -func (i *Issuer) personThroughSolutions(person string, audience state.Manifest, principals []string) (held, ceiling []*basev0.WorkScopeV1, bounded bool, err error) { - ceiling, bounded, err = i.solutionCeiling(person, principals) - if err != nil { - return - } - if !bounded { - held, err = i.state.PersonScopes(person, audience.Principal) - return - } - held = state.Intersect(ceiling, audience.ResourceKinds) - if audience.Kind == "solution" { - var target []*basev0.WorkScopeV1 - target, err = i.state.PersonScopes(person, audience.Principal) - held = state.Intersect(held, target) - } - return -} - -// roleResources projects only exact IDs or whole-kind grants, as solution roles -// declare them. Organization prefix/pattern grants keep their existing reader. -func roleResources(scopes []*basev0.WorkScopeV1, kind, action string) []resources.Grant { - var grants []resources.Grant - for _, scope := range scopes { - if scope.GetResourceKind() != kind || (!slices.Contains(scope.GetActions(), action) && !slices.Contains(scope.GetActions(), state.AnyAction)) { - continue - } - if len(scope.GetResourceIds()) == 0 { - return []resources.Grant{{All: true}} - } - for _, id := range scope.GetResourceIds() { - grants = append(grants, resources.Grant{ID: id}) - } - } - return grants -} diff --git a/modules/authority/services/local/code/pkg/business/issuer/solution_roles_test.go b/modules/authority/services/local/code/pkg/business/issuer/solution_roles_test.go deleted file mode 100644 index 55bb3dcb..00000000 --- a/modules/authority/services/local/code/pkg/business/issuer/solution_roles_test.go +++ /dev/null @@ -1,383 +0,0 @@ -package issuer_test - -import ( - "context" - "errors" - "maps" - "testing" - - "gateway/pkg/business/issuer" - "gateway/pkg/business/state" - "gateway/pkg/business/testkit" - authorityv1 "gateway/pkg/gen/host/authority/v1" - basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" - "github.com/codefly-dev/core/workcontext" -) - -func solutionRoleKit(t *testing.T, roleScope *basev0.WorkScopeV1) (*testkit.Kit, state.Manifest) { - t.Helper() - k := testkit.New(t) - m := testkit.Wiki(2) - m.Bindings = map[string]state.Binding{ - "documents": {ID: "documents", Audience: "documents", InvokeScopes: []*basev0.WorkScopeV1{testkit.Scope("documents", "read", "write")}}, - "directory": {ID: "directory", Audience: "org-directory", InvokeScopes: []*basev0.WorkScopeV1{testkit.Scope("directory.entry", "read")}}, - } - m.Roles["viewer"] = append(m.Roles["viewer"], roleScope, testkit.Scope("directory.entry", "read")) - if _, err := k.State.ApplyManifest(m); err != nil { - t.Fatal(err) - } - return k, m -} - -// Prepare parents and stored consent before an issuance-interleaving test starts. -func solutionPersonRequest(t *testing.T, k *testkit.Kit, who, path string, ask []*authorityv1.WorkScopeV1) (context.Context, *authorityv1.IssueRequest, error) { - t.Helper() - ctx := person(t, k, "fixture-"+who) - req := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "wiki"} - if path == "binding" { - req.OperationBindingId, req.Scopes = "documents", ask - return ctx, req, nil - } - if path == "delegation" { - d, err := k.Issuer.Delegate(ctx, "wiki", issuer.FromWire(ask), "solution role test") - if err != nil { - return nil, nil, err - } - return context.Background(), &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Delegation{Delegation: &authorityv1.DelegationRef{DelegationId: d.ID}}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "wiki")}, Audience: "documents", OperationBindingId: "documents", Scopes: ask}, nil - } - root, err := k.Issuer.Issue(ctx, req) - if err != nil { - return nil, nil, err - } - actor := "wiki" - if path == "later module" { - root, err = k.Issuer.Issue(context.Background(), &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_ParentToken{ParentToken: root.Token}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "wiki")}, OperationBindingId: "directory", Audience: "org-directory", Scopes: scopes(testkit.Scope("directory.entry", "read"))}) - if err != nil { - return nil, nil, err - } - actor = "org-directory" - } - return context.Background(), &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_ParentToken{ParentToken: root.Token}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, actor)}, OperationBindingId: "documents", Audience: "documents", Scopes: ask}, nil -} - -func solutionPersonIssue(t *testing.T, k *testkit.Kit, who, path string, ask []*authorityv1.WorkScopeV1) (*authorityv1.IssueResponse, error) { - t.Helper() - ctx, req, err := solutionPersonRequest(t, k, who, path, ask) - if err != nil { - return nil, err - } - return k.Issuer.Issue(ctx, req) -} - -func TestSolutionRoleIsThePermissionSourceForEveryPersonPath(t *testing.T) { - for _, path := range []string{"binding", "child", "later module", "delegation"} { - for _, tc := range []struct { - who, role, ask string - allowed bool - }{ - {"remi", "read", "read", true}, {"remi", "read", "write", false}, - {"maya", "write", "write", true}, - } { - t.Run(path+"/"+tc.who+"/"+tc.ask, func(t *testing.T) { - k, _ := solutionRoleKit(t, testkit.Scope("documents", tc.role)) - out, err := solutionPersonIssue(t, k, tc.who, path, scopes(testkit.Scope("documents", tc.ask))) - if !tc.allowed { - if err == nil { - t.Fatalf("recovered organization write outside solution role: %v", out.Context) - } - return - } - if err != nil { - t.Fatal(err) - } - if _, err := k.Issuer.Check(context.Background(), out.Token, out.Context.Audience, ""); err != nil { - t.Fatal(err) - } - grants, err := k.Issuer.ListResources(context.Background(), out.Token, out.Context.Audience, "documents", tc.ask) - if err != nil || !covered(t, grants).All() { - t.Fatalf("unrestricted role lost resources: %v %v", grants, err) - } - if tc.who == "maya" { - _, err := k.Issuer.Issue(person(t, k, "fixture-maya"), &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents", Scopes: scopes(testkit.Scope("documents", "write"))}) - if !errors.Is(err, issuer.ErrRefused) { - t.Fatalf("solution role leaked into independent module access: %v", err) - } - } - }) - } - } -} - -func TestSolutionBindingCannotBootstrapWithoutEntitlement(t *testing.T) { - for _, path := range []string{"binding", "child", "later module", "delegation"} { - for _, unavailable := range []string{"no team", "installation revoked", "other organization only"} { - t.Run(path+"/"+unavailable, func(t *testing.T) { - k, _ := solutionRoleKit(t, testkit.Scope("documents", "write")) - who := "dev" // Keep the organisation union, but grant only finance below. - if unavailable == "no team" { - if err := k.State.Revoke(state.InstallationID("acme", "wiki")); err != nil { - t.Fatal(err) - } - inst, err := k.State.Install(context.Background(), "acme", "wiki", "viewer", "alex") - if err != nil { - t.Fatal(err) - } - if _, err := k.State.GrantToTeam(context.Background(), "acme", inst.ID, "finance"); err != nil { - t.Fatal(err) - } - } - if unavailable != "no team" { - who = "remi" - if err := k.State.Revoke(state.InstallationID("acme", "wiki")); err != nil { - t.Fatal(err) - } - if unavailable == "installation revoked" { - if err := k.State.Revoke(state.InstallationID("globex", "wiki")); err != nil { - t.Fatal(err) - } - } - } - if out, err := solutionPersonIssue(t, k, who, path, scopes(testkit.Scope("documents", "write"))); err == nil { - t.Fatalf("organization grant bootstrapped solution access: %v", out.Context) - } - }) - } - } -} - -func TestSolutionInstallationRevokesLaterPersonHops(t *testing.T) { - for _, path := range []string{"binding", "child", "later module", "delegation"} { - t.Run(path, func(t *testing.T) { - k, _ := solutionRoleKit(t, testkit.Scope("documents", "write")) - out, err := solutionPersonIssue(t, k, "maya", path, scopes(testkit.Scope("documents", "write"))) - if err != nil { - t.Fatal(err) - } - if err := k.State.Revoke(state.InstallationID("acme", "wiki")); err != nil { - t.Fatal(err) - } - if _, err := k.Issuer.Check(context.Background(), out.Token, out.Context.Audience, ""); err == nil { - t.Fatal("person work survived source solution uninstall") - } - if _, err := k.Issuer.ListResources(context.Background(), out.Token, out.Context.Audience, "documents", "write"); err == nil { - t.Fatal("resource read survived source solution uninstall") - } - }) - } -} - -func TestSolutionRoleResourcesSurviveLaterHopsAndRevocation(t *testing.T) { - for _, path := range []string{"child", "later module", "delegation"} { - t.Run(path, func(t *testing.T) { - held := testkit.Scope("documents", "write") - held.ResourceIds = []string{"one"} - k, m := solutionRoleKit(t, held) - var issued *authorityv1.IssueResponse - for _, tc := range []struct { - ids []string - allowed bool - }{{[]string{"one"}, true}, {[]string{"two"}, false}, {nil, false}} { - out, err := solutionPersonIssue(t, k, "maya", path, []*authorityv1.WorkScopeV1{{ResourceKind: "documents", Actions: []string{"write"}, ResourceIds: tc.ids}}) - if !tc.allowed { - if err == nil { - t.Fatalf("widened role resource IDs %v", tc.ids) - } - continue - } - if err != nil { - t.Fatal(err) - } - grants, err := k.Issuer.ListResources(context.Background(), out.Token, "documents", "documents", "write") - if err != nil { - t.Fatal(err) - } - if got := covered(t, grants); !got.Matches("one") || got.Matches("two") || got.All() { - t.Fatalf("role resources not preserved: %v", grants) - } - issued = out - } - m.Generation++ - m.Roles = maps.Clone(m.Roles) - m.Roles["viewer"] = []*basev0.WorkScopeV1{testkit.Scope("wiki.page", "read"), testkit.Scope("directory.entry", "read")} - if _, err := k.State.ApplyManifest(m); err != nil { - t.Fatal(err) - } - if _, err := k.Issuer.Check(context.Background(), issued.Token, "documents", ""); !errors.Is(err, workcontext.ErrRevoked) { - t.Fatalf("old role authority survived removal: %v", err) - } - if _, err := solutionPersonIssue(t, k, "maya", path, []*authorityv1.WorkScopeV1{{ResourceKind: "documents", Actions: []string{"write"}, ResourceIds: []string{"one"}}}); err == nil { - t.Fatal("fresh work recovered removed role") - } - }) - } -} - -func TestSolutionDelegationRechecksCurrentRoleAtUse(t *testing.T) { - for _, change := range []string{"role narrowed", "delegation revoked"} { - t.Run(change, func(t *testing.T) { - k, m := solutionRoleKit(t, testkit.Scope("documents", "write")) - ctx := person(t, k, "fixture-maya") - d, err := k.Issuer.Delegate(ctx, "wiki", []*basev0.WorkScopeV1{testkit.Scope("documents", "write")}, "use-time validation") - if err != nil { - t.Fatal(err) - } - req := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Delegation{Delegation: &authorityv1.DelegationRef{DelegationId: d.ID}}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "wiki")}, Audience: "documents", OperationBindingId: "documents", Scopes: scopes(testkit.Scope("documents", "write"))} - before, err := k.Issuer.Issue(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if change == "role narrowed" { - m.Generation++ - m.Roles = maps.Clone(m.Roles) - m.Roles["viewer"] = []*basev0.WorkScopeV1{testkit.Scope("wiki.page", "read"), testkit.Scope("documents", "read")} - if _, err := k.State.ApplyManifest(m); err != nil { - t.Fatal(err) - } - } else if _, err := k.Issuer.RevokeDelegation(ctx, d.ID); err != nil { - t.Fatal(err) - } - if _, err := k.Issuer.Issue(context.Background(), req); err == nil { - t.Fatal("stored consent recovered write") - } - if _, err := k.Issuer.Check(context.Background(), before.Token, "documents", ""); err == nil { - t.Fatal("previously issued delegation survived revocation") - } - }) - } -} - -func TestSolutionPersonRoleChangeCannotRaceIssuance(t *testing.T) { - for _, path := range []string{"binding", "child", "later module", "delegation"} { - for _, stage := range []string{"before_resolution", "before_mint", "during_mint", "after_issue"} { - t.Run(path+"/"+stage, func(t *testing.T) { - k, m := solutionRoleKit(t, testkit.Scope("documents", "write")) - ctx, req, err := solutionPersonRequest(t, k, "maya", path, scopes(testkit.Scope("documents", "write"))) - if err != nil { - t.Fatal(err) - } - issue := func() (*authorityv1.IssueResponse, error) { return k.Issuer.Issue(ctx, req) } - if _, err := issue(); err != nil { - t.Fatal(err) - } - out, err := interleaveIssuance(t, stage, func() { - m.Generation++ - m.Roles = maps.Clone(m.Roles) - m.Roles["viewer"] = []*basev0.WorkScopeV1{testkit.Scope("wiki.page", "read"), testkit.Scope("documents", "read"), testkit.Scope("directory.entry", "read")} - if _, err := k.State.ApplyManifest(m); err != nil { - t.Fatal(err) - } - }, issue) - if err != nil { - return - } - if _, err := k.Issuer.Check(context.Background(), out.Token, out.Context.Audience, ""); err == nil { - t.Fatal("stale solution write was signed with current authorization") - } - }) - } - } -} - -func TestSolutionInstallerAndTeamRolesRemainDistinctSources(t *testing.T) { - k := testkit.New(t) - m := testkit.Proposals(2) - m.Bindings = map[string]state.Binding{"documents": {ID: "documents", Audience: "documents", InvokeScopes: []*basev0.WorkScopeV1{testkit.Scope("documents", "read", "write")}}} - m.Roles[state.InstallerRole] = append(m.Roles[state.InstallerRole], testkit.Scope("documents", "write")) - m.Roles["viewer"] = append(m.Roles["viewer"], testkit.Scope("documents", "read")) - if _, err := k.State.ApplyManifest(m); err != nil { - t.Fatal(err) - } - inst, err := k.State.Install(context.Background(), "acme", "proposals", "viewer", "alex") - if err != nil { - t.Fatal(err) - } - check := func(who string, actions ...string) { - t.Helper() - got, err := k.State.PersonSolutionScopes(who, "proposals") - if err != nil { - t.Fatal(err) - } - for _, action := range []string{"read", "write"} { - want := false - for _, expected := range actions { - want = want || action == expected - } - if state.Attenuates([]*basev0.WorkScopeV1{testkit.Scope("documents", action)}, got) != want { - t.Fatalf("%s incorrectly holds %s: %v", who, action, got) - } - } - } - check("alex", "write") - check("maya") - check("zoe") - if _, err := k.State.GrantToTeam(context.Background(), "acme", inst.ID, "finance"); err != nil { - t.Fatal(err) - } - check("alex", "read", "write") - check("maya", "read") - check("remi") - check("zoe") -} - -func TestEverySolutionInPersonChainRemainsACeiling(t *testing.T) { - for _, narrow := range []string{"wiki", "proposals"} { - t.Run(narrow, func(t *testing.T) { - k, a := solutionRoleKit(t, testkit.Scope("documents", "read", "write")) - b := testkit.Proposals(2) - b.Bindings = a.Bindings - b.Roles["viewer"] = append(b.Roles["viewer"], testkit.Scope("documents", "read", "write"), testkit.Scope("directory.entry", "read")) - if narrow == "proposals" { - b.Roles["viewer"][1] = testkit.Scope("documents", "read") - } - if _, err := k.State.ApplyManifest(b); err != nil { - t.Fatal(err) - } - a.Generation++ - a.Roles = maps.Clone(a.Roles) - a.Roles["viewer"] = []*basev0.WorkScopeV1{testkit.Scope("wiki.page", "read"), testkit.Scope("documents", "read", "write"), testkit.Scope("directory.entry", "read"), testkit.Scope("proposals.item", "read")} - if narrow == "wiki" { - a.Roles["viewer"][1] = testkit.Scope("documents", "read") - } - a.Bindings = maps.Clone(a.Bindings) - a.Bindings["tools"] = state.Binding{ID: "tools", Audience: "proposals", InvokeScopes: []*basev0.WorkScopeV1{testkit.Scope("proposals.item", "read")}} - if _, err := k.State.ApplyManifest(a); err != nil { - t.Fatal(err) - } - inst, err := k.State.Install(context.Background(), "acme", "proposals", "viewer", "alex") - if err != nil { - t.Fatal(err) - } - if _, err := k.State.GrantToTeam(context.Background(), "acme", inst.ID, "finance"); err != nil { - t.Fatal(err) - } - root, err := k.Issuer.Issue(person(t, k, "fixture-maya"), &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "wiki"}) - if err != nil { - t.Fatal(err) - } - hop := func(parent *authorityv1.IssueResponse, actor, binding, target, kind, action string) (*authorityv1.IssueResponse, error) { - return k.Issuer.Issue(context.Background(), &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_ParentToken{ParentToken: parent.Token}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, actor)}, OperationBindingId: binding, Audience: target, Scopes: scopes(testkit.Scope(kind, action))}) - } - tools, err := hop(root, "wiki", "tools", "proposals", "proposals.item", "read") - if err != nil { - t.Fatal(err) - } - later, err := hop(tools, "proposals", "directory", "org-directory", "directory.entry", "read") - if err != nil { - t.Fatal(err) - } - for _, parent := range []*authorityv1.IssueResponse{tools, later} { - actor := parent.Context.Audience - read, err := hop(parent, actor, "documents", "documents", "documents", "read") - if err != nil { - t.Fatal(err) - } - if _, err := k.Issuer.Check(context.Background(), read.Token, "documents", ""); err != nil { - t.Fatal(err) - } - if _, err := hop(parent, actor, "documents", "documents", "documents", "write"); !errors.Is(err, issuer.ErrRefused) { - t.Fatalf("%s recovered write beyond %s: %v", actor, narrow, err) - } - } - }) - } -} diff --git a/modules/authority/services/local/code/pkg/business/issuer/solution_tools_test.go b/modules/authority/services/local/code/pkg/business/issuer/solution_tools_test.go deleted file mode 100644 index 26fbefde..00000000 --- a/modules/authority/services/local/code/pkg/business/issuer/solution_tools_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package issuer_test - -import ( - "context" - "errors" - "gateway/pkg/business/issuer" - "gateway/pkg/business/state" - "gateway/pkg/business/testkit" - authorityv1 "gateway/pkg/gen/host/authority/v1" - "google.golang.org/protobuf/proto" - "testing" -) - -// Rows 5 and 15: the binding contributes the person's solution-role scope; -// the module remains the actor and never becomes the owner. -func TestModuleToolBindingUsesThePersonsSolutionEntitlement(t *testing.T) { - for _, bindRoot := range []bool{false, true} { - for _, who := range []string{"maya", "remi"} { - t.Run(who+map[bool]string{false: "/child", true: "/binding-root"}[bindRoot], func(t *testing.T) { - k := testkit.New(t) - testkit.SolutionTools(t, k) - request := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "robin"} - if bindRoot { - request.OperationBindingId = testkit.SolutionToolBinding - } - root, err := k.Issuer.Issue(person(t, k, "fixture-"+who), request) - if bindRoot && who == "remi" { - if !errors.Is(err, issuer.ErrRefused) { - t.Fatalf("person without target entitlement obtained binding scopes: %v", err) - } - return - } - if err != nil { - t.Fatal(err) - } - read := scopes(testkit.Scope("proposals.item", "read")) - if bindRoot { - found := false - for _, sc := range root.Context.AuthorityScopes { - if proto.Equal(sc, read[0]) { - found = true - } - } - if !found { - t.Fatalf("person binding omitted its scopes: %v", root.Context.AuthorityScopes) - } - } - child, err := k.Issuer.Issue(context.Background(), &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_ParentToken{ParentToken: root.Token}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "robin")}, OperationBindingId: testkit.SolutionToolBinding, Audience: "proposals", Scopes: read}) - if who == "remi" { - if !errors.Is(err, issuer.ErrRefused) { - t.Fatalf("unentitled child: %v", err) - } - return - } - if err != nil { - t.Fatal(err) - } - wc := child.Context - if wc.OwnerPrincipalId != who || wc.GetOwnerPrincipalKind() != "human" || len(wc.ActorChain) != 1 || wc.ActorChain[0].PrincipalId != "robin" { - t.Fatalf("owner/actor changed: %v", wc) - } - if _, err := k.Issuer.Check(context.Background(), child.Token, "proposals", testkit.SolutionToolMethod); err != nil { - t.Fatalf("real method check: %v", err) - } - }) - } - } -} - -// Row 12: knowing an installation ID, or holding a person's session, is not -// proof of being the installed solution's workload. -func TestInstallationIssueRequiresTheSolutionsWorkload(t *testing.T) { - k := testkit.New(t) - testkit.SolutionTools(t, k) - for _, tc := range []struct { - name, credential string - session, allowed bool - }{ - {name: "missing"}, {name: "forged", credential: "not-a-credential"}, - {name: "other module", credential: k.Credential(t, "robin")}, - {name: "other solution", credential: k.Credential(t, "proposals")}, - {name: "signed-in installer", session: true}, - {name: "own solution", credential: k.Credential(t, "wiki"), allowed: true}, - } { - t.Run(tc.name, func(t *testing.T) { - ctx := context.Background() - if tc.session { - ctx = person(t, k, "fixture-alex") - } - out, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: state.InstallationID("acme", "wiki")}}, Actor: &authorityv1.WorkloadCredential{Credential: tc.credential}, Audience: "wiki"}) - if !tc.allowed { - if err == nil { - t.Fatal("installation issuance accepted an unauthenticated or unrelated caller") - } - return - } - if err != nil { - t.Fatal(err) - } - if out.Context.OwnerPrincipalId != state.InstallationID("acme", "wiki")+"/agent" { - t.Fatalf("wrong installation owner: %v", out.Context) - } - if _, err := k.Issuer.Check(ctx, out.Token, "wiki", ""); err != nil { - t.Fatal(err) - } - }) - } -} - -func TestUnentitledPersonCannotHopToSolutionWithAnEmptyAsk(t *testing.T) { - k := testkit.New(t) - testkit.SolutionTools(t, k) - root, err := k.Issuer.Issue(person(t, k, "fixture-remi"), &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "robin"}) - if err != nil { - t.Fatal(err) - } - _, err = k.Issuer.Issue(context.Background(), &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_ParentToken{ParentToken: root.Token}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "robin")}, OperationBindingId: testkit.SolutionToolBinding, Audience: "proposals"}) - if !errors.Is(err, issuer.ErrRefused) { - t.Fatalf("unentitled empty-ask child issued: %v", err) - } -} - -func TestInstallationWorkloadDeliveryCannotChangeDuringIssuance(t *testing.T) { - for _, stage := range []string{"before_mint", "during_mint"} { - t.Run(stage, func(t *testing.T) { - k := testkit.New(t) - m := installationManifest("conformance-solution", "viewer", 1, "read") - if _, err := k.State.ApplyManifest(m); err != nil { - t.Fatal(err) - } - inst, err := k.State.Install(context.Background(), "acme", m.Module, "viewer", "alex") - if err != nil { - t.Fatal(err) - } - req := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: inst.ID}}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, m.Module)}, Audience: m.Principal} - _, err = interleaveIssuance(t, stage, func() { - m.Generation++ - if _, err := k.State.ApplyManifest(m); err != nil { - t.Fatal(err) - } - }, func() (*authorityv1.IssueResponse, error) { return k.Issuer.Issue(context.Background(), req) }) - if !errors.Is(err, issuer.ErrRefused) { - t.Fatalf("installation minted across its authenticated workload generation: %v", err) - } - }) - } -} diff --git a/modules/authority/services/local/code/pkg/business/state/approvals.go b/modules/authority/services/local/code/pkg/business/state/approvals.go index ccd7b3b7..5a356dd1 100644 --- a/modules/authority/services/local/code/pkg/business/state/approvals.go +++ b/modules/authority/services/local/code/pkg/business/state/approvals.go @@ -5,7 +5,6 @@ import ( "fmt" "sync" - basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" "github.com/google/uuid" ) @@ -106,8 +105,3 @@ func (s *MemoryApprovals) find(installation, policy, digest string) (Approval, b } return Approval{}, false } - -// HoldsAction is whether held carries action on kind. -func HoldsAction(held []*basev0.WorkScopeV1, kind, action string) bool { - return holdsAction(held, kind, action) -} diff --git a/modules/authority/services/local/code/pkg/business/state/composition.go b/modules/authority/services/local/code/pkg/business/state/composition.go index 0e98caf2..e37f6df6 100644 --- a/modules/authority/services/local/code/pkg/business/state/composition.go +++ b/modules/authority/services/local/code/pkg/business/state/composition.go @@ -67,6 +67,7 @@ func (s *State) ApplyComposition(ctx context.Context, data []byte) ([]string, er for key, role := range roles { s.roles[key] = role } + s.refreshRolesLocked() s.mu.Unlock() if len(doc.Installations) > 0 || len(doc.Modules) > 0 { setup, err := yaml.Marshal(fixtureFile{Installations: doc.Installations, Modules: doc.Modules}) @@ -186,15 +187,16 @@ func (s *State) checkComposedSetup(doc compositionFile) ([]string, error) { var lines []string for _, i := range doc.Installations { owner, ok := s.people[i.Owner] + _, known := s.orgs[i.Organization] switch { case i.Target == "" || i.Role == "": return nil, fmt.Errorf("an installation names no target or no role") - case s.orgs[i.Organization] == "": + case !known: return nil, fmt.Errorf("installation of %s: %q is not an organisation the fixture defines", i.Target, i.Organization) case !ok || owner.Organization != i.Organization: return nil, fmt.Errorf("installation of %s: %q is not a person of %s in the fixture", i.Target, i.Owner, i.Organization) } - lines = append(lines, fmt.Sprintf("installs %s at %s as %s, for %v", i.Target, i.Organization, i.Role, i.Teams)) + lines = append(lines, fmt.Sprintf("installs %s at %s as %s, granting %s the installer role and %v the role %s", i.Target, i.Organization, i.Role, i.Owner, i.Teams, i.Role)) } for _, m := range doc.Modules { if !kindGrammar.MatchString(m.Module) || len(m.Kinds) == 0 { diff --git a/modules/authority/services/local/code/pkg/business/state/composition_test.go b/modules/authority/services/local/code/pkg/business/state/composition_test.go index e202c25a..d0e01042 100644 --- a/modules/authority/services/local/code/pkg/business/state/composition_test.go +++ b/modules/authority/services/local/code/pkg/business/state/composition_test.go @@ -26,7 +26,7 @@ func fixtureOnly(t *testing.T) *testkit.Kit { if err := k.State.LoadFixture(context.Background(), data); err != nil { t.Fatal(err) } - ledger := state.Manifest{Module: "ledger", Principal: "ledger", Kind: "module", Generation: 1, + ledger := state.Manifest{Name: "ledger", Principal: "ledger", Kind: "module", Generation: 1, Methods: map[string]state.MethodPolicy{ "/ledger/Read": {Requires: []*basev0.WorkScopeV1{testkit.Scope("ledger.entry", "read")}}, "/ledger/Write": {Requires: []*basev0.WorkScopeV1{testkit.Scope("ledger.entry", "write")}}, @@ -43,7 +43,7 @@ func fixtureOnly(t *testing.T) *testkit.Kit { func TestCompositionGrantsFixtureRolesWhatItsModuleServes(t *testing.T) { k := fixtureOnly(t) for _, person := range []string{"maya", "remi", "alex", "zoe"} { - if held, _ := k.State.PersonScopes(person, "ledger"); len(held) != 0 { + if held, _ := k.PersonScopes(t, person, "ledger"); len(held) != 0 { t.Fatalf("the fixture alone grants %s %v at ledger", person, held) } } @@ -71,18 +71,18 @@ roles: "remi": testkit.Scope("ledger.entry", "read", "write"), "alex": testkit.Scope("ledger.entry", "approve"), } { - held, err := k.State.PersonScopes(person, "ledger") + held, err := k.PersonScopes(t, person, "ledger") if err != nil || !state.Attenuates([]*basev0.WorkScopeV1{scope}, held) { t.Errorf("%s holds %v at ledger (%v), want %v", person, held, err, scope) } } - if held, _ := k.State.PersonScopes("alex", "ledger"); state.Attenuates([]*basev0.WorkScopeV1{testkit.Scope("ledger.entry", "write")}, held) { + if held, _ := k.PersonScopes(t, "alex", "ledger"); state.Attenuates([]*basev0.WorkScopeV1{testkit.Scope("ledger.entry", "write")}, held) { t.Error("a grant to the editor reached the admin") } if err := k.State.AdmitMethod("ledger", "/ledger/Approve", mustHeld(t, k, "remi")); err == nil { t.Error("the editor was admitted to the admin's method") } - grants, err := k.State.ResourceGrants("maya", "ledger", "ledger.entry", "read") + grants, err := k.ResourceGrants(t, "maya", "ledger", "ledger.entry", "read") if err != nil || len(grants) != 1 || grants[0].Prefix != "public-" { t.Errorf("the viewer's grant keeps its resources: %v %v", grants, err) } @@ -94,7 +94,7 @@ roles: func mustHeld(t *testing.T, k *testkit.Kit, person string) []*basev0.WorkScopeV1 { t.Helper() - held, err := k.State.PersonScopes(person, "ledger") + held, err := k.PersonScopes(t, person, "ledger") if err != nil { t.Fatal(err) } diff --git a/modules/authority/services/local/code/pkg/business/state/delegation_fixture_test.go b/modules/authority/services/local/code/pkg/business/state/delegation_fixture_test.go index 26ce8df6..19e1d475 100644 --- a/modules/authority/services/local/code/pkg/business/state/delegation_fixture_test.go +++ b/modules/authority/services/local/code/pkg/business/state/delegation_fixture_test.go @@ -6,6 +6,8 @@ import ( "strings" "testing" + "github.com/codefly-dev/authority/libraries/authority-core/go/authority" + "gateway/pkg/business/testkit" ) @@ -30,11 +32,11 @@ func TestDelegationFixtureRetainsExactIDsAndRefusesRoleMatchers(t *testing.T) { if err != nil { t.Fatal(err) } - d, err := k.State.DelegationByID("narrow") + d, err := k.State.Service().Delegation(context.Background(), "narrow", nil, &authority.Workload{Module: "org-directory"}) if err != nil { t.Fatal(err) } - scopes := d.WorkScopes() + scopes := d.Scopes if len(scopes) != 1 || !slices.Equal(scopes[0].GetResourceIds(), []string{"a"}) { t.Fatalf("fixture restriction lost: %v", scopes) } diff --git a/modules/authority/services/local/code/pkg/business/state/delivery.go b/modules/authority/services/local/code/pkg/business/state/delivery.go index 5ba16163..72506ea3 100644 --- a/modules/authority/services/local/code/pkg/business/state/delivery.go +++ b/modules/authority/services/local/code/pkg/business/state/delivery.go @@ -20,9 +20,9 @@ type appliedDelivery struct { completion chan struct{} } -// pendingInstallation follows one fixture intent across failed callbacks and -// superseding deliveries. Its fields are guarded by State.mu; revision fences -// an explicit uninstall/reinstall and done prevents replay of completed grants. +// pendingInstallation is one fixture installation waiting on a delivery: +// revision is what its first attempt took, done prevents its replay once +// performed. Guarded by State.mu. type pendingInstallation struct { FixtureInstallation revision uint64 @@ -91,11 +91,13 @@ func (s *State) admitDeliveryLocked(coordinate string, carrier *solutionhost.Del if candidate.Removed { // An unknown tombstone must not erase an unrelated transitional sketch. if previous == nil { - if _, exists := s.modules[candidate.Alias]; exists { + if _, exists := s.Module(candidate.Alias); exists { return nil, fmt.Errorf("%w: withdrawal has no admitted binding for %s", ErrForbidden, candidate.Alias) } } - s.withdrawLocked(candidate.Alias) + if err := s.withdrawLocked(candidate.Alias); err != nil { + return nil, err + } } else { ids, pending, err := s.applyManifestLocked(m) if err != nil { @@ -142,7 +144,7 @@ func (s *State) finishDelivery(ctx context.Context, manifest *solutionhost.Modul return nil } -func (s *State) finishFixtureInstalls(ctx context.Context, manifest *solutionhost.ModuleManifest, pending []*pendingInstallation) error { +func (s *State) finishFixtureInstalls(ctx context.Context, _ *solutionhost.ModuleManifest, pending []*pendingInstallation) error { for _, i := range pending { if err := ctx.Err(); err != nil { return err @@ -153,53 +155,25 @@ func (s *State) finishFixtureInstalls(ctx context.Context, manifest *solutionhos if done { continue } - inst, err := s.install(ctx, i.Organization, i.Target, i.Role, i.Owner, manifest.Generation, i) - if err != nil { - return fmt.Errorf("fixture installation of %s: %w", i.Target, err) + if err := s.performInstall(ctx, i.FixtureInstallation, i); err != nil { + return err } s.mu.Lock() - current, live := s.modules[i.Target] - held := s.installations[inst.ID] - if !live || current.Generation != manifest.Generation || held.Status != Active || held.Revision != inst.Revision { - s.mu.Unlock() - return fmt.Errorf("%w: fixture installation changed before team grants", ErrStale) - } - for _, team := range i.Teams { - if !slices.Contains(held.Teams, team) { - held.Teams = append(held.Teams, team) - held.Revision++ - s.revisions[i.Organization]++ - } - } - s.installations[inst.ID] = held i.done = true s.mu.Unlock() } return nil } -func (s *State) withdrawLocked(alias string) { - if previous, ok := s.modules[alias]; ok { - delete(s.principals, previous.Principal) - } - delete(s.modules, alias) +// withdrawLocked removes a module through the core: its installations are +// revoked and the delegations granted to it end. Caller holds mu. +func (s *State) withdrawLocked(alias string) error { s.takePendingInstalls(alias) - for id, installation := range s.installations { - if installation.Target != alias || installation.Status == Revoked { - continue - } - installation.Status = Revoked - installation.Revision++ - s.installations[id] = installation - s.revisions[installation.Organization]++ - } - for id, delegation := range s.delegations { - if delegation.Module != alias || delegation.Revoked { - continue + if _, ok := s.Module(alias); ok { + if err := s.svc.WithdrawModule(context.Background(), alias); err != nil { + return err } - delegation.Revoked = true - s.delegations[id] = delegation - s.revisions[delegation.Organization]++ } - s.refreshDeveloperRoles() + s.refreshRolesLocked() + return nil } diff --git a/modules/authority/services/local/code/pkg/business/state/installation_access.go b/modules/authority/services/local/code/pkg/business/state/installation_access.go deleted file mode 100644 index 8cdd7d65..00000000 --- a/modules/authority/services/local/code/pkg/business/state/installation_access.go +++ /dev/null @@ -1,34 +0,0 @@ -package state - -import ( - "fmt" - "slices" -) - -// RequireAdministrator authenticates the session and reads the person's current -// organization roles, rather than trusting the roles captured at sign-in. -func (s *State) RequireAdministrator(session, org string) error { - s.mu.RLock() - defer s.mu.RUnlock() - sess, ok := s.sessions[session] - if !ok { - return fmt.Errorf("%w: an open session is required", ErrUnauthenticated) - } - person, ok := s.people[sess.Person] - if !ok || person.Organization != org || (!slices.Contains(person.Roles, "admin") && !slices.Contains(person.Roles, "owner")) { - return fmt.Errorf("%w: installation administration requires an administrator or owner of organization %q", ErrForbidden, org) - } - return nil -} - -// RequireInstallation distinguishes an uninstalled audience from a person who -// holds nothing there. The issuer fences this read with authorization revision. -func (s *State) RequireInstallation(org, module, audience string) error { - s.mu.RLock() - defer s.mu.RUnlock() - inst, ok := s.installations[InstallationID(org, module)] - if !ok || inst.Status != Active { - return fmt.Errorf("%w: audience %q is not installed in this organization, so there is no installation to seal the capability to", ErrNotInstalled, audience) - } - return nil -} diff --git a/modules/authority/services/local/code/pkg/business/state/installation_seal_test.go b/modules/authority/services/local/code/pkg/business/state/installation_seal_test.go deleted file mode 100644 index 7954f0e6..00000000 --- a/modules/authority/services/local/code/pkg/business/state/installation_seal_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package state_test - -import ( - "context" - "errors" - "testing" - - "gateway/pkg/business/state" - "gateway/pkg/business/testkit" - "github.com/codefly-dev/core/workcontext" -) - -func TestInstallationAgentSealIsBoundedToLiveSameOrganizationModules(t *testing.T) { - for _, tc := range []struct { - name, source, org, target string - revoke string - allowed bool - }{ - {name: "same organization module", source: "wiki", org: "acme", target: "documents", allowed: true}, - {name: "own installation", source: "wiki", org: "acme", target: "wiki", allowed: true}, - {name: "foreign organization", source: "wiki", org: "globex", target: "documents"}, - {name: "unknown agent", source: "missing", org: "acme", target: "documents"}, - {name: "module is not a solution agent", source: "org-directory", org: "acme", target: "documents"}, - {name: "revoked source", source: "wiki", org: "acme", target: "documents", revoke: "wiki"}, - {name: "pending source", source: "wiki", org: "acme", target: "documents", revoke: "wiki"}, - {name: "revoked module", source: "wiki", org: "acme", target: "documents", revoke: "documents"}, - {name: "another solution", source: "wiki", org: "acme", target: "proposals"}, - } { - t.Run(tc.name, func(t *testing.T) { - k := testkit.New(t) - if tc.target == "proposals" { - if _, err := k.State.ApplyManifest(testkit.Proposals(1)); err != nil { - t.Fatal(err) - } - if _, err := k.State.Install(context.Background(), "acme", "proposals", "viewer", "alex"); err != nil { - t.Fatal(err) - } - } - if tc.revoke != "" { - if err := k.State.Revoke(state.InstallationID("acme", tc.revoke)); err != nil { - t.Fatal(err) - } - } - if tc.name == "pending source" { - k.State.OnInstall(func(context.Context, state.Installation) error { return errors.New("not ready") }) - inst, err := k.State.Install(context.Background(), "acme", "wiki", "viewer", "alex") - if err == nil || inst.Status != state.Pending { - t.Fatalf("pending source precondition: %+v %v", inst, err) - } - } - target := state.InstallationID(tc.org, tc.target) - got, err := k.State.Seal(context.Background(), state.InstallationID("acme", tc.source)+"/agent", target) - if tc.allowed { - if err != nil || got.InstallationID != target || got.InstallationRevision == 0 { - t.Fatalf("active installation association: %+v %v", got, err) - } - } else if !errors.Is(err, workcontext.ErrNoSeal) { - t.Fatalf("unrelated installation association: %+v %v", got, err) - } - }) - } -} diff --git a/modules/authority/services/local/code/pkg/business/state/permissions.go b/modules/authority/services/local/code/pkg/business/state/permissions.go index 41eb55e7..4de020c1 100644 --- a/modules/authority/services/local/code/pkg/business/state/permissions.go +++ b/modules/authority/services/local/code/pkg/business/state/permissions.go @@ -5,49 +5,82 @@ import ( "slices" "sort" + "github.com/codefly-dev/authority/libraries/authority-core/go/authority" + "github.com/codefly-dev/authority/libraries/authority-core/go/decide" + "github.com/codefly-dev/authority/libraries/authority-core/go/domain" + resources "github.com/codefly-dev/authority/libraries/resources/go" basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" ) -// refreshDeveloperRoles replaces, never accumulates, the local fixture's -// generated union. It does not assign roles to people or change business roles. -// Caller holds mu. Explicit non-method ceilings remain supported. -func (s *State) refreshDeveloperRoles() { +// refreshRolesLocked recomputes the generated developer role from the +// current delivered catalog (replaced, never accumulated) and writes every +// organisation role to the core's store as authored. Caller holds mu. +func (s *State) refreshRolesLocked() { union := s.permissionCatalog() for key, role := range s.roles { - if !role.Generated { - continue + if role.Generated { + role.Scopes = nil + for _, sc := range union { + role.Scopes = append(role.Scopes, Scope{Kind: sc.GetResourceKind(), Actions: slices.Clone(sc.GetActions())}) + } + s.roles[key] = role } - role.Scopes = nil - for _, sc := range union { - role.Scopes = append(role.Scopes, Scope{Kind: sc.GetResourceKind(), Actions: append([]string(nil), sc.GetActions()...)}) + stored := domain.OrgRole{Organization: role.Organization, Name: role.Name} + for _, sc := range role.Scopes { + grants := make([]resources.Grant, 0, len(sc.Resources)) + for _, m := range sc.Resources { + grants = append(grants, m.Grant()) + } + stored.Scopes = append(stored.Scopes, domain.RoleScope{Kind: sc.Kind, Actions: slices.Clone(sc.Actions), Resources: grants}) } - s.roles[key] = role + s.store.PutOrgRole(stored) } } -// permissionCatalog contains concrete declarations only. Caller holds mu. +// RoleScopes is what an organisation role grants as authored, as scopes. +func (s *State) RoleScopes(org, role string) []*basev0.WorkScopeV1 { + s.mu.RLock() + defer s.mu.RUnlock() + var out []*basev0.WorkScopeV1 + for _, sc := range s.roles[org+"/"+role].Scopes { + out = append(out, &basev0.WorkScopeV1{ResourceKind: sc.Kind, Actions: slices.Clone(sc.Actions)}) + } + return Normalize(out) +} + +func (s *State) modules() []Manifest { + var out []Manifest + _ = s.read(func(tx authority.Tx) error { + var err error + out, err = tx.Modules() + return err + }) + return out +} + +// permissionCatalog is every concrete (kind, action) a delivered module serves. func (s *State) permissionCatalog() []*basev0.WorkScopeV1 { var union []*basev0.WorkScopeV1 - for _, m := range s.modules { - if m.Kind != "solution" { - for _, sc := range m.ResourceKinds { - if sc.GetResourceKind() == AnyKind { - continue - } - var actions []string - for _, action := range sc.GetActions() { - if action != AnyAction { - actions = append(actions, action) - } - } - if len(actions) > 0 { - union = append(union, &basev0.WorkScopeV1{ResourceKind: sc.GetResourceKind(), Actions: actions}) + for _, m := range s.modules() { + if m.Kind == domain.KindSolution { + continue + } + for _, sc := range m.ResourceKinds { + if sc.GetResourceKind() == AnyKind { + continue + } + var actions []string + for _, action := range sc.GetActions() { + if action != AnyAction { + actions = append(actions, action) } } + if len(actions) > 0 { + union = append(union, &basev0.WorkScopeV1{ResourceKind: sc.GetResourceKind(), Actions: actions}) + } } } - union = Normalize(union) - return union + return Normalize(union) } // PermissionReport describes a snapshot of delivered generations. Coverage @@ -57,18 +90,22 @@ func (s *State) PermissionReport() []string { s.mu.RLock() defer s.mu.RUnlock() var lines []string + modules := s.modules() catalog := s.permissionCatalog() kinds := map[string]bool{} - for _, m := range s.modules { - if m.Kind == "solution" { + for _, m := range modules { + if m.Kind == domain.KindSolution { continue } for _, sc := range m.ResourceKinds { kinds[sc.GetResourceKind()] = true } } + var orgRoles []domain.OrgRole for key, role := range s.roles { + stored := domain.OrgRole{Organization: role.Organization, Name: key} for _, sc := range role.Scopes { + stored.Scopes = append(stored.Scopes, domain.RoleScope{Kind: sc.Kind, Actions: sc.Actions}) if sc.Kind == AnyKind { continue } @@ -78,39 +115,30 @@ func (s *State) PermissionReport() []string { } if !kinds[sc.Kind] { lines = append(lines, fmt.Sprintf("WARN role %s: %s:%s unresolved in current delivery", key, sc.Kind, action)) - } else if !holdsAction(catalog, sc.Kind, action) { + } else if !HoldsAction(catalog, sc.Kind, action) { lines = append(lines, fmt.Sprintf("WARN role %s: %s:%s is not declared by current delivery", key, sc.Kind, action)) } } } + orgRoles = append(orgRoles, stored) } - for _, m := range s.modules { + for _, m := range modules { if len(m.Methods) == 0 { - lines = append(lines, fmt.Sprintf("WARN %s@%d: no delivered method policies; declared ceilings alone do not admit a call", m.Module, m.Generation)) + lines = append(lines, fmt.Sprintf("WARN %s@%d: no delivered method policies; declared ceilings alone do not admit a call", m.Name, m.Generation)) } for _, sc := range m.ResourceKinds { if sc.GetResourceKind() == AnyKind || slices.Contains(sc.GetActions(), AnyAction) { - lines = append(lines, fmt.Sprintf("WARN %s@%d %s: wildcard ceiling is unresolved; generated developer uses explicit actions only", m.Module, m.Generation, sc.GetResourceKind())) + lines = append(lines, fmt.Sprintf("WARN %s@%d %s: wildcard ceiling is unresolved; generated developer uses explicit actions only", m.Name, m.Generation, sc.GetResourceKind())) } } roles := map[string][]*basev0.WorkScopeV1{} - if m.Kind == "solution" { + if m.Kind == domain.KindSolution { for name, scopes := range m.Roles { roles["installation/"+name] = scopes } } else { - for key, role := range s.roles { - var scopes []*basev0.WorkScopeV1 - for _, sc := range role.Scopes { - if sc.Kind == AnyKind { - for _, kind := range m.ResourceKinds { - scopes = append(scopes, &basev0.WorkScopeV1{ResourceKind: kind.GetResourceKind(), Actions: sc.Actions}) - } - } else { - scopes = append(scopes, &basev0.WorkScopeV1{ResourceKind: sc.Kind, Actions: sc.Actions}) - } - } - roles[key] = Intersect(Normalize(scopes), m.ResourceKinds) + for _, role := range orgRoles { + roles[role.Name] = decide.OrgScopes([]domain.OrgRole{role}, m) } } for method := range m.Methods { @@ -122,10 +150,10 @@ func (s *State) PermissionReport() []string { } else { covered = true } - lines = append(lines, fmt.Sprintf("role %s × %s@%d × %s: %s", role, m.Module, m.Generation, method, result)) + lines = append(lines, fmt.Sprintf("role %s × %s@%d × %s: %s", role, m.Name, m.Generation, method, result)) } if !covered { - lines = append(lines, fmt.Sprintf("WARN %s@%d %s: no single declared role covers this method", m.Module, m.Generation, method)) + lines = append(lines, fmt.Sprintf("WARN %s@%d %s: no single declared role covers this method", m.Name, m.Generation, method)) } } } diff --git a/modules/authority/services/local/code/pkg/business/state/permissions_test.go b/modules/authority/services/local/code/pkg/business/state/permissions_test.go index 80d019f9..925d909f 100644 --- a/modules/authority/services/local/code/pkg/business/state/permissions_test.go +++ b/modules/authority/services/local/code/pkg/business/state/permissions_test.go @@ -11,7 +11,7 @@ import ( func TestGeneratedDeveloperTracksReplacementAndReportsBusinessGaps(t *testing.T) { k := testkit.New(t) - m := state.Manifest{Module: "ledger", Principal: "ledger", Kind: "module", Generation: 1, + m := state.Manifest{Name: "ledger", Principal: "ledger", Kind: "module", Generation: 1, Methods: map[string]state.MethodPolicy{ "/ledger/Write": {Requires: []*basev0.WorkScopeV1{testkit.Scope("ledger.entry", "write")}}, "/ledger/Version": {AnyCheckedCaller: true}, @@ -21,7 +21,7 @@ func TestGeneratedDeveloperTracksReplacementAndReportsBusinessGaps(t *testing.T) if _, err := k.State.ApplyManifest(m); err != nil { t.Fatal(err) } - held, err := k.State.PersonScopes("dev", "ledger") + held, err := k.PersonScopes(t, "dev", "ledger") if err != nil || !state.Attenuates([]*basev0.WorkScopeV1{testkit.Scope("ledger.entry", "write")}, held) { t.Fatalf("developer: %v %v", held, err) } @@ -36,7 +36,7 @@ func TestGeneratedDeveloperTracksReplacementAndReportsBusinessGaps(t *testing.T) if _, err := k.State.ApplyManifest(m); err != nil { t.Fatal(err) } - held, err = k.State.PersonScopes("dev", "ledger") + held, err = k.PersonScopes(t, "dev", "ledger") if err != nil || state.Attenuates([]*basev0.WorkScopeV1{testkit.Scope("ledger.entry", "write")}, held) { t.Fatalf("removed write retained: %v %v", held, err) } @@ -56,7 +56,7 @@ func TestGeneratedDeveloperTracksReplacementAndReportsBusinessGaps(t *testing.T) func TestGeneratedDeveloperNeverCopiesWildcardCarrierCeilings(t *testing.T) { k := testkit.New(t) - m := state.Manifest{Module: "unresolved", Principal: "unresolved", Kind: "module", Generation: 1, + m := state.Manifest{Name: "unresolved", Principal: "unresolved", Kind: "module", Generation: 1, ResourceKinds: []*basev0.WorkScopeV1{testkit.Scope("unresolved.entry", "*")}, Methods: map[string]state.MethodPolicy{"/unresolved/Read": {Requires: []*basev0.WorkScopeV1{testkit.Scope("unresolved.entry", "read")}}}} if _, err := k.State.ApplyManifest(m); err != nil { diff --git a/modules/authority/services/local/code/pkg/business/state/restricted_method_test.go b/modules/authority/services/local/code/pkg/business/state/restricted_method_test.go index 4806b885..c82aa98e 100644 --- a/modules/authority/services/local/code/pkg/business/state/restricted_method_test.go +++ b/modules/authority/services/local/code/pkg/business/state/restricted_method_test.go @@ -12,7 +12,7 @@ import ( func TestMethodAdmissionNeverWidensResourceRestrictedScopes(t *testing.T) { k := testkit.New(t) - m := state.Manifest{Module: "gauge", Principal: "gauge", Kind: "module", Generation: 1, + m := state.Manifest{Name: "gauge", Principal: "gauge", Kind: "module", Generation: 1, Methods: map[string]state.MethodPolicy{ "/gauge.Service/List": reads("gauge.items"), "/gauge.Service/Edit": {Requires: []*basev0.WorkScopeV1{scope("gauge.items", nil, "read", "write")}}, diff --git a/modules/authority/services/local/code/pkg/business/state/route_test.go b/modules/authority/services/local/code/pkg/business/state/route_test.go index ae708f25..1c91f423 100644 --- a/modules/authority/services/local/code/pkg/business/state/route_test.go +++ b/modules/authority/services/local/code/pkg/business/state/route_test.go @@ -21,7 +21,7 @@ func reads(kind string) state.MethodPolicy { // served, and two matching routes are refused rather than guessed between. func TestAnHTTPRequestIsAdmittedUnderTheRouteItsPathMatches(t *testing.T) { k := testkit.New(t) - m := state.Manifest{Module: "gauge", Principal: "gauge", Kind: "module", Generation: 1, Bindings: map[string]state.Binding{}, + m := state.Manifest{Name: "gauge", Principal: "gauge", Kind: "module", Generation: 1, Bindings: map[string]state.Binding{}, Methods: map[string]state.MethodPolicy{ "GET /v1/gauge/profiles": reads("gauge.profiles"), "GET /v1/gauge/profiles/{ref}": reads("gauge.profiles"), @@ -64,11 +64,11 @@ func TestAnHTTPRequestIsAdmittedUnderTheRouteItsPathMatches(t *testing.T) { // carries; a delivered module is not replaced by a stale one. func TestADeliveryReplacesTheFixturesStandIn(t *testing.T) { k := testkit.New(t) - standIn := state.Manifest{Module: "gauge", Principal: "gauge", Kind: "module", Generation: 1, Bindings: map[string]state.Binding{}, StandIn: true} + standIn := state.Manifest{Name: "gauge", Principal: "gauge", Kind: "module", Generation: 1, Bindings: map[string]state.Binding{}, StandIn: true} if _, err := k.State.ApplyManifest(standIn); err != nil { t.Fatal(err) } - delivered := state.Manifest{Module: "gauge", Principal: "gauge", Kind: "module", Generation: 1, Bindings: map[string]state.Binding{}, + delivered := state.Manifest{Name: "gauge", Principal: "gauge", Kind: "module", Generation: 1, Bindings: map[string]state.Binding{}, Methods: map[string]state.MethodPolicy{"GET /v1/gauge/profiles": reads("gauge.profiles")}} if _, err := k.State.ApplyManifest(delivered); err != nil { t.Fatalf("the module's own delivery did not replace the stand-in: %v", err) @@ -80,7 +80,7 @@ func TestADeliveryReplacesTheFixturesStandIn(t *testing.T) { func TestConcreteInventorySelectionPreservesManifestPolicy(t *testing.T) { k := testkit.New(t) - m := state.Manifest{Module: "gauge", Principal: "gauge", Kind: "module", Generation: 1, Methods: map[string]state.MethodPolicy{ + m := state.Manifest{Name: "gauge", Principal: "gauge", Kind: "module", Generation: 1, Methods: map[string]state.MethodPolicy{ "GET /things/{id}": {AnyCheckedCaller: true}, "GET /things/export": {Requires: []*basev0.WorkScopeV1{{ResourceKind: "gauge.items", Actions: []string{"write"}}}}, "GET /things/{name}": {AnyCheckedCaller: true}, diff --git a/modules/authority/services/local/code/pkg/business/state/served_test.go b/modules/authority/services/local/code/pkg/business/state/served_test.go index 07832865..a1a4df96 100644 --- a/modules/authority/services/local/code/pkg/business/state/served_test.go +++ b/modules/authority/services/local/code/pkg/business/state/served_test.go @@ -15,14 +15,14 @@ import ( // manifest's other declarations named the kind. func TestAModuleServesWhatItsMethodsRequire(t *testing.T) { k := testkit.New(t) - m := state.Manifest{Module: "ledger", Principal: "ledger", Kind: "module", Generation: 1, Bindings: map[string]state.Binding{}, + m := state.Manifest{Name: "ledger", Principal: "ledger", Kind: "module", Generation: 1, Bindings: map[string]state.Binding{}, Methods: map[string]state.MethodPolicy{ "/ledger.v1.Ledger/DefineAccount": {Requires: []*basev0.WorkScopeV1{{ResourceKind: "ledger.accounts", Actions: []string{"define"}}}}, }} if _, err := k.State.ApplyManifest(m); err != nil { t.Fatal(err) } - held, err := k.State.PersonScopes("dev", "ledger") + held, err := k.PersonScopes(t, "dev", "ledger") if err != nil { t.Fatal(err) } diff --git a/modules/authority/services/local/code/pkg/business/state/solution_roles.go b/modules/authority/services/local/code/pkg/business/state/solution_roles.go deleted file mode 100644 index 9dea510c..00000000 --- a/modules/authority/services/local/code/pkg/business/state/solution_roles.go +++ /dev/null @@ -1,48 +0,0 @@ -package state - -import ( - "fmt" - basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" - "slices" -) - -// PersonSolutionScopes returns the person's full effective installation roles, -// including declared consumed permissions. It never reads organization roles. -// Issuers fence this read with their existing authorization revision checks. -func (s *State) PersonSolutionScopes(person, solution string) ([]*basev0.WorkScopeV1, error) { - s.mu.RLock() - defer s.mu.RUnlock() - p, ok := s.people[person] - if !ok { - return nil, fmt.Errorf("%w: person", ErrUnknown) - } - m, err := s.moduleByPrincipal(solution) - if err != nil { - return nil, err - } - if m.Kind != "solution" { - return nil, fmt.Errorf("%w: %s is not a solution", ErrForbidden, solution) - } - return s.personSolutionScopes(p, m), nil -} - -// personSolutionScopes uses the caller's state lock. Owners hold installer; -// granted team members hold the selected role; both sources may contribute. -func (s *State) personSolutionScopes(p Person, m Manifest) []*basev0.WorkScopeV1 { - var held []*basev0.WorkScopeV1 - for _, inst := range s.entitled(p) { - if inst.Target != m.Module { - continue - } - if inst.Owner == p.ID { - held = append(held, m.Roles[InstallerRole]...) - } - for _, team := range inst.Teams { - if slices.Contains(p.Teams, team) { - held = append(held, m.Roles[inst.Role]...) - break - } - } - } - return Normalize(held) -} diff --git a/modules/authority/services/local/code/pkg/business/state/state.go b/modules/authority/services/local/code/pkg/business/state/state.go index dfefef91..534041fc 100644 --- a/modules/authority/services/local/code/pkg/business/state/state.go +++ b/modules/authority/services/local/code/pkg/business/state/state.go @@ -1,34 +1,37 @@ -// Package state is the local authority's whole memory: the fixture people and -// roles, the composition as delivered by manifests, the installations and the -// open sessions. It is the seal and revision source core's issuer and verifier -// read, so one struct answers every "is this still true" the authority asks. +// Package state is the local authority's host half: the fixture it boots on +// (organisations, roles, people and their sign-in tokens), the composition as +// delivered, the open sessions, delivery admission and method admission. What +// the authority decides and records (modules, installations, role grants, +// delegations, revisions, and every issuance) is the generic authority core's, +// over its in-memory store: this package seeds that store and asks the core. package state import ( "context" - "crypto/rand" - "encoding/hex" + "crypto/ed25519" "errors" "fmt" - delivery "github.com/codefly-dev/authority/libraries/delivery/go" - "github.com/codefly-dev/authority/libraries/hop/go/routing" - resources "github.com/codefly-dev/authority/libraries/resources/go" + "maps" "slices" - "sort" "strings" "sync" + "github.com/codefly-dev/authority/libraries/authority-core/go/authority" + "github.com/codefly-dev/authority/libraries/authority-core/go/domain" + "github.com/codefly-dev/authority/libraries/authority-core/go/memstore" + "github.com/codefly-dev/authority/libraries/authority-core/go/scope" + "github.com/codefly-dev/authority/libraries/hop/go/routing" + resources "github.com/codefly-dev/authority/libraries/resources/go" basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" - "github.com/codefly-dev/core/workcontext" "github.com/codefly-dev/core/workloadidentity/native" "github.com/google/uuid" "gopkg.in/yaml.v3" ) -// Errors a caller distinguishes. +// Errors a caller distinguishes for what this host decides itself; what the +// core decides arrives as a domain.Refusal with its code. var ( ErrUnknown = errors.New("state: unknown") - ErrNotInstalled = errors.New("state: not installed") ErrUnauthenticated = errors.New("state: unauthenticated") ErrForbidden = errors.New("state: forbidden") ErrStale = errors.New("state: generation is not newer") @@ -76,15 +79,43 @@ type Role struct { Scopes []Scope `yaml:"scopes"` } -// Binding is a module's declared call towards another module. -type Binding struct { - ID string - Audience string - InvokeScopes []*basev0.WorkScopeV1 - HeadlessScopes []*basev0.WorkScopeV1 - // LookupScopes are what a receipt lookup through the binding may read. - LookupScopes []*basev0.WorkScopeV1 -} +// The core's objects, as this host names them. +type ( + Manifest = domain.Module + MethodPolicy = domain.MethodPolicy + Binding = domain.Binding + Installation = domain.Installation + Status = domain.Status + Delegation = domain.Delegation + InstallStep = authority.InstallStep +) + +// The installation states, the reserved role and the wildcards. +const ( + Pending = domain.Pending + Active = domain.Active + Revoked = domain.Revoked + InstallerRole = domain.InstallerRole + AnyAction = scope.AnyAction + AnyKind = scope.AnyKind +) + +// The core's scope algebra, for this host's own checks. +var ( + Normalize = scope.Normalize + Intersect = scope.Intersect + Attenuates = scope.Attenuates + HoldsAction = scope.HoldsAction +) + +// InstallationID is the id of org's installation of target: a canonical UUID +// named by organisation and target, so a module's stored rows still name the +// installation after the authority restarts with its fixture. +func InstallationID(org, target string) string { return authority.StableInstallationID(org, target) } + +// Namespace is the source an installation's authored content belongs to, +// named by the host so a module never chooses it: ".". +func Namespace(i Installation) string { return i.Organization + "." + i.Target } // DelegationScope preserves the exact resource ceiling of a WorkScope. // Role matchers are a separate contract; a delegation names IDs, not patterns. @@ -94,8 +125,8 @@ type DelegationScope struct { ResourceIDs []string `yaml:"resource_ids,omitempty"` } -// Refuse role-matcher syntax instead of silently decoding a restricted grant -// as an unrestricted one. Fixture decoding otherwise permits unknown fields. +// UnmarshalYAML refuses role-matcher syntax instead of silently decoding a +// restricted grant as an unrestricted one. func (s *DelegationScope) UnmarshalYAML(node *yaml.Node) error { type plain DelegationScope var decoded struct { @@ -112,234 +143,16 @@ func (s *DelegationScope) UnmarshalYAML(node *yaml.Node) error { return nil } -// Delegation is a person's grant to a module to act for them beyond any -// session, on scopes they hold. Every capability issued from it is the -// grantor's, re-checked against what the grantor holds at the time. -type Delegation struct { +// fixtureDelegation is a delegation a fixture person has already granted. +type fixtureDelegation struct { ID string `yaml:"id"` Organization string `yaml:"organization"` Grantor string `yaml:"grantor"` Module string `yaml:"module"` Purpose string `yaml:"purpose"` Scopes []DelegationScope `yaml:"scopes"` - Revoked bool `yaml:"-"` -} - -// WorkScopes is the delegation's scopes as core spells them. -func (d Delegation) WorkScopes() []*basev0.WorkScopeV1 { - var out []*basev0.WorkScopeV1 - for _, sc := range d.Scopes { - out = append(out, &basev0.WorkScopeV1{ - ResourceKind: sc.Kind, Actions: append([]string(nil), sc.Actions...), - ResourceIds: append([]string(nil), sc.ResourceIDs...), - }) - } - return Normalize(out) -} - -// Manifest is what the render delivered for one module instance. -type Manifest struct { - Module string - Principal string - Kind string - Generation uint64 - // StandIn marks a module the fixture declares for a run that supervises it - // itself: what it serves, until the module's own delivery replaces it. - StandIn bool - Bindings map[string]Binding - ResourceKinds []*basev0.WorkScopeV1 - // Roles are what a solution declares a person may hold on an installation of - // it, each within what the solution serves or explicitly consumes through - // invoke/lookup bindings. A solution always declares - // InstallerRole; a module declares none, since a module is installed by - // composition and a person's access to it is the organisation's roles. - Roles map[string][]*basev0.WorkScopeV1 - // Methods are what each method the module serves declares a call must hold, - // from the authority's signed document delivered with the manifest. Nil - // means the delivery declared no method policy: every check naming one of - // the module's methods is then refused. - Methods map[string]MethodPolicy -} - -// MethodPolicy is one method's declaration: exactly one of its fields is set. -type MethodPolicy struct { - Requires []*basev0.WorkScopeV1 - AnyCheckedCaller bool -} - -// servedByMethods is every (kind, action) a module's methods require of a call: -// what the module serves, read from its own method declarations rather than -// from any summary of them. A person's or an installation's grant at the module -// is cut to what it serves, so a kind a method requires is never one nobody can -// be granted. -func servedByMethods(methods map[string]MethodPolicy) []*basev0.WorkScopeV1 { - var served []*basev0.WorkScopeV1 - for _, policy := range methods { - for _, required := range policy.Requires { - served = append(served, &basev0.WorkScopeV1{ResourceKind: required.GetResourceKind(), Actions: append([]string(nil), required.GetActions()...)}) - } - } - return served -} - -// AdmitMethod applies what the module at audience declares for method to the -// scopes a checked call holds. A method that declares nothing, or both that it -// requires something and nothing, is refused: an absent or ambiguous -// declaration is never read as permission. -func (s *State) AdmitMethod(audience, method string, held []*basev0.WorkScopeV1) error { - s.mu.RLock() - m, err := s.moduleByPrincipal(audience) - s.mu.RUnlock() - if err != nil { - return err - } - return admitMethod(m, audience, method, held) -} - -func admitMethod(m Manifest, audience, method string, held []*basev0.WorkScopeV1) error { - var err error - if m.Methods == nil { - return fmt.Errorf("%w: %s's delivery declares no method policy, so %s is refused", ErrForbidden, audience, method) - } - p, ok := m.Methods[method] - if !ok { - if p, ok, err = routeOf(m.Methods, method); err != nil { - return fmt.Errorf("%w: %s: %w", ErrForbidden, audience, err) - } - } - switch { - case !ok: - return fmt.Errorf("%w: %s serves no method %s", ErrUnknown, audience, method) - case p.AnyCheckedCaller && len(p.Requires) > 0: - return fmt.Errorf("%w: %s declares for %s both that it requires scopes and that it requires none", ErrForbidden, audience, method) - case p.AnyCheckedCaller: - return nil - case len(p.Requires) == 0: - return fmt.Errorf("%w: %s declares no policy for %s", ErrForbidden, audience, method) - } - // Method admission checks the kind and action. The module must still check - // each requested resource against the unchanged resource-limited scopes. - var missing []string - for _, r := range p.Requires { - for _, action := range r.GetActions() { - if !holdsAction(held, r.GetResourceKind(), action) { - missing = append(missing, r.GetResourceKind()+":"+action) - } - } - } - if len(missing) > 0 { - return fmt.Errorf("%w: %s requires %s for %s, which the call does not hold (it holds %s)", - ErrForbidden, audience, strings.Join(missing, ", "), method, describe(held)) - } - return nil -} - -// routeOf is the route a module declares that an HTTP request " " -// is for: a declared "