1 package channels 2 3 import ( 4 "time" 5 6 "github.com/google/uuid" 7 "gopkg.in/yaml.v3" 8 ) 9 10 const ( 11 // TODO: ask Lee what values he wants for the DefaultChannelRotationIntervalDuration and DefaultChannelExpireBufferDuration. 12 DefaultChannelRotationIntervalDuration = time.Hour * 24 * 365 13 DefaultChannelExpireBufferDuration = time.Hour * 24 * 7 14 MagPieNamespace = "magpie" 15 ) 16 17 // secondStr = fmt.Sprintf("%d", time.Second) 18 const secondStr = "1000000000" 19 20 // Channel represents a row in the `channels` table. 21 // 22 // Channels are configured per environment (dev1, stage1, prod1, etc). 23 // Channels should never be created, updated, or deleted by anyone other than Edge super admins. 24 type Channel struct { 25 ID uuid.UUID // readonly 26 27 // Name is unique per environment. 28 // 29 // It must be less than 46 characters and match the following regular expression `^[a-z]([-]?[a-z0-9])+$` 30 // since it is used to construct the bearer token secret's name that is mounted by helm workloads. 31 Name string // required 32 33 Team string // required 34 Description string // required 35 36 // ExpireBufferDuration is added to a ChannelKeyVersion's RotateAt time to calculate its ExpireAt time. 37 ExpireBufferDuration time.Duration // optional (default: DefaultChannelExpireBufferDuration) 38 39 // RotationIntervalDuration is added to a ChannelKeyVersion's CreatedAt time to calculate its RotateAt time. 40 RotationIntervalDuration time.Duration // optional (default: DefaultChannelRotationIntervalDuration) 41 42 CreatedAt time.Time // readonly 43 } 44 45 func (c *Channel) setDefaults() { 46 if c.ExpireBufferDuration == 0 { 47 c.ExpireBufferDuration = DefaultChannelExpireBufferDuration 48 } 49 50 if c.RotationIntervalDuration == 0 { 51 c.RotationIntervalDuration = DefaultChannelRotationIntervalDuration 52 } 53 } 54 55 // ChannelUpdateRequest updates a channel's columns using the provided fields. 56 // Fields set to `nil` are ignored. 57 type ChannelUpdateRequest struct { 58 Team *string 59 Description *string 60 ExpireBufferDuration *time.Duration 61 RotationIntervalDuration *time.Duration 62 // NOTE: 63 // Never update the `name` or `channel_id` in the database. 64 // Those columns are passed into immutable fields within GCP resources. 65 } 66 67 // ChannelKeyVersion represents a row in the `channels_key_versions` table. 68 // 69 // TODO: confirm with Luc that the Version & SecretManagerLink descriptions are correct. 70 // IDK if the source of truth is derived from SecretManager resources or KMS resources. 71 // 72 // TODO: check with Luc to see if we need to make any additions or changes to the ChannelKeyVersion type. 73 // Lets confirm/deny this sooner rather than later. 74 // 75 // I have a feeling that we will need additional fields for ChannelKeyVersion, and that Version is not enough. 76 // Are we future proof with just the Version? 77 // Should we store metadata related the sparrow/magpie services' API like their major version, or the API's path? 78 // 79 // I also feel like the SecretManagerLink should be constructed by controllers instead of being stored in the database. 80 // What happens if the underlying resource ID changes, or the project number changes? 81 // These things happen rarely, but they do happen, especially changes to resource IDs for resources with immutable fields. 82 // Will we need a service method to update the SecretManagerLink, or would that be moot if we constructed it in controllers? 83 type ChannelKeyVersion struct { 84 ID uuid.UUID // readonly 85 86 ChannelID uuid.UUID // required 87 BannerEdgeID uuid.UUID // required 88 89 // Version is the SecretManagerSecretVersion's `.status.version` value. 90 // Version is a serial number that begins at `1`. 91 Version int // required 92 93 // SecretManagerLink is the SecretManagerSecret's external reference. 94 // 95 // Format: "projects/{{gcp_foreman_project_number}}/secrets/{{secret_manager_secret_resource_id}}" 96 SecretManagerLink string // required 97 98 // RotateAt is when the latest ChannelKeyVersion should be rotated. 99 // RotateAt is the zero time if the ChannelKeyVersions has already been rotated. 100 // 101 // When the channel service's `RotateChannelNow()` method is called, it sets the latest ChannelKeyVersion's RotateAt time to the current time, which marks it for rotation. 102 // The channel service's `CreateChannelKeyVersion()` method actually rotates a channel. 103 // When a new ChannelKeyVersion is created, the old ChannelKeyVersion's RotateAt time is set to null in the database. 104 RotateAt time.Time // readonly 105 106 // ExpireAt is when data encrypted using this ChannelKeyVersion will no longer be decrypted by magpie. 107 // 108 // ExpireAt is calculated when a ChannelKeyVersion is created, by adding the Channel's RotationIntervalDuration + ExpireBufferDuration to the CreatedAt time. 109 // When a ChannelKeyVersion is actually rotated, the ExpireAt time is recalculated by adding the channel's ExpireBufferDuration to the current time. 110 ExpireAt time.Time // readonly 111 112 CreatedAt time.Time // readonly 113 } 114 115 func (ckv *ChannelKeyVersion) IsExpired() bool { 116 return ckv.ExpireAt.Before(time.Now().UTC()) 117 } 118 119 func (ckv *ChannelKeyVersion) IsLatest() bool { 120 return !ckv.RotateAt.IsZero() 121 } 122 123 // ShouldRotate returns true if the RotateAt time has elapsed. 124 // This method may only be used with the latest ChannelKeyVersion. 125 // 126 // NOTE: ShouldRotate panics if the ChannelKeyVersion has already been rotated. 127 func (ckv *ChannelKeyVersion) ShouldRotate() bool { 128 if !ckv.IsLatest() { 129 // Panicking is valid in this situation. 130 // Checking `ShouldRotate` on a ChannelKeyVersion that has already been rotated is a logic error. 131 // Therefore, instead of returning false, or returning an error that will be logged and ignored, this function panics if `IsLatest()` returns false. 132 return false 133 } 134 return ckv.RotateAt.Before(time.Now().UTC()) 135 } 136 137 // BannerChannel is a composite type that combines: 138 // - the `Channel` being used by this banner. 139 // - a slice of `uuid.UUID` containing every `helm_edge_id` mapped to the channel, that is owned by this banner. 140 // - a slice of `ChannelKeyVersion` containing every key version for the channel, that is owned by this banner. 141 // 142 // TODO: ask team if any additional methods should be added to BannerChannel type. 143 type BannerChannel struct { 144 Channel 145 146 // The `LatestKeyVersion()`, `UnexpiredKeyVersions()`, and `ExpiredKeyVersions()` methods are provided for convenience. 147 KeyVersions []ChannelKeyVersion 148 HelmEdgeIDs []uuid.UUID 149 } 150 151 // LatestKeyVersion returns the channel's most recently created ChannelKeyVersion for this banner. 152 func (bc *BannerChannel) LatestKeyVersion() (latest ChannelKeyVersion, exists bool) { 153 for _, ckv := range bc.KeyVersions { 154 if ckv.IsLatest() { 155 return ckv, true 156 } 157 } 158 return ChannelKeyVersion{}, false 159 } 160 161 // UnexpiredKeyVersions are the ChannelKeyVersions that can be used for decryption by magpie. 162 func (bc *BannerChannel) UnexpiredKeyVersions() []ChannelKeyVersion { 163 var unexpired []ChannelKeyVersion 164 for _, ckv := range bc.KeyVersions { 165 if !ckv.IsExpired() { 166 unexpired = append(unexpired, ckv) 167 } 168 } 169 return unexpired 170 } 171 172 // ExpiredKeyVersions need to be cleaned up. 173 func (bc *BannerChannel) ExpiredKeyVersions() []ChannelKeyVersion { 174 var expired []ChannelKeyVersion 175 for _, ckv := range bc.KeyVersions { 176 if ckv.IsExpired() { 177 expired = append(expired, ckv) 178 } 179 } 180 return expired 181 } 182 183 // ShouldBumpChannelKeyVersion encapsulates the Bannerctl logic that determines when the ChannelKeyVersion should be rotated, created for the first time, or be allowed to expire. 184 // When this function returns `true`, bannerctl needs to generate a new encryption/decryption key pair, and then call `CreateChannelKeyVersion` in the channel service. 185 // 186 // This function returns `false` when none of the banner's helm workloads are using this channel. 187 // For unused channels, bannerctl should allow the existing key versions to expire instead of rotating them. 188 // When an unused channel's key versions eventually expire, all of its banner-specific resources should be cleaned up, so that Edge isn't wasting money on unused secret manager secrets & k8s resources. 189 // 190 // If any of the banner's helm workloads are using this channel, ShouldBumpKeyVersion returns true when: 191 // - no channel key version exists for this banner. 192 // - the latest channel key version's RotateAt time has elapsed. 193 func (bc *BannerChannel) ShouldBumpKeyVersion() bool { 194 latest, exists := bc.LatestKeyVersion() 195 rotate := latest.ShouldRotate() 196 return exists && rotate 197 } 198 199 // IsUsedByHelmWorkloads returns true if any the banner's helm workloads are using the channel. 200 func (bc *BannerChannel) IsUsedByHelmWorkloads() bool { 201 return len(bc.HelmEdgeIDs) > 0 202 } 203 204 type HelmConfigChannel struct { 205 Name string `yaml:"name"` 206 } 207 208 type ParsedHelmConfigChannels struct { 209 Channels []HelmConfigChannel `yaml:"channels,omitempty"` 210 } 211 212 func ParseHelmConfigChannels(configYaml string) (*ParsedHelmConfigChannels, error) { 213 var config ParsedHelmConfigChannels 214 if err := yaml.Unmarshal([]byte(configYaml), &config); err != nil { 215 return nil, err 216 } 217 218 if err := config.validateParsed(); err != nil { 219 return nil, err 220 } 221 return &config, nil 222 } 223 224 func (config *ParsedHelmConfigChannels) HasChannels() bool { 225 return len(config.Channels) > 0 226 } 227 228 func (config *ParsedHelmConfigChannels) Names() []string { 229 var names []string 230 for _, c := range config.Channels { 231 names = append(names, c.Name) 232 } 233 return names 234 } 235