github.com/xmidt-org/webpa-common@v1.11.9/device/metadata.go (about) 1 package device 2 3 import ( 4 "sync" 5 "sync/atomic" 6 7 "github.com/segmentio/ksuid" 8 "github.com/spf13/cast" 9 ) 10 11 // Reserved metadata keys 12 const ( 13 JWTClaimsKey = "jwt-claims" 14 SessionIDKey = "session-id" 15 ) 16 17 // Top level JWTClaim keys 18 const ( 19 PartnerIDClaimKey = "partner-id" 20 TrustClaimKey = "trust" 21 ) 22 23 // Default values 24 const ( 25 UnknownPartner = "unknown" 26 ) 27 28 var reservedMetadataKeys = map[string]bool{ 29 JWTClaimsKey: true, SessionIDKey: true, 30 } 31 32 func init() { 33 ksuid.SetRand(ksuid.FastRander) 34 } 35 36 // Metadata contains information such as security credentials 37 // related to a device. Read operations are optimized with a 38 // copy-on-write strategy. Client code must further synchronize concurrent 39 // writers to avoid stale data. 40 // Metadata uses an atomic.Value internally and thus it should not be copied 41 // after creation. 42 type Metadata struct { 43 v atomic.Value 44 once sync.Once 45 } 46 47 // SessionID returns the UUID associated with a device's current connection 48 // to the cluster if one has been set. The zero value is returned as default. 49 func (m *Metadata) SessionID() (sessionID string) { 50 sessionID, _ = m.loadData()[SessionIDKey].(string) 51 return 52 } 53 54 // SetSessionID sets the UUID associated the device's current connection to the cluster. 55 // It uses sync.Once to ensure the sessionID is unchanged through the metadata's lifecycle. 56 func (m *Metadata) SetSessionID(sessionID string) { 57 m.once.Do(func() { 58 m.copyAndStore(SessionIDKey, sessionID) 59 }) 60 } 61 62 // Load returns the value associated with the given key in the metadata map. 63 // It is not recommended modifying values returned by reference. 64 func (m *Metadata) Load(key string) interface{} { 65 return m.loadData()[key] 66 } 67 68 // Store updates the key value mapping in the device metadata map. 69 // A boolean result is given indicating whether the operation was successful. 70 // Operations will fail for reserved keys. 71 // To avoid updating keys with stale data/value, client code will need to 72 // synchronize the entire transaction of reading, copying, modifying and 73 // writing back the value. 74 func (m *Metadata) Store(key string, value interface{}) bool { 75 if reservedMetadataKeys[key] { 76 return false 77 } 78 m.copyAndStore(key, value) 79 return true 80 } 81 82 // SetClaims updates the claims associated with the device that's 83 // owner of the metadata. 84 // To avoid updating the claims with stale data, client code will need to 85 // synchronize the entire transaction of reading, copying, modifying and 86 // writing back the value. 87 func (m *Metadata) SetClaims(claims map[string]interface{}) { 88 m.copyAndStore(JWTClaimsKey, deepCopyMap(claims)) 89 } 90 91 // Claims returns the claims attached to a device. The returned map 92 // should not be modified to avoid any race conditions. To update the claims, 93 // take a look at the ClaimsCopy() function 94 func (m *Metadata) Claims() (claims map[string]interface{}) { 95 claims, _ = m.loadData()[JWTClaimsKey].(map[string]interface{}) 96 return 97 } 98 99 // ClaimsCopy returns a deep copy of the claims. Use this, along with the 100 // SetClaims() method to update the claims. 101 func (m *Metadata) ClaimsCopy() map[string]interface{} { 102 return deepCopyMap(m.Claims()) 103 } 104 105 // TrustClaim returns the device's trust level claim. 106 // By Default, a device is untrusted (trust = 0). 107 func (m *Metadata) TrustClaim() int { 108 return cast.ToInt(m.Claims()[TrustClaimKey]) 109 } 110 111 // PartnerIDClaim returns the partner ID claim. 112 // If no claim is found, the value defaults to "unknown" 113 func (m *Metadata) PartnerIDClaim() string { 114 partnerID, ok := m.Claims()[PartnerIDClaimKey].(string) 115 if !ok { 116 return UnknownPartner 117 } 118 return partnerID 119 } 120 121 func (m *Metadata) loadData() (data map[string]interface{}) { 122 data, _ = m.v.Load().(map[string]interface{}) 123 return 124 } 125 126 func (m *Metadata) storeData(data map[string]interface{}) { 127 m.v.Store(data) 128 } 129 130 func (m *Metadata) copyAndStore(key string, val interface{}) { 131 data := deepCopyMap(m.loadData()) 132 data[key] = val 133 m.storeData(data) 134 } 135 136 func deepCopyMap(m map[string]interface{}) map[string]interface{} { 137 deepCopy := make(map[string]interface{}) 138 for key, val := range m { 139 switch val.(type) { 140 case map[interface{}]interface{}: 141 val = cast.ToStringMap(val) 142 deepCopy[key] = deepCopyMap(val.(map[string]interface{})) 143 case map[string]interface{}: 144 deepCopy[key] = deepCopyMap(val.(map[string]interface{})) 145 default: 146 deepCopy[key] = val 147 } 148 149 } 150 return deepCopy 151 }