blob: 72f2ee26e129097b4f259be6204f36a2a8269ceb [file] [log] [blame]
avm9996383f8f292021-08-24 18:26:52 +02001package main
2
3import (
4 "context"
5 "database/sql"
6 "flag"
7 "fmt"
8 "log"
9 "net"
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +020010 "net/http"
avm9996383f8f292021-08-24 18:26:52 +020011 "time"
12
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +020013 "github.com/ReneKroon/ttlcache/v2"
avm9996383f8f292021-08-24 18:26:52 +020014 _ "github.com/go-sql-driver/mysql"
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +020015 "google.golang.org/api/idtoken"
avm9996383f8f292021-08-24 18:26:52 +020016 "google.golang.org/grpc"
17 "google.golang.org/grpc/codes"
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +020018 "google.golang.org/grpc/metadata"
avm9996383f8f292021-08-24 18:26:52 +020019 "google.golang.org/grpc/reflection"
20 "google.golang.org/grpc/status"
21 "google.golang.org/protobuf/proto"
22
23 pb "gomodules.avm99963.com/twpt-server/api_proto"
24 db "gomodules.avm99963.com/twpt-server/internal/db"
25)
26
27var (
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +020028 authorizedUsersCacheTime = 15 * time.Minute
29)
30
31var (
avm9996383f8f292021-08-24 18:26:52 +020032 port = flag.Int("port", 10000, "The server port")
33 dbDsn = flag.String("db", "", "MySQL/MariaDB database data source name (DSN)") // https://github.com/go-sql-driver/mysql#dsn-data-source-name
34 dbConnMaxLifetime = flag.Int("dbConnMaxLifetime", 3*60, "Maximum amount of time a connection to the database may be reused in seconds.")
35 dbMaxOpenConns = flag.Int("dbMaxOpenConns", 5, "Maximum number of open connections to the database.")
36 dbMaxIdleConns = flag.Int("dbMaxIdleConns", *dbMaxOpenConns, "Maximum number of connections to the database in the idle connection pool.")
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +020037 jwtAudience = flag.String("jwtAudience", "", "JWT audience string.")
avm9996383f8f292021-08-24 18:26:52 +020038)
39
40type killSwitchServiceServer struct {
41 pb.UnimplementedKillSwitchServiceServer
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +020042 dbPool *sql.DB
43 jwtValidator *idtoken.Validator
44 cache ttlcache.SimpleCache
avm9996383f8f292021-08-24 18:26:52 +020045}
46
47func newKillSwitchServiceServer() *killSwitchServiceServer {
48 s := &killSwitchServiceServer{}
49 db, err := sql.Open("mysql", *dbDsn)
50 if err != nil {
51 log.Fatalf("unable to open database connection: %v", err)
52 }
53 db.SetConnMaxLifetime(time.Duration(*dbConnMaxLifetime) * time.Second)
54 db.SetMaxOpenConns(*dbMaxOpenConns)
55 db.SetMaxIdleConns(*dbMaxIdleConns)
56
57 ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
58 defer cancel()
59
60 if err := db.PingContext(ctx); err != nil {
61 log.Fatalf("unable to connect to database: %v", err)
62 }
63
64 s.dbPool = db
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +020065
66 client := &http.Client{}
67 validator, err := idtoken.NewValidator(context.Background(), idtoken.WithHTTPClient(client))
68 if err != nil {
69 log.Fatalf("unable to start idtoken validator: %v", err)
70 }
71 s.jwtValidator = validator
72
73 s.cache = ttlcache.NewCache()
74
avm9996383f8f292021-08-24 18:26:52 +020075 return s
76}
77
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +020078func getAuthenticatedUser(s *killSwitchServiceServer, ctx context.Context) (*pb.KillSwitchAuthorizedUser, error) {
79 md, ok := metadata.FromIncomingContext(ctx)
80 if !ok {
81 return nil, status.Errorf(codes.Internal, "getAuthenticatedUser: can't retrieve metadata from incoming request")
82 }
83
84 authorization := md.Get("authorization")
85 if len(authorization) < 1 {
86 return nil, status.Errorf(codes.Unauthenticated, "Unauthenticated")
87 }
88
89 token := authorization[0]
90 payload, err := s.jwtValidator.Validate(ctx, token, *jwtAudience)
91 if err != nil {
Adrià Vilanova Martínezf7ee6582021-09-02 21:53:28 +020092 return nil, status.Errorf(codes.Unauthenticated, "getAuthenticatedUser: can't parse or validate idtoken")
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +020093 }
94
95 var authorizedUsers []*pb.KillSwitchAuthorizedUser
96 var okAssertion bool
97
98 authorizedUsersInterface, err := s.cache.Get("AuthorizedUsers")
99 if err == nil {
100 authorizedUsers, okAssertion = authorizedUsersInterface.([]*pb.KillSwitchAuthorizedUser)
101 }
102 if err != nil || !okAssertion {
103 freshAuthorizedUsers, err := db.ListAuthorizedUsers(s.dbPool, ctx)
104 if err != nil {
105 log.Printf("getAuthenticatedUser: error while getting authorized users: %v", err)
106 return nil, status.Errorf(codes.Internal, "getAuthenticatedUser: can't get list of authorized users")
107 }
108 s.cache.SetWithTTL("AuthorizedUsers", freshAuthorizedUsers, authorizedUsersCacheTime)
109 authorizedUsers = freshAuthorizedUsers
110 }
111
112 // Check if the current user is one of the authorized users, and if so return it.
113 for _, u := range authorizedUsers {
114 if u.GetGoogleUid() != "" && u.GetGoogleUid() == payload.Subject {
115 return u, nil
116 }
117
118 email, emailExists := payload.Claims["email"]
119 emailVerified, emailVerifiedExists := payload.Claims["email_verified"]
120
121 if u.GetEmail() != "" && emailVerifiedExists && emailExists && emailVerified == true && email == u.GetEmail() {
122 return u, nil
123 }
124 }
125
126 return nil, status.Errorf(codes.PermissionDenied, "User is not part of the authorized users list.")
127}
128
129func userHasAccessLevel(requiredLevel pb.KillSwitchAuthorizedUser_AccessLevel, user *pb.KillSwitchAuthorizedUser) bool {
130 return requiredLevel <= user.GetAccessLevel()
131}
132
133func errorWhenMissingAccess(requiredLevel pb.KillSwitchAuthorizedUser_AccessLevel, user *pb.KillSwitchAuthorizedUser) error {
134 if userHasAccessLevel(requiredLevel, user) {
135 return nil
136 }
137
138 return status.Errorf(codes.PermissionDenied, "User has lower access level than the action that they are trying to perform.")
139}
140
avm9996383f8f292021-08-24 18:26:52 +0200141func (s *killSwitchServiceServer) GetKillSwitchStatus(ctx context.Context, req *pb.GetKillSwitchStatusRequest) (*pb.GetKillSwitchStatusResponse, error) {
142 return nil, status.Errorf(codes.Unimplemented, "Unimplemented method.")
143}
144
145func (s *killSwitchServiceServer) GetKillSwitchOverview(ctx context.Context, req *pb.GetKillSwitchOverviewRequest) (*pb.GetKillSwitchOverviewResponse, error) {
Adrià Vilanova Martínez86103142021-09-03 15:13:20 +0200146 killSwitches, err := db.ListKillSwitches(s.dbPool, ctx, req.WithNonactiveKillSwitches)
Adrià Vilanova Martínez25e12112021-08-25 13:48:06 +0200147 if err != nil {
148 return nil, status.Errorf(codes.Unavailable, err.Error())
149 }
150 res := &pb.GetKillSwitchOverviewResponse{
151 KillSwitches: killSwitches,
152 }
153 return res, nil
avm9996383f8f292021-08-24 18:26:52 +0200154}
155
156func (s *killSwitchServiceServer) SyncFeatures(ctx context.Context, req *pb.SyncFeaturesRequest) (*pb.SyncFeaturesResponse, error) {
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +0200157 // This method requires authentication
158 authenticatedUser, err := getAuthenticatedUser(s, ctx)
159 if err != nil {
160 return nil, err
161 }
162 err = errorWhenMissingAccess(pb.KillSwitchAuthorizedUser_ACCESS_LEVEL_ADMIN, authenticatedUser)
163 if err != nil {
164 return nil, err
165 }
166
avm9996383f8f292021-08-24 18:26:52 +0200167 log.Println("Syncing features...")
168
169 for _, feature := range req.Features {
170 existingFeature, err := db.GetFeatureByCodename(s.dbPool, ctx, feature.Codename)
171 if err != nil {
172 return nil, status.Errorf(codes.Unavailable, err.Error())
173 }
174 // If the feature didn't exist in the db, add it. Otherwise, update it if applicable.
175 if existingFeature == nil {
176 if err := db.AddFeature(s.dbPool, ctx, feature); err != nil {
177 return nil, status.Error(codes.Unavailable, err.Error())
178 }
179 } else {
180 canonicalExistingFeature := *existingFeature
181 canonicalExistingFeature.Id = 0
182 if !proto.Equal(&canonicalExistingFeature, feature) {
183 if err := db.UpdateFeature(s.dbPool, ctx, existingFeature.Id, feature); err != nil {
184 return nil, status.Error(codes.Unavailable, err.Error())
185 }
186 }
187 }
188 }
189
190 res := &pb.SyncFeaturesResponse{}
191 return res, nil
192}
193
194func (s *killSwitchServiceServer) ListFeatures(ctx context.Context, req *pb.ListFeaturesRequest) (*pb.ListFeaturesResponse, error) {
Adrià Vilanova Martínez25e12112021-08-25 13:48:06 +0200195 features, err := db.ListFeatures(s.dbPool, ctx, req.WithDeprecatedFeatures)
196 if err != nil {
197 return nil, status.Errorf(codes.Unavailable, err.Error())
198 }
199 res := &pb.ListFeaturesResponse{
200 Features: features,
201 }
202 return res, nil
avm9996383f8f292021-08-24 18:26:52 +0200203}
204
205func (s *killSwitchServiceServer) EnableKillSwitch(ctx context.Context, req *pb.EnableKillSwitchRequest) (*pb.EnableKillSwitchResponse, error) {
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +0200206 // This method requires authentication
207 authenticatedUser, err := getAuthenticatedUser(s, ctx)
208 if err != nil {
209 return nil, err
210 }
211 err = errorWhenMissingAccess(pb.KillSwitchAuthorizedUser_ACCESS_LEVEL_ACTIVATOR, authenticatedUser)
212 if err != nil {
213 return nil, err
214 }
215
Adrià Vilanova Martínez25e12112021-08-25 13:48:06 +0200216 if req.GetKillSwitch().GetFeature().GetId() == 0 {
217 return nil, status.Errorf(codes.InvalidArgument, "feature.id must be set.")
218 }
219
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +0200220 err = db.EnableKillSwitch(s.dbPool, ctx, req.KillSwitch, authenticatedUser)
Adrià Vilanova Martínez25e12112021-08-25 13:48:06 +0200221 if err != nil {
222 return nil, status.Errorf(codes.Unavailable, err.Error())
223 }
224 res := &pb.EnableKillSwitchResponse{}
225 return res, nil
avm9996383f8f292021-08-24 18:26:52 +0200226}
227
228func (s *killSwitchServiceServer) DisableKillSwitch(ctx context.Context, req *pb.DisableKillSwitchRequest) (*pb.DisableKillSwitchResponse, error) {
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +0200229 // This method requires authentication
230 authenticatedUser, err := getAuthenticatedUser(s, ctx)
231 if err != nil {
232 return nil, err
233 }
234 err = errorWhenMissingAccess(pb.KillSwitchAuthorizedUser_ACCESS_LEVEL_ACTIVATOR, authenticatedUser)
235 if err != nil {
236 return nil, err
237 }
238
Adrià Vilanova Martínez25e12112021-08-25 13:48:06 +0200239 if req.GetKillSwitchId() == 0 {
240 return nil, status.Errorf(codes.InvalidArgument, "kill_switch_id must be set.")
241 }
242
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +0200243 err = db.DisableKillSwitch(s.dbPool, ctx, req.KillSwitchId, authenticatedUser)
Adrià Vilanova Martínez25e12112021-08-25 13:48:06 +0200244 if err != nil {
245 return nil, status.Errorf(codes.Unavailable, err.Error())
246 }
247 res := &pb.DisableKillSwitchResponse{}
248 return res, nil
avm9996383f8f292021-08-24 18:26:52 +0200249}
250
251func (s *killSwitchServiceServer) ListAuthorizedUsers(ctx context.Context, req *pb.ListAuthorizedUsersRequest) (*pb.ListAuthorizedUsersResponse, error) {
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +0200252 // This method requires authentication
253 authenticatedUser, err := getAuthenticatedUser(s, ctx)
254 if err != nil {
255 return nil, err
256 }
257 err = errorWhenMissingAccess(pb.KillSwitchAuthorizedUser_ACCESS_LEVEL_ACTIVATOR, authenticatedUser)
258 if err != nil {
259 return nil, err
260 }
261
Adrià Vilanova Martínez25e12112021-08-25 13:48:06 +0200262 users, err := db.ListAuthorizedUsers(s.dbPool, ctx)
263 if err != nil {
264 return nil, status.Errorf(codes.Unavailable, err.Error())
265 }
266 res := &pb.ListAuthorizedUsersResponse{
267 Users: users,
268 }
269 return res, nil
avm9996383f8f292021-08-24 18:26:52 +0200270}
271
272func (s *killSwitchServiceServer) AddAuthorizedUser(ctx context.Context, req *pb.AddAuthorizedUserRequest) (*pb.AddAuthorizedUserResponse, error) {
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +0200273 // This method requires authentication
274 authenticatedUser, err := getAuthenticatedUser(s, ctx)
275 if err != nil {
276 return nil, err
277 }
278 err = errorWhenMissingAccess(pb.KillSwitchAuthorizedUser_ACCESS_LEVEL_ADMIN, authenticatedUser)
279 if err != nil {
280 return nil, err
281 }
282
Adrià Vilanova Martínez25e12112021-08-25 13:48:06 +0200283 if req.GetUser().GetGoogleUid() == "" && req.GetUser().GetEmail() == "" {
284 return nil, status.Errorf(codes.InvalidArgument, "At least one of google_uid or email must be set.")
285 }
286
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +0200287 err = db.AddAuthorizedUser(s.dbPool, ctx, req.User, authenticatedUser)
Adrià Vilanova Martínez25e12112021-08-25 13:48:06 +0200288 if err != nil {
289 return nil, status.Errorf(codes.Unavailable, err.Error())
290 }
291 res := &pb.AddAuthorizedUserResponse{}
292 return res, nil
avm9996383f8f292021-08-24 18:26:52 +0200293}
294
295func (s *killSwitchServiceServer) UpdateAuthorizedUser(ctx context.Context, req *pb.UpdateAuthorizedUserRequest) (*pb.UpdateAuthorizedUserResponse, error) {
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +0200296 // This method requires authentication
297 authenticatedUser, err := getAuthenticatedUser(s, ctx)
298 if err != nil {
299 return nil, err
300 }
301 err = errorWhenMissingAccess(pb.KillSwitchAuthorizedUser_ACCESS_LEVEL_ADMIN, authenticatedUser)
302 if err != nil {
303 return nil, err
304 }
305
Adrià Vilanova Martínez25e12112021-08-25 13:48:06 +0200306 if req.GetUserId() == 0 {
307 return nil, status.Errorf(codes.InvalidArgument, "user_id must be greater than 0.")
308 }
309
310 if req.GetUser().GetGoogleUid() == "" && req.GetUser().GetEmail() == "" {
311 return nil, status.Errorf(codes.InvalidArgument, "At least one of google_uid or email must be set.")
312 }
313
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +0200314 err = db.UpdateAuthorizedUser(s.dbPool, ctx, req.UserId, req.User, authenticatedUser)
Adrià Vilanova Martínez25e12112021-08-25 13:48:06 +0200315 if err != nil {
316 return nil, status.Errorf(codes.Unavailable, err.Error())
317 }
318 res := &pb.UpdateAuthorizedUserResponse{}
319 return res, nil
avm9996383f8f292021-08-24 18:26:52 +0200320}
321
322func (s *killSwitchServiceServer) DeleteAuthorizedUser(ctx context.Context, req *pb.DeleteAuthorizedUserRequest) (*pb.DeleteAuthorizedUserResponse, error) {
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +0200323 // This method requires authentication
324 authenticatedUser, err := getAuthenticatedUser(s, ctx)
325 if err != nil {
326 return nil, err
327 }
328 err = errorWhenMissingAccess(pb.KillSwitchAuthorizedUser_ACCESS_LEVEL_ADMIN, authenticatedUser)
329 if err != nil {
330 return nil, err
331 }
332
Adrià Vilanova Martínez25e12112021-08-25 13:48:06 +0200333 if req.GetUserId() == 0 {
334 return nil, status.Errorf(codes.InvalidArgument, "user_id must be greater than 0.")
335 }
336
Adrià Vilanova Martínezc147b6a2021-09-01 17:25:38 +0200337 err = db.DeleteAuthorizedUser(s.dbPool, ctx, req.UserId, authenticatedUser)
Adrià Vilanova Martínez25e12112021-08-25 13:48:06 +0200338 if err != nil {
339 return nil, status.Errorf(codes.Unavailable, err.Error())
340 }
341 res := &pb.DeleteAuthorizedUserResponse{}
342 return res, nil
avm9996383f8f292021-08-24 18:26:52 +0200343}
344
345func main() {
346 flag.Parse()
347
Adrià Vilanova Martínezf7ee6582021-09-02 21:53:28 +0200348 lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", *port))
avm9996383f8f292021-08-24 18:26:52 +0200349 if err != nil {
350 log.Fatalf("Failed to listen: %v", err)
351 }
352
353 grpcServer := grpc.NewServer()
354 pb.RegisterKillSwitchServiceServer(grpcServer, newKillSwitchServiceServer())
355 // Register reflection service on gRPC server.
356 reflection.Register(grpcServer)
357 grpcServer.Serve(lis)
358}