package api_impl import ( "database/sql" "net/http" "atlas9.dev/c/core" "atlas9.dev/c/core/dbi" "atlas9.dev/c/core/iam" "atlas9.dev/c/demo/api" "atlas9.dev/c/demo/lib/access" ) type TenantsImpl struct { DB *sql.DB Guard access.Guard Tenants dbi.Factory[iam.TenantStore] Grants dbi.Factory[iam.GrantStore] Audit dbi.Factory[iam.AuditStore] } func (s *TenantsImpl) ServeMux(mux *http.ServeMux) { mux.HandleFunc(api.Path_Tenants_Create, s.Create) mux.HandleFunc(api.Path_Tenants_Update, s.Update) mux.HandleFunc(api.Path_Tenants_Get, s.Get) mux.HandleFunc(api.Path_Tenants_Delete, s.Delete) mux.HandleFunc(api.Path_Tenants_List, s.List) } func (s *TenantsImpl) Create(w http.ResponseWriter, r *http.Request) { var req api.Tenants_CreateReq if read(w, r, &req) { return } if checkSystem(w, r, s.Guard, iam.CapTenantsCreate) { return } ctx := r.Context() userID, err := core.ParseID(iam.GetPrincipal(ctx).Subject) if err != nil { writeErr(ctx, w, iam.ErrForbidden) return } err = dbi.ReadWrite(ctx, s.DB, func(tx dbi.DBI) error { if err := s.Tenants(tx).Create(ctx, &req.Tenant); err != nil { return err } // Establish the caller as owner of the newly-created tenant so they // can subsequently update it. // No tenant-scoped grants exist yet on this fresh ID, so escalate. sysCtx := access.PutSystem(ctx, iam.CapGrantsAdd) if err := s.Grants(tx).Add(sysCtx, iam.Grant{ Tenant: req.Tenant.ID, Type: iam.GrantTypeUser, Principal: userID.String(), Role: "owner", }); err != nil { return err } return audit(ctx, s.Audit(tx), iam.AuditEntry{ Tenant: req.Tenant.ID, Action: "Tenants_Create", Resource: req.Tenant.ID.String(), Detail: req.Tenant.Name, }) }) write(ctx, w, err, api.Tenants_CreateRes{Tenant: req.Tenant}) } func (s *TenantsImpl) Update(w http.ResponseWriter, r *http.Request) { var req api.Tenants_UpdateReq if read(w, r, &req) { return } if check(w, r, s.Guard, iam.CapTenantsUpdate, req.Tenant.ID, "") { return } ctx := r.Context() err := dbi.ReadWrite(ctx, s.DB, func(tx dbi.DBI) error { if err := s.Tenants(tx).Update(ctx, &req.Tenant); err != nil { return err } return audit(ctx, s.Audit(tx), iam.AuditEntry{ Tenant: req.Tenant.ID, Action: "Tenants_Update", Resource: req.Tenant.ID.String(), Detail: req.Tenant.Name, }) }) write(ctx, w, err, api.Tenants_UpdateRes{Tenant: req.Tenant}) } func (s *TenantsImpl) Get(w http.ResponseWriter, r *http.Request) { // Read and validate the request body var req api.Tenants_GetReq if read(w, r, &req) { return } // Check access if check(w, r, s.Guard, iam.CapTenantsRead, req.ID, "") { return } // Get tenant from database ctx := r.Context() var res api.Tenants_GetRes err := dbi.ReadOnly(ctx, s.DB, func(tx dbi.DBI) error { return s.Tenants(tx).Get(ctx, req.ID, &res.Tenant) }) write(ctx, w, err, res) } func (s *TenantsImpl) Delete(w http.ResponseWriter, r *http.Request) { // Read and validate the request body var req api.Tenants_DeleteReq if read(w, r, &req) { return } // Check access if check(w, r, s.Guard, iam.CapTenantsDelete, req.ID, "") { return } // Delete the record from the database ctx := r.Context() err := dbi.ReadWrite(ctx, s.DB, func(tx dbi.DBI) error { if err := s.Tenants(tx).Delete(ctx, req.ID); err != nil { return err } return audit(ctx, s.Audit(tx), iam.AuditEntry{ Tenant: req.ID, Action: "Tenants_Delete", Resource: req.ID.String(), }) }) write(ctx, w, err, nil) } func (s *TenantsImpl) List(w http.ResponseWriter, r *http.Request) { // Read and validate the request body var req api.Tenants_ListReq if read(w, r, &req) { return } // No cap check here: the store filters by the caller's grants when the // caller lacks system CapTenantsRead, so any authenticated user can list // only the tenants they belong to. // Load the data from the database ctx := r.Context() var res api.Tenants_ListRes err := dbi.ReadOnly(ctx, s.DB, func(tx dbi.DBI) error { return s.Tenants(tx).List(ctx, req.Page, &res.Page) }) write(ctx, w, err, res) }