From b4cfad20ce570892d89e6895dc3033506abdaf35 Mon Sep 17 00:00:00 2001 From: Dillon Nys Date: Wed, 13 Mar 2024 11:46:55 -0700 Subject: [PATCH] feat(cedar): Template-linked policies Adds support for templates and template-linked policies. --- .github/workflows/cedar_ffi.yaml | 4 +- .github/workflows/corks_cedar.yaml | 3 - packages/cedar/CHANGELOG.md | 4 + .../cedar/lib/src/policy/cedar_policy.dart | 69 ++++++++++- .../cedar/lib/src/policy/cedar_policy.g.dart | 110 ++++++++++++++++-- .../lib/src/policy/cedar_policy_set.dart | 25 ++-- .../lib/src/policy/cedar_policy_set.g.dart | 39 ++++++- packages/cedar/lib/src/policy/json_expr.dart | 7 +- packages/cedar/lib/src/serializers.dart | 1 + packages/cedar/lib/src/serializers.g.dart | 5 + packages/cedar/pubspec.yaml | 2 +- packages/cedar_ffi/.pubignore | 1 + packages/cedar_ffi/CHANGELOG.md | 4 + .../lib/src/cedar_policy_set_ffi.dart | 26 ++++- .../lib/src/ffi/cedar_bindings.g.dart | 22 ++++ packages/cedar_ffi/pubspec.yaml | 4 +- packages/cedar_ffi/src/include/bindings.h | 21 ++++ packages/cedar_ffi/src/src/c_api/global.rs | 94 ++++++++++++++- packages/cedar_ffi/test/template_test.dart | 57 +++++++++ packages/corks_cedar/CHANGELOG.md | 4 + .../lib/src/interop/proto_interop.dart | 1 + packages/corks_cedar/pubspec.yaml | 4 +- 22 files changed, 465 insertions(+), 42 deletions(-) create mode 100644 packages/cedar_ffi/.pubignore create mode 100644 packages/cedar_ffi/test/template_test.dart diff --git a/.github/workflows/cedar_ffi.yaml b/.github/workflows/cedar_ffi.yaml index ad2101a2..8f1dade2 100644 --- a/.github/workflows/cedar_ffi.yaml +++ b/.github/workflows/cedar_ffi.yaml @@ -3,6 +3,7 @@ on: pull_request: paths: - ".github/workflows/cedar_ffi.yaml" + - "packages/cedar/**" - "packages/cedar_ffi/**" - "packages/cedar_ffi/third_party/cedar" - "packages/cedar_ffi/third_party/cedar/**" @@ -32,9 +33,8 @@ jobs: submodules: true - name: Setup Dart uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 # main - # Will often be out-of-date on runners - name: Setup Rust - run: rustup update stable && rustup default stable + uses: actions-rust-lang/setup-rust-toolchain@b113a30d27a8e59c969077c0a0168cc13dab5ffc # 1.8.0 - name: Get Packages working-directory: packages/cedar_ffi run: dart pub get diff --git a/.github/workflows/corks_cedar.yaml b/.github/workflows/corks_cedar.yaml index 4e3e7e2b..2d58e092 100644 --- a/.github/workflows/corks_cedar.yaml +++ b/.github/workflows/corks_cedar.yaml @@ -30,9 +30,6 @@ jobs: submodules: true - name: Setup Dart uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 # main - # Will often be out-of-date on runners - - name: Setup Rust - run: rustup update stable && rustup default stable - name: Get Packages working-directory: packages/corks_cedar run: dart pub get diff --git a/packages/cedar/CHANGELOG.md b/packages/cedar/CHANGELOG.md index a0712a79..bfabdb40 100644 --- a/packages/cedar/CHANGELOG.md +++ b/packages/cedar/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.1 + +- Add template-linked policies + ## 0.1.0 - Initial version. diff --git a/packages/cedar/lib/src/policy/cedar_policy.dart b/packages/cedar/lib/src/policy/cedar_policy.dart index 37b2a6a2..2a2e25dd 100644 --- a/packages/cedar/lib/src/policy/cedar_policy.dart +++ b/packages/cedar/lib/src/policy/cedar_policy.dart @@ -80,6 +80,27 @@ class CedarPolicyConditionKind extends EnumClass { _$cedarPolicyConditionKindSerializer; } +class CedarSlotId extends EnumClass { + const CedarSlotId._(super.name); + + @BuiltValueEnumConst(wireName: '?principal') + static const CedarSlotId principal = _$principal; + + @BuiltValueEnumConst(wireName: '?resource') + static const CedarSlotId resource = _$resource; + + static BuiltSet get values => _$CedarSlotIdValues; + static CedarSlotId valueOf(String name) => _$CedarSlotIdValueOf(name); + + static CedarSlotId fromJson(String json) => + cedarSerializers.deserializeWith(CedarSlotId.serializer, json)!; + + String toJson() => + cedarSerializers.serializeWith(CedarSlotId.serializer, this) as String; + + static Serializer get serializer => _$cedarSlotIdSerializer; +} + abstract class CedarPolicy implements Built { factory CedarPolicy({ required CedarPolicyEffect effect, @@ -130,6 +151,8 @@ abstract class CedarPolicy implements Built { BuiltList get conditions; BuiltMap? get annotations; + bool get isTemplate => principal.slot != null || resource.slot != null; + Map toJson() => { 'effect': effect.toJson(), 'principal': principal.toJson(), @@ -148,11 +171,13 @@ abstract class CedarPolicyPrincipal required CedarPolicyOp op, CedarEntityId? entity, String? entityType, + CedarSlotId? slot, }) { return CedarPolicyPrincipal.build((b) { b ..op = op - ..entityType = entityType; + ..entityType = entityType + ..slot = slot; if (entity != null) { b.entity.replace(entity); } @@ -166,6 +191,9 @@ abstract class CedarPolicyPrincipal ? null : CedarEntityId.fromJson(json['entity'] as Map), entityType: json['entity_type'] as String?, + slot: json['slot'] == null + ? null + : CedarSlotId.fromJson(json['slot'] as String), ); } @@ -180,15 +208,25 @@ abstract class CedarPolicyPrincipal case CedarPolicyOp.all: _expectAbsent('entity', policy._entity); _expectAbsent('entityType', policy.entityType); + _expectAbsent('slot', policy.slot); case CedarPolicyOp.equals: - _expectPresent('entity', policy._entity); + if (policy._entity == null && policy.slot == null) { + throw ArgumentError( + 'entity and slot must be specified for op equals', + ); + } _expectAbsent('entityType', policy.entityType); case CedarPolicyOp.in$: - _expectPresent('entity', policy._entity); + if (policy._entity == null && policy.slot == null) { + throw ArgumentError( + 'entity and slot must be specified for op in', + ); + } _expectAbsent('entityType', policy.entityType); case CedarPolicyOp.is$: _expectPresent('entityType', policy.entityType); _expectAbsent('entity', policy._entity); + _expectAbsent('slot', policy.slot); default: throw ArgumentError.value(policy.op, 'op', 'Invalid op for principal'); } @@ -199,11 +237,13 @@ abstract class CedarPolicyPrincipal @BuiltValueField(wireName: 'entity_type') String? get entityType; + CedarSlotId? get slot; Map toJson() => { 'op': op.toJson(), if (entity != null) 'entity': entity!.toJson(), if (entityType != null) 'entity_type': entityType, + if (slot != null) 'slot': slot!.toJson(), }; static Serializer get serializer => @@ -301,11 +341,13 @@ abstract class CedarPolicyResource required CedarPolicyOp op, CedarEntityId? entity, String? entityType, + CedarSlotId? slot, }) { return CedarPolicyResource.build((b) { b ..op = op - ..entityType = entityType; + ..entityType = entityType + ..slot = slot; if (entity != null) { b.entity.replace(entity); } @@ -323,15 +365,25 @@ abstract class CedarPolicyResource case CedarPolicyOp.all: _expectAbsent('entity', policy._entity); _expectAbsent('entityType', policy.entityType); + _expectAbsent('slot', policy.slot); case CedarPolicyOp.equals: - _expectPresent('entity', policy._entity); + if (policy._entity == null && policy.slot == null) { + throw ArgumentError( + 'entity and slot must be specified for op equals', + ); + } _expectAbsent('entityType', policy.entityType); case CedarPolicyOp.in$: - _expectPresent('entity', policy._entity); + if (policy._entity == null && policy.slot == null) { + throw ArgumentError( + 'entity and slot must be specified for op in', + ); + } _expectAbsent('entityType', policy.entityType); case CedarPolicyOp.is$: _expectPresent('entityType', policy.entityType); _expectAbsent('entity', policy._entity); + _expectAbsent('slot', policy.slot); default: throw ArgumentError.value(policy.op, 'op', 'Invalid op for resource'); } @@ -342,11 +394,13 @@ abstract class CedarPolicyResource @BuiltValueField(wireName: 'entity_type') String? get entityType; + CedarSlotId? get slot; Map toJson() => { 'op': op.toJson(), if (entity != null) 'entity': entity!.toJson(), if (entityType != null) 'entity_type': entityType, + if (slot != null) 'slot': slot!.toJson(), }; factory CedarPolicyResource.fromJson(Map json) { @@ -356,6 +410,9 @@ abstract class CedarPolicyResource ? null : CedarEntityId.fromJson(json['entity'] as Map), entityType: json['entity_type'] as String?, + slot: json['slot'] == null + ? null + : CedarSlotId.fromJson(json['slot'] as String), ); } diff --git a/packages/cedar/lib/src/policy/cedar_policy.g.dart b/packages/cedar/lib/src/policy/cedar_policy.g.dart index 2766132a..aa8c918a 100644 --- a/packages/cedar/lib/src/policy/cedar_policy.g.dart +++ b/packages/cedar/lib/src/policy/cedar_policy.g.dart @@ -76,12 +76,33 @@ final BuiltSet _$CedarPolicyConditionKindValues = _$unless, ]); +const CedarSlotId _$principal = const CedarSlotId._('principal'); +const CedarSlotId _$resource = const CedarSlotId._('resource'); + +CedarSlotId _$CedarSlotIdValueOf(String name) { + switch (name) { + case 'principal': + return _$principal; + case 'resource': + return _$resource; + default: + throw new ArgumentError(name); + } +} + +final BuiltSet _$CedarSlotIdValues = + new BuiltSet(const [ + _$principal, + _$resource, +]); + Serializer _$cedarPolicyEffectSerializer = new _$CedarPolicyEffectSerializer(); Serializer _$cedarPolicyOpSerializer = new _$CedarPolicyOpSerializer(); Serializer _$cedarPolicyConditionKindSerializer = new _$CedarPolicyConditionKindSerializer(); +Serializer _$cedarSlotIdSerializer = new _$CedarSlotIdSerializer(); Serializer _$cedarPolicySerializer = new _$CedarPolicySerializer(); Serializer _$cedarPolicyPrincipalSerializer = new _$CedarPolicyPrincipalSerializer(); @@ -160,6 +181,33 @@ class _$CedarPolicyConditionKindSerializer CedarPolicyConditionKind.valueOf(serialized as String); } +class _$CedarSlotIdSerializer implements PrimitiveSerializer { + static const Map _toWire = const { + 'principal': '?principal', + 'resource': '?resource', + }; + static const Map _fromWire = const { + '?principal': 'principal', + '?resource': 'resource', + }; + + @override + final Iterable types = const [CedarSlotId]; + @override + final String wireName = 'CedarSlotId'; + + @override + Object serialize(Serializers serializers, CedarSlotId object, + {FullType specifiedType = FullType.unspecified}) => + _toWire[object.name] ?? object.name; + + @override + CedarSlotId deserialize(Serializers serializers, Object serialized, + {FullType specifiedType = FullType.unspecified}) => + CedarSlotId.valueOf( + _fromWire[serialized] ?? (serialized is String ? serialized : '')); +} + class _$CedarPolicySerializer implements StructuredSerializer { @override final Iterable types = const [CedarPolicy, _$CedarPolicy]; @@ -282,6 +330,13 @@ class _$CedarPolicyPrincipalSerializer ..add(serializers.serialize(value, specifiedType: const FullType(String))); } + value = object.slot; + if (value != null) { + result + ..add('slot') + ..add(serializers.serialize(value, + specifiedType: const FullType(CedarSlotId))); + } return result; } @@ -309,6 +364,10 @@ class _$CedarPolicyPrincipalSerializer result.entityType = serializers.deserialize(value, specifiedType: const FullType(String)) as String?; break; + case 'slot': + result.slot = serializers.deserialize(value, + specifiedType: const FullType(CedarSlotId)) as CedarSlotId?; + break; } } @@ -417,6 +476,13 @@ class _$CedarPolicyResourceSerializer ..add(serializers.serialize(value, specifiedType: const FullType(String))); } + value = object.slot; + if (value != null) { + result + ..add('slot') + ..add(serializers.serialize(value, + specifiedType: const FullType(CedarSlotId))); + } return result; } @@ -444,6 +510,10 @@ class _$CedarPolicyResourceSerializer result.entityType = serializers.deserialize(value, specifiedType: const FullType(String)) as String?; break; + case 'slot': + result.slot = serializers.deserialize(value, + specifiedType: const FullType(CedarSlotId)) as CedarSlotId?; + break; } } @@ -693,12 +763,15 @@ class _$CedarPolicyPrincipal extends CedarPolicyPrincipal { final CedarEntityId? entity; @override final String? entityType; + @override + final CedarSlotId? slot; factory _$CedarPolicyPrincipal( [void Function(CedarPolicyPrincipalBuilder)? updates]) => (new CedarPolicyPrincipalBuilder()..update(updates))._build(); - _$CedarPolicyPrincipal._({required this.op, this.entity, this.entityType}) + _$CedarPolicyPrincipal._( + {required this.op, this.entity, this.entityType, this.slot}) : super._() { BuiltValueNullFieldError.checkNotNull(op, r'CedarPolicyPrincipal', 'op'); } @@ -718,7 +791,8 @@ class _$CedarPolicyPrincipal extends CedarPolicyPrincipal { return other is CedarPolicyPrincipal && op == other.op && entity == other.entity && - entityType == other.entityType; + entityType == other.entityType && + slot == other.slot; } @override @@ -727,6 +801,7 @@ class _$CedarPolicyPrincipal extends CedarPolicyPrincipal { _$hash = $jc(_$hash, op.hashCode); _$hash = $jc(_$hash, entity.hashCode); _$hash = $jc(_$hash, entityType.hashCode); + _$hash = $jc(_$hash, slot.hashCode); _$hash = $jf(_$hash); return _$hash; } @@ -736,7 +811,8 @@ class _$CedarPolicyPrincipal extends CedarPolicyPrincipal { return (newBuiltValueToStringHelper(r'CedarPolicyPrincipal') ..add('op', op) ..add('entity', entity) - ..add('entityType', entityType)) + ..add('entityType', entityType) + ..add('slot', slot)) .toString(); } } @@ -758,6 +834,10 @@ class CedarPolicyPrincipalBuilder String? get entityType => _$this._entityType; set entityType(String? entityType) => _$this._entityType = entityType; + CedarSlotId? _slot; + CedarSlotId? get slot => _$this._slot; + set slot(CedarSlotId? slot) => _$this._slot = slot; + CedarPolicyPrincipalBuilder(); CedarPolicyPrincipalBuilder get _$this { @@ -766,6 +846,7 @@ class CedarPolicyPrincipalBuilder _op = $v.op; _entity = $v.entity?.toBuilder(); _entityType = $v.entityType; + _slot = $v.slot; _$v = null; } return this; @@ -794,7 +875,8 @@ class CedarPolicyPrincipalBuilder op: BuiltValueNullFieldError.checkNotNull( op, r'CedarPolicyPrincipal', 'op'), entity: _entity?.build(), - entityType: entityType); + entityType: entityType, + slot: slot); } catch (_) { late String _$failedField; try { @@ -946,12 +1028,15 @@ class _$CedarPolicyResource extends CedarPolicyResource { final CedarEntityId? entity; @override final String? entityType; + @override + final CedarSlotId? slot; factory _$CedarPolicyResource( [void Function(CedarPolicyResourceBuilder)? updates]) => (new CedarPolicyResourceBuilder()..update(updates))._build(); - _$CedarPolicyResource._({required this.op, this.entity, this.entityType}) + _$CedarPolicyResource._( + {required this.op, this.entity, this.entityType, this.slot}) : super._() { BuiltValueNullFieldError.checkNotNull(op, r'CedarPolicyResource', 'op'); } @@ -971,7 +1056,8 @@ class _$CedarPolicyResource extends CedarPolicyResource { return other is CedarPolicyResource && op == other.op && entity == other.entity && - entityType == other.entityType; + entityType == other.entityType && + slot == other.slot; } @override @@ -980,6 +1066,7 @@ class _$CedarPolicyResource extends CedarPolicyResource { _$hash = $jc(_$hash, op.hashCode); _$hash = $jc(_$hash, entity.hashCode); _$hash = $jc(_$hash, entityType.hashCode); + _$hash = $jc(_$hash, slot.hashCode); _$hash = $jf(_$hash); return _$hash; } @@ -989,7 +1076,8 @@ class _$CedarPolicyResource extends CedarPolicyResource { return (newBuiltValueToStringHelper(r'CedarPolicyResource') ..add('op', op) ..add('entity', entity) - ..add('entityType', entityType)) + ..add('entityType', entityType) + ..add('slot', slot)) .toString(); } } @@ -1011,6 +1099,10 @@ class CedarPolicyResourceBuilder String? get entityType => _$this._entityType; set entityType(String? entityType) => _$this._entityType = entityType; + CedarSlotId? _slot; + CedarSlotId? get slot => _$this._slot; + set slot(CedarSlotId? slot) => _$this._slot = slot; + CedarPolicyResourceBuilder(); CedarPolicyResourceBuilder get _$this { @@ -1019,6 +1111,7 @@ class CedarPolicyResourceBuilder _op = $v.op; _entity = $v.entity?.toBuilder(); _entityType = $v.entityType; + _slot = $v.slot; _$v = null; } return this; @@ -1047,7 +1140,8 @@ class CedarPolicyResourceBuilder op: BuiltValueNullFieldError.checkNotNull( op, r'CedarPolicyResource', 'op'), entity: _entity?.build(), - entityType: entityType); + entityType: entityType, + slot: slot); } catch (_) { late String _$failedField; try { diff --git a/packages/cedar/lib/src/policy/cedar_policy_set.dart b/packages/cedar/lib/src/policy/cedar_policy_set.dart index c1bcdbaa..2696f888 100644 --- a/packages/cedar/lib/src/policy/cedar_policy_set.dart +++ b/packages/cedar/lib/src/policy/cedar_policy_set.dart @@ -10,17 +10,26 @@ abstract class CedarPolicySet implements Built { factory CedarPolicySet({ Map policies = const {}, + Map templates = const {}, }) { - return _$CedarPolicySet._(policies: policies.build()); + return _$CedarPolicySet._( + policies: policies.build(), + templates: templates.build(), + ); } factory CedarPolicySet.fromJson(Map json) { - return CedarPolicySet( - policies: { - for (final MapEntry(key: id, value: json) in json.entries) - id: CedarPolicy.fromJson(json as Map), - }, - ); + return CedarPolicySet.build((b) { + for (final MapEntry(key: id, value: json) in json.entries) { + final policyOrTemplate = + CedarPolicy.fromJson(json as Map); + if (policyOrTemplate.isTemplate) { + b.templates[id] = policyOrTemplate; + } else { + b.policies[id] = policyOrTemplate; + } + } + }); } factory CedarPolicySet.build([ @@ -30,7 +39,7 @@ abstract class CedarPolicySet const CedarPolicySet._(); BuiltMap get policies; - // TODO(dnys1): Templates + BuiltMap get templates; Map toJson() => policies.asMap().map((key, value) => MapEntry(key, value.toJson())); diff --git a/packages/cedar/lib/src/policy/cedar_policy_set.g.dart b/packages/cedar/lib/src/policy/cedar_policy_set.g.dart index 8f6fac8f..5734103a 100644 --- a/packages/cedar/lib/src/policy/cedar_policy_set.g.dart +++ b/packages/cedar/lib/src/policy/cedar_policy_set.g.dart @@ -24,6 +24,10 @@ class _$CedarPolicySetSerializer serializers.serialize(object.policies, specifiedType: const FullType(BuiltMap, const [const FullType(String), const FullType(CedarPolicy)])), + 'templates', + serializers.serialize(object.templates, + specifiedType: const FullType(BuiltMap, + const [const FullType(String), const FullType(CedarPolicy)])), ]; return result; @@ -48,6 +52,13 @@ class _$CedarPolicySetSerializer const FullType(CedarPolicy) ]))!); break; + case 'templates': + result.templates.replace(serializers.deserialize(value, + specifiedType: const FullType(BuiltMap, const [ + const FullType(String), + const FullType(CedarPolicy) + ]))!); + break; } } @@ -58,13 +69,18 @@ class _$CedarPolicySetSerializer class _$CedarPolicySet extends CedarPolicySet { @override final BuiltMap policies; + @override + final BuiltMap templates; factory _$CedarPolicySet([void Function(CedarPolicySetBuilder)? updates]) => (new CedarPolicySetBuilder()..update(updates))._build(); - _$CedarPolicySet._({required this.policies}) : super._() { + _$CedarPolicySet._({required this.policies, required this.templates}) + : super._() { BuiltValueNullFieldError.checkNotNull( policies, r'CedarPolicySet', 'policies'); + BuiltValueNullFieldError.checkNotNull( + templates, r'CedarPolicySet', 'templates'); } @override @@ -78,13 +94,16 @@ class _$CedarPolicySet extends CedarPolicySet { @override bool operator ==(Object other) { if (identical(other, this)) return true; - return other is CedarPolicySet && policies == other.policies; + return other is CedarPolicySet && + policies == other.policies && + templates == other.templates; } @override int get hashCode { var _$hash = 0; _$hash = $jc(_$hash, policies.hashCode); + _$hash = $jc(_$hash, templates.hashCode); _$hash = $jf(_$hash); return _$hash; } @@ -92,7 +111,8 @@ class _$CedarPolicySet extends CedarPolicySet { @override String toString() { return (newBuiltValueToStringHelper(r'CedarPolicySet') - ..add('policies', policies)) + ..add('policies', policies) + ..add('templates', templates)) .toString(); } } @@ -107,12 +127,19 @@ class CedarPolicySetBuilder set policies(MapBuilder? policies) => _$this._policies = policies; + MapBuilder? _templates; + MapBuilder get templates => + _$this._templates ??= new MapBuilder(); + set templates(MapBuilder? templates) => + _$this._templates = templates; + CedarPolicySetBuilder(); CedarPolicySetBuilder get _$this { final $v = _$v; if ($v != null) { _policies = $v.policies.toBuilder(); + _templates = $v.templates.toBuilder(); _$v = null; } return this; @@ -135,12 +162,16 @@ class CedarPolicySetBuilder _$CedarPolicySet _build() { _$CedarPolicySet _$result; try { - _$result = _$v ?? new _$CedarPolicySet._(policies: policies.build()); + _$result = _$v ?? + new _$CedarPolicySet._( + policies: policies.build(), templates: templates.build()); } catch (_) { late String _$failedField; try { _$failedField = 'policies'; policies.build(); + _$failedField = 'templates'; + templates.build(); } catch (e) { throw new BuiltValueNestedFieldError( r'CedarPolicySet', _$failedField, e.toString()); diff --git a/packages/cedar/lib/src/policy/json_expr.dart b/packages/cedar/lib/src/policy/json_expr.dart index c0338b76..0abd3d04 100644 --- a/packages/cedar/lib/src/policy/json_expr.dart +++ b/packages/cedar/lib/src/policy/json_expr.dart @@ -7,6 +7,7 @@ library; import 'dart:convert'; import 'package:cedar/src/ast/cedar_entity_id.dart'; +import 'package:cedar/src/policy/cedar_policy.dart'; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; @@ -585,13 +586,11 @@ final class JsonExprVariable extends JsonExpr { int get hashCode => Object.hash(op, variable); } -enum CedarSlotId { principal, resource } - final class JsonExprSlot extends JsonExpr { const JsonExprSlot(this.slotId); factory JsonExprSlot.fromJson(String json) { - return JsonExprSlot(CedarSlotId.values.byName(json.substring(1))); + return JsonExprSlot(CedarSlotId.fromJson(json)); } final CedarSlotId slotId; @@ -600,7 +599,7 @@ final class JsonExprSlot extends JsonExpr { JsonExprOpCode get op => JsonExprOpCode.slot; @override - String valueToJson() => '?${slotId.name}'; + String valueToJson() => slotId.toJson(); @override bool operator ==(Object other) => diff --git a/packages/cedar/lib/src/serializers.dart b/packages/cedar/lib/src/serializers.dart index 6da3e6f9..a98f2557 100644 --- a/packages/cedar/lib/src/serializers.dart +++ b/packages/cedar/lib/src/serializers.dart @@ -17,5 +17,6 @@ part 'serializers.g.dart'; CedarPolicyAction, CedarPolicyResource, CedarPolicyCondition, + CedarSlotId, ]) final Serializers cedarSerializers = _$cedarSerializers; diff --git a/packages/cedar/lib/src/serializers.g.dart b/packages/cedar/lib/src/serializers.g.dart index b682b045..33c4121b 100644 --- a/packages/cedar/lib/src/serializers.g.dart +++ b/packages/cedar/lib/src/serializers.g.dart @@ -18,6 +18,7 @@ Serializers _$cedarSerializers = (new Serializers().toBuilder() ..add(CedarPolicyPrincipal.serializer) ..add(CedarPolicyResource.serializer) ..add(CedarPolicySet.serializer) + ..add(CedarSlotId.serializer) ..addBuilderFactory( const FullType(BuiltList, const [const FullType(CedarEntityId)]), () => new ListBuilder()) @@ -36,6 +37,10 @@ Serializers _$cedarSerializers = (new Serializers().toBuilder() const FullType( BuiltMap, const [const FullType(String), const FullType(String)]), () => new MapBuilder()) + ..addBuilderFactory( + const FullType(BuiltMap, + const [const FullType(String), const FullType(CedarPolicy)]), + () => new MapBuilder()) ..addBuilderFactory( const FullType(BuiltMap, const [const FullType(String), const FullType(CedarPolicy)]), diff --git a/packages/cedar/pubspec.yaml b/packages/cedar/pubspec.yaml index 5fcfb113..d73764dd 100644 --- a/packages/cedar/pubspec.yaml +++ b/packages/cedar/pubspec.yaml @@ -1,6 +1,6 @@ name: cedar description: Core types and interfaces of the Cedar policy language in Dart. -version: 0.1.0 +version: 0.1.1 repository: https://github.com/celest-dev/celest/tree/main/packages/cedar environment: diff --git a/packages/cedar_ffi/.pubignore b/packages/cedar_ffi/.pubignore new file mode 100644 index 00000000..28e05f62 --- /dev/null +++ b/packages/cedar_ffi/.pubignore @@ -0,0 +1 @@ +third_party/ \ No newline at end of file diff --git a/packages/cedar_ffi/CHANGELOG.md b/packages/cedar_ffi/CHANGELOG.md index a0712a79..bfabdb40 100644 --- a/packages/cedar_ffi/CHANGELOG.md +++ b/packages/cedar_ffi/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.1 + +- Add template-linked policies + ## 0.1.0 - Initial version. diff --git a/packages/cedar_ffi/lib/src/cedar_policy_set_ffi.dart b/packages/cedar_ffi/lib/src/cedar_policy_set_ffi.dart index 2eeb1aac..89f73eba 100644 --- a/packages/cedar_ffi/lib/src/cedar_policy_set_ffi.dart +++ b/packages/cedar_ffi/lib/src/cedar_policy_set_ffi.dart @@ -35,14 +35,38 @@ Map> parsePolicies(String policiesIdl) { case bindings.CCedarPolicySetResult( :final policies, :final policy_ids, - :final policies_len + :final policies_len, + :final templates, + :final template_ids, + :final templates_len, ): return { for (var i = 0; i < policies_len; i++) policy_ids[i].cast().toDartString(): jsonDecode(policies[i].cast().toDartString()) as Map, + for (var i = 0; i < templates_len; i++) + template_ids[i].cast().toDartString(): + jsonDecode(templates[i].cast().toDartString()) + as Map, }; } }); } + +extension CedarPolicyLinkFfi on CedarPolicy { + CedarPolicy link(Map values) => using((arena) { + final linkedPolicy = bindings.cedar_link_policy_template( + jsonEncode(toJson()).toNativeUtf8(allocator: arena).cast(), + jsonEncode(values.map((k, v) => MapEntry(k.toJson(), v.toString()))) + .toNativeUtf8(allocator: arena) + .cast(), + ); + if (linkedPolicy == nullptr) { + throw FormatException('Could not link policy'); + } + return CedarPolicy.fromJson( + jsonDecode(linkedPolicy.cast().toDartString()), + ); + }); +} diff --git a/packages/cedar_ffi/lib/src/ffi/cedar_bindings.g.dart b/packages/cedar_ffi/lib/src/ffi/cedar_bindings.g.dart index ff820c71..332c51c5 100644 --- a/packages/cedar_ffi/lib/src/ffi/cedar_bindings.g.dart +++ b/packages/cedar_ffi/lib/src/ffi/cedar_bindings.g.dart @@ -11,6 +11,16 @@ external CCedarPolicySetResult cedar_parse_policy_set( ffi.Pointer policies, ); +/// Links a policy template to a set of entities +@ffi.Native< + ffi.Pointer Function( + ffi.Pointer, ffi.Pointer)>( + symbol: 'cedar_link_policy_template', isLeaf: true) +external ffi.Pointer cedar_link_policy_template( + ffi.Pointer policy_template_json, + ffi.Pointer entities_json, +); + /// Initializes the Cedar policy engine with the given configuration. /// /// This must be called exactly once before any other Cedar functions are called. @@ -55,6 +65,18 @@ final class CCedarPolicySetResult extends ffi.Struct { /// The IDs for the `policies` in the policy set. external ffi.Pointer> policy_ids; + /// The number of templates in the policy set. + @ffi.UintPtr() + external int templates_len; + + /// The templates in the policy set, in JSON format. + /// + /// This is only valid if `templates_len` is greater than 0 and `errors_len` is 0. + external ffi.Pointer> templates; + + /// The IDs for the `templates` in the policy set. + external ffi.Pointer> template_ids; + /// The number of errors encountered while parsing the policy set. @ffi.UintPtr() external int errors_len; diff --git a/packages/cedar_ffi/pubspec.yaml b/packages/cedar_ffi/pubspec.yaml index ed15c2ec..c3bcf3ff 100644 --- a/packages/cedar_ffi/pubspec.yaml +++ b/packages/cedar_ffi/pubspec.yaml @@ -1,6 +1,6 @@ name: cedar_ffi description: FFI bindings for the Cedar policy language, written in Rust. -version: 0.1.0 +version: 0.1.1 repository: https://github.com/celest-dev/celest/tree/main/packages/cedar_ffi environment: @@ -9,7 +9,7 @@ environment: dependencies: built_collection: ^5.1.1 built_value: ^8.9.1 - cedar: ^0.1.0 + cedar: ^0.1.1 cli_config: ^0.1.1 collection: ^1.18.0 ffi: ^2.1.0 diff --git a/packages/cedar_ffi/src/include/bindings.h b/packages/cedar_ffi/src/include/bindings.h index bd3be5fe..d9ed731a 100644 --- a/packages/cedar_ffi/src/include/bindings.h +++ b/packages/cedar_ffi/src/include/bindings.h @@ -24,6 +24,20 @@ typedef struct CCedarPolicySetResult { * The IDs for the `policies` in the policy set. */ const char *const *policy_ids; + /** + * The number of templates in the policy set. + */ + uintptr_t templates_len; + /** + * The templates in the policy set, in JSON format. + * + * This is only valid if `templates_len` is greater than 0 and `errors_len` is 0. + */ + const char *const *templates; + /** + * The IDs for the `templates` in the policy set. + */ + const char *const *template_ids; /** * The number of errors encountered while parsing the policy set. */ @@ -165,6 +179,13 @@ typedef struct CCedarQuery { */ struct CCedarPolicySetResult cedar_parse_policy_set(const char *policies); +/** + * Links a policy template to a set of entities. + * + * Returns the linked policy template in JSON format. + */ +const char *cedar_link_policy_template(const char *policy_template_json, const char *entities_json); + /** * Initializes the Cedar policy engine with the given configuration. * diff --git a/packages/cedar_ffi/src/src/c_api/global.rs b/packages/cedar_ffi/src/src/c_api/global.rs index 6ac48ecb..2ba7d1c2 100644 --- a/packages/cedar_ffi/src/src/c_api/global.rs +++ b/packages/cedar_ffi/src/src/c_api/global.rs @@ -1,4 +1,6 @@ -use std::{ffi::c_char, ptr::null, str::FromStr}; +use std::{collections::HashMap, ffi::c_char, ptr::null, str::FromStr}; + +use cedar_policy::{EntityUid, PolicyId, SlotId}; use super::helpers; @@ -18,6 +20,17 @@ pub struct CCedarPolicySetResult { /// The IDs for the `policies` in the policy set. policy_ids: *const *const c_char, + /// The number of templates in the policy set. + templates_len: usize, + + /// The templates in the policy set, in JSON format. + /// + /// This is only valid if `templates_len` is greater than 0 and `errors_len` is 0. + templates: *const *const c_char, + + /// The IDs for the `templates` in the policy set. + template_ids: *const *const c_char, + /// The number of errors encountered while parsing the policy set. errors_len: usize, @@ -60,6 +73,29 @@ pub extern "C" fn cedar_parse_policy_set(policies: *const c_char) -> CCedarPolic "policy_ids_len != policies_len" ); + let template_ids = policy_set + .templates() + .map(|t| Ok(helpers::string_to_c(t.id().to_string())?)) + .collect::>>()? + .to_owned(); + let template_ids_len = template_ids.len(); + let template_ids_ptr = template_ids.as_ptr(); + std::mem::forget(template_ids); + + let templates = policy_set + .templates() + .map(|t| Ok(helpers::string_to_c(serde_json::to_string(&t.to_json()?)?)?)) + .collect::>>()? + .to_owned(); + let templates_len = templates.len(); + let templates_ptr = templates.as_ptr(); + std::mem::forget(templates); + + assert!( + template_ids_len == templates_len, + "template_ids_len != templates_len" + ); + let errors = errors .iter() .map(|e| helpers::string_to_c(e.to_string())) @@ -73,6 +109,9 @@ pub extern "C" fn cedar_parse_policy_set(policies: *const c_char) -> CCedarPolic policies_len, policies: policies_ptr, policy_ids: policy_ids_ptr, + templates_len, + templates: templates_ptr, + template_ids: template_ids_ptr, errors_len, errors: errors_ptr, }) @@ -84,9 +123,62 @@ pub extern "C" fn cedar_parse_policy_set(policies: *const c_char) -> CCedarPolic policies_len: 0, policies: null(), policy_ids: null(), + templates_len: 0, + templates: null(), + template_ids: null(), errors_len: 1, errors: &error_str, } }, ) } + +/// Links a policy template to a set of entities. +/// +/// Returns the linked policy template in JSON format. +#[no_mangle] +pub extern "C" fn cedar_link_policy_template( + policy_template_json: *const c_char, + entities_json: *const c_char, +) -> *const c_char { + helpers::log_on_error( + || { + let policy_template_json = + helpers::string_from_c("policy_template_json", policy_template_json)?; + let entities_json = helpers::string_from_c("entities_json", entities_json)?; + + let policy_template = cedar_policy::Template::from_json( + Some(PolicyId::new("template")), + serde_json::from_str(&policy_template_json)?, + )?; + let entities: HashMap = serde_json::from_str(&entities_json)?; + let entities: HashMap = entities + .iter() + .map(|(k, v)| { + Ok(( + match k.as_str() { + "?principal" => SlotId::principal(), + "?resource" => SlotId::resource(), + _ => return Err(anyhow::anyhow!("invalid slot id")), + }, + EntityUid::from_str(v)?, + )) + }) + .collect::>()?; + + let mut policy_set = cedar_policy::PolicySet::new(); + policy_set.add_template(policy_template)?; + policy_set.link(PolicyId::new("template"), PolicyId::new("linked"), entities)?; + + let linked_policy = policy_set + .policy(&PolicyId::new("linked")) + .ok_or(anyhow::anyhow!("linked policy not found"))?; + let linked_policy_template_json = serde_json::to_string(&linked_policy.to_json()?)?; + let linked_policy_template_json = helpers::string_to_c(linked_policy_template_json)?; + + Ok(linked_policy_template_json) + }, + "linking policy template", + |_| null(), + ) +} diff --git a/packages/cedar_ffi/test/template_test.dart b/packages/cedar_ffi/test/template_test.dart new file mode 100644 index 00000000..056c8683 --- /dev/null +++ b/packages/cedar_ffi/test/template_test.dart @@ -0,0 +1,57 @@ +import 'package:cedar/cedar.dart'; +import 'package:cedar_ffi/cedar_ffi.dart'; +import 'package:test/test.dart'; + +void main() { + group('templates', () { + test('principal', () { + const principalTemplate = ''' +permit( + principal == ?principal, + action, + resource +); +'''; + + final policySet = CedarPolicySetFfi.fromCedar(principalTemplate); + + expect(policySet.templates, hasLength(1)); + + final template = policySet.templates.values.first; + expect(template.isTemplate, true); + expect(template.principal.slot, CedarSlotId.principal); + + final linkedPolicy = template.link( + {CedarSlotId.principal: CedarEntityId('Test', 'test')}, + ); + expect(linkedPolicy.isTemplate, false); + expect(linkedPolicy.principal.op, CedarPolicyOp.equals); + expect(linkedPolicy.principal.entity, CedarEntityId('Test', 'test')); + }); + + test('resource', () { + const resourceTemplate = ''' +permit( + principal, + action, + resource == ?resource +); +'''; + + final policySet = CedarPolicySetFfi.fromCedar(resourceTemplate); + + expect(policySet.templates, hasLength(1)); + + final template = policySet.templates.values.first; + expect(template.isTemplate, true); + expect(template.resource.slot, CedarSlotId.resource); + + final linkedPolicy = template.link( + {CedarSlotId.resource: CedarEntityId('Test', 'test')}, + ); + expect(linkedPolicy.isTemplate, false); + expect(linkedPolicy.resource.op, CedarPolicyOp.equals); + expect(linkedPolicy.resource.entity, CedarEntityId('Test', 'test')); + }); + }); +} diff --git a/packages/corks_cedar/CHANGELOG.md b/packages/corks_cedar/CHANGELOG.md index a0712a79..bfabdb40 100644 --- a/packages/corks_cedar/CHANGELOG.md +++ b/packages/corks_cedar/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.1 + +- Add template-linked policies + ## 0.1.0 - Initial version. diff --git a/packages/corks_cedar/lib/src/interop/proto_interop.dart b/packages/corks_cedar/lib/src/interop/proto_interop.dart index 94cdee46..4beac575 100644 --- a/packages/corks_cedar/lib/src/interop/proto_interop.dart +++ b/packages/corks_cedar/lib/src/interop/proto_interop.dart @@ -554,6 +554,7 @@ extension ExprToProto on cedar.JsonExpr { slotId: switch (slotId) { cedar.CedarSlotId.principal => SlotId.SLOT_ID_PRINCIPAL, cedar.CedarSlotId.resource => SlotId.SLOT_ID_RESOURCE, + _ => throw UnimplementedError(), }, ), ), diff --git a/packages/corks_cedar/pubspec.yaml b/packages/corks_cedar/pubspec.yaml index 02f25df0..103215fc 100644 --- a/packages/corks_cedar/pubspec.yaml +++ b/packages/corks_cedar/pubspec.yaml @@ -1,13 +1,13 @@ name: corks_cedar description: An embedded authorization token format, based off Google's macaroons. -version: 0.1.0 +version: 0.1.1 repository: https://github.com/celest-dev/celest/tree/main/packages/corks_cedar environment: sdk: ^3.3.0 dependencies: - cedar: ^0.1.0 + cedar: ^0.1.1 crypto: ^3.0.3 fixnum: ^1.1.0 meta: ^1.10.0